Skip to content

Commit

Permalink
1.35.0 Improve energy efficiency using React Compiler
Browse files Browse the repository at this point in the history
Improve energy efficiency
  • Loading branch information
matthijsgroen authored Jan 11, 2025
2 parents 9032183 + eb500c5 commit fbf2917
Show file tree
Hide file tree
Showing 35 changed files with 823 additions and 304 deletions.
275 changes: 242 additions & 33 deletions .pnp.cjs

Large diffs are not rendered by default.

Binary file modified .yarn/install-state.gz
Binary file not shown.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## Version 1.35.0 - 2025-01-11

### Added

- Build app using React Compiler, making game more power efficient

## Version 1.34.1 - 2025-01-04

### Fixed
Expand Down
15 changes: 13 additions & 2 deletions eslint.config.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,28 @@
import eslintConfigPrettier from "eslint-config-prettier";
import * as importPlugin from "eslint-plugin-import";
import eslintPluginPrettierRecommended from "eslint-plugin-prettier/recommended";
import pluginReact from "eslint-plugin-react";
import reactCompiler from "eslint-plugin-react-compiler";
import simpleImportSort from "eslint-plugin-simple-import-sort";
import unusedImports from "eslint-plugin-unused-imports";
import globals from "globals";
import tseslint from "typescript-eslint";

export default [
{
plugins: {
"react-compiler": reactCompiler
},
rules: {
"react-compiler/react-compiler": "error"
}
},
{ files: ["**/*.{js,mjs,cjs,ts,jsx,tsx}"] },
{ ignores: [".yarn/*", "dist/*", ".pnp.*", "storybook-static/", "public/*"] },
{ languageOptions: { globals: { ...globals.browser, ...globals.commonjs } } },
...tseslint.configs.recommended,
{
...pluginReact.configs.flat.recommended,
...pluginReact.configs.flat?.recommended,
settings: {
react: {
version: "detect"
Expand Down Expand Up @@ -113,5 +123,6 @@ export default [
}
]
}
}
},
eslintPluginPrettierRecommended
];
8 changes: 5 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "block-sorting",
"version": "1.34.1",
"version": "1.35.0",
"description": "Mobile Block sorting game, as a Progressive Web App",
"author": {
"name": "Matthijs Groen",
Expand Down Expand Up @@ -30,8 +30,8 @@
"localforage": "^1.10.0",
"pako": "^2.1.0",
"qrcode": "^1.5.4",
"react": "19.0.0-rc-d6cb4e77-20240911",
"react-dom": "19.0.0-rc-d6cb4e77-20240911"
"react": "19.0.0",
"react-dom": "19.0.0"
},
"devDependencies": {
"@babel/core": "^7.26.0",
Expand Down Expand Up @@ -60,12 +60,14 @@
"@vitejs/plugin-react": "^4.3.4",
"ansi-colors": "^4.1.3",
"autoprefixer": "^10.4.20",
"babel-plugin-react-compiler": "19.0.0-beta-63e3235-20250105",
"commander": "^12.1.0",
"eslint": "^9.15.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-import": "^2.31.0",
"eslint-plugin-prettier": "^5.2.1",
"eslint-plugin-react": "^7.37.2",
"eslint-plugin-react-compiler": "19.0.0-beta-63e3235-20250105",
"eslint-plugin-react-hooks": "^5.1.0-rc.0",
"eslint-plugin-react-refresh": "^0.4.14",
"eslint-plugin-simple-import-sort": "^12.1.1",
Expand Down
4 changes: 2 additions & 2 deletions postcss.config.cjs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
autoprefixer: {}
}
};
2 changes: 1 addition & 1 deletion prettier.config.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,5 @@
module.exports = {
plugins: [require.resolve("prettier-plugin-tailwindcss")],
tailwindFunctions: ["clsx"],
trailingComma: "none",
trailingComma: "none"
};
6 changes: 3 additions & 3 deletions pwa-assets.config.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import {
defineConfig,
minimal2023Preset as preset,
minimal2023Preset as preset
} from "@vite-pwa/assets-generator/config";

export default defineConfig({
headLinkOptions: {
preset: "2023",
preset: "2023"
},
preset,
images: ["public/app-logo.png"],
images: ["public/app-logo.png"]
});
2 changes: 1 addition & 1 deletion src/data/levelSeeds.ts

Large diffs are not rendered by default.

12 changes: 10 additions & 2 deletions src/game/factories.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import { solvers } from "./level-creation/solvers";
import { LimitColor } from "./blocks";
import { Block, BlockColor, Column, LevelState } from "./types";

export const createLevelState = (columns: Column[]): LevelState => {
export const createLevelState = (
columns: Column[],
solver: keyof typeof solvers = "default"
): LevelState => {
const colors = columns.reduce<BlockColor[]>(
(r, c) =>
c.blocks.reduce<BlockColor[]>(
Expand All @@ -12,11 +16,15 @@ export const createLevelState = (columns: Column[]): LevelState => {
);
colors.sort();

return {
const state: LevelState = {
colors,
columns,
moves: []
};
if (solver !== "default") {
state.solver = solver;
}
return state;
};

export const createPlacementColumn = (
Expand Down
169 changes: 169 additions & 0 deletions src/game/level-creation/configurable-solver.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
import { moveBlocks } from "../actions";
import { hasWon, isStuck } from "../state";
import { LevelState, Move } from "../types";

import { Tactic, WeightedMove } from "./solver-tactics/types";
import { Solver } from "./types";

const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
const MAX_LEVEL_MOVES = 2_000; // The dumb moves get filtered out, so this is a safe upper limit

export const FAIL_STATE: [beaten: boolean, moves: Move[], cost: number] = [
false,
[],
0
];

export const configureSolver =
(
tactics: Tactic[],
scoreState: (level: LevelState) => number,
scoreStateWithMove: (state: LevelState, move: Move) => number,
lookAheadCount: number
): Solver =>
async (level, random = Math.random, displayState) => {
let playLevel = level;
const moves: Move[] = [];

while (!isStuck(playLevel)) {
const nextMove = evaluateBestMove(
playLevel,
tactics,
scoreStateWithMove,
scoreState,
lookAheadCount,
random
);

if (!nextMove) {
break;
} else {
if (moves.length > MAX_LEVEL_MOVES) {
break;
}

moves.push({
from: nextMove.move.from,
to: nextMove.move.to,
tactic: nextMove.name
});

playLevel = moveBlocks(playLevel, nextMove.move);
if (displayState) {
const keepSolving = await displayState(
playLevel,
nextMove.move,
nextMove.name ?? "unknown",
moves.length
);
if (!keepSolving) {
return FAIL_STATE;
}
}
if (moves.length % 30 === 0) {
await delay(2);
}
}
if (hasWon(playLevel)) {
return [true, moves, moves.length + MAX_LEVEL_MOVES];
}
}

return FAIL_STATE;
};

const lookahead = (
state: LevelState,
move: Move,
depth: number,
tactics: Tactic[],
scoreStateWithMove: (state: LevelState, move: Move) => number,
scoreState: (state: LevelState) => number,
random = Math.random
): number => {
if (depth === 0) {
return scoreStateWithMove(state, move); // Base case: return the score of the current state
}
const nextState = moveBlocks(state, move);

const moves = generatePossibleMoves(state, tactics, random);
if (moves.length === 0) {
return scoreState(state); // No more moves available, return score
}

let bestScore = -Infinity;

for (const move of moves) {
const score = lookahead(
nextState,
move.move,
depth - 1,
tactics,
scoreStateWithMove,
scoreState,
random
); // Recursive lookahead
bestScore = Math.max(bestScore, score); // Track the best score
}

return bestScore;
};

const removeDoubleMoves = (
move: WeightedMove,
index: number,
list: WeightedMove[]
) =>
list.findIndex(
(m2) => m2.move.from === move.move.from && m2.move.to === move.move.to
) === index;

const generatePossibleMoves = (
state: LevelState,
tactics: Tactic[],
random = Math.random
): WeightedMove[] =>
tactics
.reduce<WeightedMove[]>(
(r, tactic) =>
r.concat(
tactic(state, random)
.sort((a, b) => a.weight - b.weight)
.slice(0, 3)
),
[]
)
.filter(removeDoubleMoves);

const evaluateBestMove = (
initialState: LevelState,
tactics: Tactic[],
scoreStateWithMove: (state: LevelState, move: Move) => number,
scoreState: (state: LevelState) => number,
lookAheadCount: number,
random = Math.random
): WeightedMove | null => {
const possibleMoves = generatePossibleMoves(initialState, tactics, random);

let bestMove: WeightedMove | null = null;
let bestScore = -Infinity;

for (const move of possibleMoves) {
const moveScore = lookahead(
initialState,
move.move,
lookAheadCount,
tactics,
scoreStateWithMove,
scoreState,
random
);

if (moveScore > bestScore) {
bestScore = moveScore;
bestMove = move;
}
}

return bestMove;
};
3 changes: 2 additions & 1 deletion src/game/level-creation/generateRandomLevel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export const generateRandomLevel = (
blockColorPick = "start",
hideBlockTypes = "none",
stacksPerColor = 1,
solver = "default",
layoutMap
}: LevelSettings,
random: () => number
Expand Down Expand Up @@ -101,7 +102,7 @@ export const generateRandomLevel = (
})
);

const levelState = createLevelState(columns);
const levelState = createLevelState(columns, solver);
return applyLayoutMap(levelState, layoutMap);
};

Expand Down
5 changes: 4 additions & 1 deletion src/game/level-creation/scoreState.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import { moveBlocks } from "../actions";
import { Column, LevelState, Move } from "../types";

import { canPlaceBlock, isColumnCorrectlySorted } from "./tactics/support";
import {
canPlaceBlock,
isColumnCorrectlySorted
} from "./solver-tactics/support";

const columnCompletionScore = (state: LevelState): number =>
state.columns.reduce((score, col) => {
Expand Down
File renamed without changes.
17 changes: 17 additions & 0 deletions src/game/level-creation/solvers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { randomMove } from "./solver-tactics/randomMove";
import { stackColumn } from "./solver-tactics/stackColumn";
import { startColumn } from "./solver-tactics/startColumn";
import { configureSolver } from "./configurable-solver";
import { scoreState, scoreStateWithMove } from "./scoreState";
import { Solver } from "./types";

export const defaultSolver: Solver = configureSolver(
[randomMove, startColumn, stackColumn],
scoreState,
scoreStateWithMove,
2
);

export const solvers = {
default: defaultSolver
} as const satisfies Record<string, Solver>;
Loading

0 comments on commit fbf2917

Please sign in to comment.