diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index a0c9c36..2b11501 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -54,7 +54,7 @@ jobs:
- name: Build solver (standalone)
working-directory: apps/solver
- run: bun build --minify --sourcemap --compile --bytecode --target=bun-${{ matrix.target }} --outfile=dist/solver-${{ matrix.outfile }} $(find dist -name '*.js')
+ run: bun build --minify --sourcemap --compile --bytecode --target=bun-${{ matrix.target }} --outfile=dist/solver-${{ matrix.outfile }} dist/index.js $(find dist -name '*.js' -not -name 'index.js')
- name: Build mock server
working-directory: apps/mock-server
diff --git a/.gitignore b/.gitignore
index f7835fe..ae01bd9 100644
--- a/.gitignore
+++ b/.gitignore
@@ -280,3 +280,6 @@ poetry.toml
# LSP config files
pyrightconfig.json
+
+# Profiling data
+*.prof
diff --git a/apps/frontend/package.json b/apps/frontend/package.json
index 0c2c5ca..40f18c3 100644
--- a/apps/frontend/package.json
+++ b/apps/frontend/package.json
@@ -21,6 +21,7 @@
},
"dependencies": {
"@data-maki/schemas": "workspace:*",
+ "@radix-ui/colors": "^3.0.0",
"@remix-run/cloudflare": "2.12.0",
"@remix-run/cloudflare-pages": "2.12.0",
"@remix-run/react": "2.12.0",
@@ -29,6 +30,7 @@
"dataclass": "^2.1.1",
"eventsource-parser": "^2.0.1",
"hono": "^4.6.3",
+ "inter-ui": "^4.0.2",
"isbot": "^4.4.0",
"jotai": "^2.10.0",
"luxon": "^3.5.0",
diff --git a/apps/frontend/src/app/entry.server.tsx b/apps/frontend/src/app/entry.server.tsx
index f4ad399..6ba01b0 100644
--- a/apps/frontend/src/app/entry.server.tsx
+++ b/apps/frontend/src/app/entry.server.tsx
@@ -14,14 +14,6 @@ const indexHtml = `
Data Maki UI
-
-
-
-
-
-
-
-
@@ -33,7 +25,7 @@ export default async function handleRequest(
responseStatusCode: number,
responseHeaders: Headers,
remixContext: EntryContext,
- loadContext: AppLoadContext,
+ _loadContext: AppLoadContext,
) {
const appHtml = renderToString(
);
diff --git a/apps/frontend/src/app/index.css b/apps/frontend/src/app/index.css
index 2bfc725..4c4d73a 100644
--- a/apps/frontend/src/app/index.css
+++ b/apps/frontend/src/app/index.css
@@ -7,7 +7,7 @@
@supports (font-variation-settings: normal) {
:root {
- font-family: "Inter var", sans-serif;
+ font-family: "InterVariable", sans-serif;
}
}
diff --git a/apps/frontend/src/app/root.tsx b/apps/frontend/src/app/root.tsx
index 204cebd..423126e 100644
--- a/apps/frontend/src/app/root.tsx
+++ b/apps/frontend/src/app/root.tsx
@@ -6,7 +6,15 @@ import type { PropsWithChildren } from "react";
import { Footer } from "./components/layout/Footer";
import { Header } from "./components/layout/Header";
-import "./index.css";
+import radixBlue from "@radix-ui/colors/blue.css?url";
+import radixGreen from "@radix-ui/colors/green.css?url";
+import radixMauve from "@radix-ui/colors/mauve.css?url";
+import radixOrange from "@radix-ui/colors/orange.css?url";
+import radixRed from "@radix-ui/colors/red.css?url";
+import radixYellow from "@radix-ui/colors/yellow.css?url";
+import interVariableStyle from "inter-ui/inter-variable.css?url";
+import interStyle from "inter-ui/inter.css?url";
+import globalStyle from "./index.css?url";
export function HydrateFallback() {
return (
@@ -20,6 +28,12 @@ export function HydrateFallback() {
export function Layout({ children }: PropsWithChildren) {
return (
<>
+
+
+ {[radixMauve, radixRed, radixBlue, radixGreen, radixYellow, radixOrange].map((style) => (
+
+ ))}
+
{children}
>
diff --git a/apps/frontend/src/app/routes/replay.tsx b/apps/frontend/src/app/routes/replay.tsx
index 6f8169a..c15e5ed 100644
--- a/apps/frontend/src/app/routes/replay.tsx
+++ b/apps/frontend/src/app/routes/replay.tsx
@@ -31,12 +31,22 @@ export default function Page() {
const boards: string[][] = [structuredClone(replayInfo.problem.board.start)];
- for (const op of replayInfo.answer.ops) {
- const afterBoard = easyKatanuki(replayInfo.problem, op);
+ for (const [i, op] of replayInfo.answer.ops.entries()) {
+ try {
+ const afterBoard = easyKatanuki(replayInfo.problem, op);
- replayInfo.problem.board.start = afterBoard;
+ replayInfo.problem.board.start = afterBoard;
- boards.push(afterBoard);
+ boards.push(afterBoard);
+ } catch (e) {
+ if (e instanceof Error) {
+ throw new Error(`Failed to apply operation ${i + 1}`, { cause: e });
+ }
+
+ console.error(e);
+
+ throw new Error(`Failed to apply operation ${i + 1}`);
+ }
}
replayInfo.problem.board.start = boards[0];
@@ -98,31 +108,39 @@ export default function Page() {
{(board && extraOpInfo) || turn === turns - 1 ? (
<>
-
-
- Delta
-
- {delta ? (
-
-
-
- {delta[0]}
-
-
-
- {delta[1]}
-
-
-
- {delta[2]}
-
-
-
- {delta[3]}
-
-
- ) : null}
-
+
+
+
+ Delta
+
+ {delta && (
+
+
+
+ {delta[0]}
+
+
+
+ {delta[1]}
+
+
+
+ {delta[2]}
+
+
+
+ {delta[3]}
+
+
+ )}
+
+
+
+ Pattern
+
+ {replayInfo.answer.ops[turn] ? replayInfo.answer.ops[turn].p : "None"}
+
+
Start
diff --git a/apps/solver/package.json b/apps/solver/package.json
index caf5a67..1e96a21 100644
--- a/apps/solver/package.json
+++ b/apps/solver/package.json
@@ -21,11 +21,11 @@
"axios": "^1.7.7",
"comlink": "^4.4.1",
"fast-deep-equal": "^3.1.3",
- "hono": "^4.6.3",
+ "hono": "^4.6.5",
"hono-pino": "^0.3.0",
"loglayer": "^4.8.0",
- "pino": "^9.4.0",
- "pino-pretty": "^11.2.2",
+ "pino": "^9.5.0",
+ "pino-pretty": "^11.3.0",
"pino-roll": "^2.1.0",
"reactive-channel": "^3.1.2",
"universal-stores": "^2.4.3",
diff --git a/apps/solver/scripts/build.ts b/apps/solver/scripts/build.ts
index 6ccb7aa..a9e4134 100644
--- a/apps/solver/scripts/build.ts
+++ b/apps/solver/scripts/build.ts
@@ -26,9 +26,8 @@ const copyFiles = async (from: string[], to: string) => {
console.time("Finished building solver");
-await Bun.build({
+const results = await Bun.build({
entrypoints: ["./src/index.ts"],
- splitting: true,
sourcemap: "linked",
plugins: [
typia({
@@ -46,6 +45,13 @@ await Bun.build({
},
});
+if (!results.success) {
+ console.error("Failed to build solver");
+ console.error(results.logs);
+
+ process.exit(1);
+}
+
console.log("Copying files...");
const files = await Array.fromAsync(new Glob("../../packages/algorithm/dist/workers/*.worker.js").scan());
diff --git a/apps/solver/scripts/hotfix-run.sh b/apps/solver/scripts/hotfix-run.sh
new file mode 100644
index 0000000..a286865
--- /dev/null
+++ b/apps/solver/scripts/hotfix-run.sh
@@ -0,0 +1,4 @@
+bun run build
+bun build --minify --compile --target=bun-linux-x64 --outfile=dist/solver-hotfix-linux-x64 dist/index.js $(find dist -name '*.js' -not -name 'index.js')
+
+dist/solver-hotfix-linux-x64
diff --git a/apps/solver/src/features/algorithm/index.ts b/apps/solver/src/features/algorithm/index.ts
index 8485f9f..65ddc4c 100644
--- a/apps/solver/src/features/algorithm/index.ts
+++ b/apps/solver/src/features/algorithm/index.ts
@@ -1,6 +1,6 @@
import { ALGO_VERSION } from "@/constants/env";
import { LATEST_VERSION, type SolveFunc, getSolveFunc, isSolveFuncVersion } from "@data-maki/algorithm";
-import type { UIMessageEventBase } from "@data-maki/schemas";
+import type { Answer, Problem, UIMessageEventBase } from "@data-maki/schemas";
import type { SolveStartEvent } from "@data-maki/schemas";
import type { SolveProgressEvent } from "@data-maki/schemas";
import type { SolveFinishedEvent } from "@data-maki/schemas";
@@ -9,6 +9,7 @@ import type { LogLayer } from "loglayer";
import type { ChannelTx, ReadonlyStore } from "reactive-channel";
import { span } from "../../logging";
import { DoneState } from "../../state/done.ts";
+import { IdleState } from "../../state/idle.ts";
import { StateManager } from "../../state/manager.ts";
import { SolvingState } from "../../state/solving.ts";
import { FeatureBase } from "../base";
@@ -53,6 +54,36 @@ export class AlgorithmFeature extends FeatureBase {
}
}
+ private async submitAnswer(id: string, problem: Problem, answer: Answer, finalBoard?: string[]) {
+ let correct = true;
+
+ if (finalBoard) {
+ correct = deepEqual(finalBoard, problem.board.goal);
+
+ if (correct) {
+ this.log
+ .withMetadata({
+ id,
+ turns: answer.ops.length,
+ })
+ .info("Answer is correct");
+ } else {
+ this.log
+ .withMetadata({
+ id,
+ turns: answer.ops.length,
+ expected: problem.board.goal,
+ actual: finalBoard,
+ })
+ .error("Answer is incorrect");
+ }
+ }
+
+ const revision = await this.serverComm.submitAnswer(id, answer);
+
+ return [correct, revision] as const;
+ }
+
init() {}
async start() {
@@ -80,40 +111,36 @@ export class AlgorithmFeature extends FeatureBase {
board: solvingState.problem.board,
general: solvingState.problem.general,
} satisfies SolveStartEvent);
+
+ solvingState.workers = workers;
},
- (workerId, turns) => {
+ async (workerId, answer) => {
this.sendEvent({
eventName: "solve.progress",
solveId: solvingState.id,
workerId,
- turns,
+ turns: answer.n,
} satisfies SolveProgressEvent);
- },
- );
- const correct = deepEqual(finalBoard, solvingState.problem.board.goal);
+ if (solvingState.currentShortestAnswer === null || answer.n < solvingState.currentShortestAnswer.n) {
+ solvingState.currentShortestAnswer = answer;
- if (correct) {
- this.log
- .withMetadata({
- id: solvingState.id,
- turns: answer.ops.length,
- })
- .info("Answer is correct");
- } else {
- this.log
- .withMetadata({
- id: solvingState.id,
- turns: answer.ops.length,
- expected: solvingState.problem.board.goal,
- actual: finalBoard,
- })
- .error("Answer is incorrect");
- }
+ const [, revision] = await this.submitAnswer(solvingState.id, solvingState.problem, answer);
+
+ this.log
+ .withMetadata({
+ id: solvingState.id,
+ turns: answer.ops.length,
+ revision,
+ })
+ .info("Answer updated");
+ }
+ },
+ );
scope.end();
- const revision = await this.serverComm.submitAnswer(solvingState.id, solvingState.problem, answer);
+ const [correct, revision] = await this.submitAnswer(solvingState.id, solvingState.problem, answer, finalBoard);
this.sendEvent({
eventName: "solve.finished",
@@ -124,6 +151,12 @@ export class AlgorithmFeature extends FeatureBase {
} satisfies SolveFinishedEvent);
StateManager.instance.setState(new DoneState(solvingState.id, answer));
+
+ setTimeout(() => {
+ StateManager.instance.setState(IdleState.instance);
+
+ IdleState.instance.oldProblem = solvingState.problem;
+ }, 1000);
});
}
}
diff --git a/apps/solver/src/features/server-comm/index.ts b/apps/solver/src/features/server-comm/index.ts
index bc1b04d..2349b4e 100644
--- a/apps/solver/src/features/server-comm/index.ts
+++ b/apps/solver/src/features/server-comm/index.ts
@@ -90,7 +90,7 @@ export class ServerCommunicatorFeature extends FeatureBase {
}
}
- async submitAnswer(id: string, problem: Problem, answer: Answer): Promise {
+ async submitAnswer(id: string, answer: Answer): Promise {
const scope = span(
this.log.withMetadata({
answerId: id,
@@ -108,12 +108,6 @@ export class ServerCommunicatorFeature extends FeatureBase {
scope.end();
- setTimeout(() => {
- StateManager.instance.setState(IdleState.instance);
-
- IdleState.instance.oldProblem = problem;
- }, 1000);
-
return revision;
}
diff --git a/apps/solver/src/features/ui-comm/route.ts b/apps/solver/src/features/ui-comm/route.ts
index 45e1254..3e096b0 100644
--- a/apps/solver/src/features/ui-comm/route.ts
+++ b/apps/solver/src/features/ui-comm/route.ts
@@ -39,7 +39,7 @@ export const createRouteDefinition = (app: Hono) =>
eventToSSE({
eventName: "solve.start",
solveId: solvingState.id,
- workers: 1, // TODO: Implement multi-worker solving
+ workers: solvingState.workers,
startedAt: solvingState.startedAt,
board: solvingState.problem.board,
general: solvingState.problem.general,
diff --git a/apps/solver/src/logging/index.ts b/apps/solver/src/logging/index.ts
index 96e819b..6e58237 100644
--- a/apps/solver/src/logging/index.ts
+++ b/apps/solver/src/logging/index.ts
@@ -1,4 +1,3 @@
-import { resolve } from "node:path";
import { type ILogBuilder, LogLayer, LoggerType } from "loglayer";
import pino, { type P } from "pino";
import type PinoPretty from "pino-pretty";
@@ -26,7 +25,7 @@ export const internalPinoLogger = pino({
{
target: "pino-roll",
options: {
- file: resolve("logs", "solver"),
+ file: `${process.cwd()}/logs/solver`,
extension: ".log",
frequency: "daily",
dateFormat: "yyyy-MM-dd",
diff --git a/apps/solver/src/state/solving.ts b/apps/solver/src/state/solving.ts
index f06c4c7..375032a 100644
--- a/apps/solver/src/state/solving.ts
+++ b/apps/solver/src/state/solving.ts
@@ -1,9 +1,12 @@
-import type { Problem } from "@data-maki/schemas";
+import type { Answer, Problem } from "@data-maki/schemas";
import { StateBase } from "./base.ts";
export class SolvingState extends StateBase {
static readonly stateName = "Solving";
+ workers = 1;
+ currentShortestAnswer: Answer | null = null;
+
constructor(
readonly id: string,
readonly startedAt: Date,
diff --git a/bun.lockb b/bun.lockb
index 16d14b6..55fd17e 100755
Binary files a/bun.lockb and b/bun.lockb differ
diff --git a/package.json b/package.json
index 035f7ec..0b0f8dc 100644
--- a/package.json
+++ b/package.json
@@ -19,7 +19,7 @@
},
"dependencies": {
"@biomejs/biome": "^1.9.3",
- "@types/bun": "^1.1.10",
+ "@types/bun": "^1.1.11",
"cross-env": "^7.0.3",
"lefthook": "^1.7.18",
"npm-run-all2": "^6.2.3",
@@ -27,7 +27,7 @@
"turbo": "^2.1.3",
"type-fest": "^4.26.1",
"typescript": "5.5.2",
- "typia": "^6.11.1"
+ "typia": "^6.11.2"
},
"trustedDependencies": [
"@biomejs/biome",
diff --git a/packages/algorithm/README.md b/packages/algorithm/README.md
index c693b9b..a8234ef 100644
--- a/packages/algorithm/README.md
+++ b/packages/algorithm/README.md
@@ -8,6 +8,11 @@ Follow the root monorepo README.md for setup instructions.
## Changelog
+## v2 (2024-10-16)
+
+- Evaluation-based selection
+- Threaded parallelism
+
## v1 (2024-10-06)
- Initial version
diff --git a/packages/algorithm/src/index.ts b/packages/algorithm/src/index.ts
index 571796d..92339cd 100644
--- a/packages/algorithm/src/index.ts
+++ b/packages/algorithm/src/index.ts
@@ -1,12 +1,14 @@
import type { Answer, Problem } from "@data-maki/schemas";
import { solve as solveV1 } from "./workers/v1.master";
+import { solve as solveV2 } from "./workers/v2.master";
export { easyKatanuki } from "./katanuki";
-export const VERSIONS = ["v1"] as const;
+export const VERSIONS = ["v1", "v2"] as const;
const solveFuncs: { [key in (typeof VERSIONS)[number]]: SolveFunc } = {
v1: solveV1,
+ v2: solveV2,
};
export const isSolveFuncVersion = (version: string): version is (typeof VERSIONS)[number] =>
@@ -17,6 +19,6 @@ export const LATEST_VERSION = VERSIONS[VERSIONS.length - 1];
export type SolveFunc = (
problem: Problem,
- onStartWorker: ((totalWorkers: number) => void) | undefined,
- onWorkerFinish: ((workerId: number, turns: number) => void) | undefined,
+ onStartWorker?: ((totalWorkers: number) => void | Promise) | undefined,
+ onWorkerFinish?: ((workerId: number, answer: Answer) => void | Promise) | undefined,
) => Promise<[answer: Answer, board: string[]]>;
diff --git a/packages/algorithm/src/katanuki.ts b/packages/algorithm/src/katanuki.ts
index 669f61a..238295f 100644
--- a/packages/algorithm/src/katanuki.ts
+++ b/packages/algorithm/src/katanuki.ts
@@ -1,22 +1,68 @@
-import type { Board, Op, Problem } from "@data-maki/schemas";
+import type { Op, Problem } from "@data-maki/schemas";
import { cellsToBoard } from "./models/answer";
-import { type InternalPattern, getPattern } from "./models/pattern";
-import { boardToCells } from "./models/problem";
-import { type Context, DOWN, type Direction, LEFT, type Point, UP } from "./types";
-import { type TwoDimensionalCells, dbgCells, reverseCells } from "./utils/arrays";
+import { getPattern } from "./models/pattern";
+import { type Context, DOWN, type Direction, LEFT, type Point, RIGHT, UP } from "./types";
+import { type TwoDimensionalCells, dbgCells, multiReverse, reverseCells } from "./utils/arrays";
import { countElementsColumnWise } from "./utils/board";
import { createContext } from "./v1";
import { dbg } from "./workers/log";
-export const addOp = (c: Context, ops: Op) => {
+export const addOp = (c: Context, op: Op) => {
+ let { p, x, y, s } = op;
+
+ const pattern = getPattern(op.p, c.patterns);
+
+ if (c.rvOp.hasReverse90) {
+ const tmp = x;
+ x = y;
+ y = tmp;
+
+ if (c.rvOp.hasReverseUpDown) {
+ y = c.width - y - 1;
+ y -= pattern.height - 1;
+
+ if (s === LEFT) s = RIGHT;
+ else if (s === RIGHT) s = LEFT;
+ }
+
+ if (c.rvOp.hasReverseLeftRight) {
+ x = c.height - x - 1;
+ x -= pattern.width - 1;
+
+ if (s === UP) s = DOWN;
+ else if (s === DOWN) s = UP;
+ }
+
+ if (s === UP) s = LEFT;
+ else if (s === DOWN) s = RIGHT;
+ else if (s === LEFT) s = UP;
+ else if (s === RIGHT) s = DOWN;
+ } else {
+ if (c.rvOp.hasReverseUpDown) {
+ y = c.height - y - 1;
+ y -= pattern.height - 1;
+
+ if (s === UP) s = DOWN;
+ else if (s === DOWN) s = UP;
+ }
+
+ if (c.rvOp.hasReverseLeftRight) {
+ x = c.width - x - 1;
+ x -= pattern.width - 1;
+
+ if (s === LEFT) s = RIGHT;
+ else if (s === RIGHT) s = LEFT;
+ }
+ }
+
c.n += 1;
- c.ops.push(ops);
+ c.ops.push({ p, x, y, s });
};
const generatePatternData = (
dir: Direction,
b: TwoDimensionalCells,
- pattern: InternalPattern,
+ pattern: TwoDimensionalCells,
w: number,
h: number,
x: number,
@@ -33,7 +79,7 @@ const generatePatternData = (
x: y,
y: x,
},
- pattern: reverseCells(pattern.cells, "reverse-90"),
+ pattern: reverseCells(pattern, "reverse-90"),
}
: /* dir === LEFT || dir === RIGHT */ {
b: b.clone(),
@@ -45,23 +91,33 @@ const generatePatternData = (
x,
y,
},
- pattern: pattern.cells,
+ pattern,
};
-export const katanuki = (c: Context, p: number, x: number, y: number, dir: Direction) => {
+export const katanukiBoard = (c: Context, p: number, x: number, y: number, s: Direction) => {
const pattern_ = getPattern(p, c.patterns);
- dbg(c.worker, "katanuki", { p: pattern_.p, x, y, dir });
+ dbg(c.worker, "katanuki", { p: pattern_.p, x, y, s });
if (x + pattern_.width <= 0 || x >= c.width || y + pattern_.height <= 0 || y >= c.height) {
- throw new Error("Cannot pick any cells: out of range");
+ throw new Error(
+ `Cannot pick any cells: out of range (x: ${c.width} > [${x}, ${x + pattern_.width}), y: ${c.height} > [${y}, ${y + pattern_.height})`,
+ );
}
// Looking point on the board
const l: Point = { x: 0, y: 0 };
// Stripe -> Reverse / Border -> Normal
- let { b, bx, by, pw, ph, pp, pattern } = generatePatternData(dir, c.board, pattern_, c.width, c.height, x, y);
+ let { b, bx, by, pw, ph, pp, pattern } = generatePatternData(
+ s,
+ c.board,
+ multiReverse(pattern_.cells, c.rvOp),
+ c.width,
+ c.height,
+ x,
+ y,
+ );
for (const i of Array(ph).keys()) {
l.y = pp.y + i;
@@ -84,7 +140,7 @@ export const katanuki = (c: Context, p: number, x: number, y: number, dir: Direc
picked.unshift(currentRow.splice(l.x, 1)[0]);
}
- if (dir === UP || dir === LEFT) {
+ if (s === UP || s === LEFT) {
currentRow.push(...picked);
} else {
currentRow.unshift(...picked);
@@ -93,32 +149,29 @@ export const katanuki = (c: Context, p: number, x: number, y: number, dir: Direc
b.setRow(l.y, currentRow);
}
- if (dir === UP || dir === DOWN) {
+ if (s === UP || s === DOWN) {
b = reverseCells(b, "reverse-90");
}
+ return b;
+};
+
+export const easyKatanuki = (problem: Problem, op: Op): string[] => {
+ const c = createContext(problem);
+
+ return cellsToBoard(katanukiBoard(c, op.p, op.x, op.y, op.s as Direction));
+};
+
+export const katanuki = (c: Context, p: number, x: number, y: number, s: Direction) => {
+ const b = katanukiBoard(c, p, x, y, s);
+
if (!c.board.equals(b)) {
c.board = b;
c.currentElementCounts = countElementsColumnWise(c.board, c.worker);
- const op: Op = {
- p: pattern_.p,
- x,
- y,
- s: dir,
- };
-
- addOp(c, op);
+ addOp(c, { p, x, y, s });
dbgCells(c.board, c.worker);
} else {
dbg(c.worker, c.board, b);
}
};
-
-export const easyKatanuki = (problem: Problem, op: Op): string[] => {
- const c = createContext(problem);
-
- katanuki(c, op.p, op.x, op.y, op.s as Direction);
-
- return cellsToBoard(c.board);
-};
diff --git a/packages/algorithm/src/types.ts b/packages/algorithm/src/types.ts
index ed87349..feacf95 100644
--- a/packages/algorithm/src/types.ts
+++ b/packages/algorithm/src/types.ts
@@ -13,15 +13,22 @@ export type Direction = typeof UP | typeof DOWN | typeof LEFT | typeof RIGHT;
export type CellCounts = FixedLengthArray;
+export interface ReverseOperationPatterns {
+ hasReverse90: boolean;
+ hasReverseUpDown: boolean;
+ hasReverseLeftRight: boolean;
+}
+
export interface Context {
worker?: Worker;
board: TwoDimensionalCells;
goalBoard: TwoDimensionalCells;
// Element counts per column
- currentElementCounts: Array>;
+ currentElementCounts: CellCounts[];
patterns: InternalPattern[];
width: number;
height: number;
+ rvOp: ReverseOperationPatterns;
// Answer
n: number;
diff --git a/packages/algorithm/src/utils/arrays.bench.ts b/packages/algorithm/src/utils/arrays.bench.ts
index 1480f78..9637b25 100644
--- a/packages/algorithm/src/utils/arrays.bench.ts
+++ b/packages/algorithm/src/utils/arrays.bench.ts
@@ -1,5 +1,5 @@
import { bench, describe } from "vitest";
-import { TwoDimensionalCells, reverseCells, reverseCellsInPlace } from "./arrays";
+import { TwoDimensionalCells, reverseCells } from "./arrays";
const createRandomTwoDimensionalCells = (x: number, y: number) =>
new TwoDimensionalCells(
@@ -69,16 +69,4 @@ describe("benchmark reverseCells", () => {
bench("reverse-left-right", () => {
reverseCells(list, "reverse-left-right");
});
-
- bench("in-place reverse-90", () => {
- reverseCellsInPlace(list, "reverse-90");
- });
-
- bench("in-place reverse-up-down", () => {
- reverseCellsInPlace(list, "reverse-up-down");
- });
-
- bench("in-place reverse-left-right", () => {
- reverseCellsInPlace(list, "reverse-left-right");
- });
});
diff --git a/packages/algorithm/src/utils/arrays.ts b/packages/algorithm/src/utils/arrays.ts
index 72b087d..1e056d6 100644
--- a/packages/algorithm/src/utils/arrays.ts
+++ b/packages/algorithm/src/utils/arrays.ts
@@ -1,5 +1,6 @@
import type { TypedArray } from "type-fest";
import { isUint8Array } from "uint8array-extras";
+import type { ReverseOperationPatterns } from "../types";
import { dbg } from "../workers/log";
const sliceJump = (arr: T, start: number, jump: number) => {
@@ -223,13 +224,54 @@ export const reverseCells = (cells: TwoDimensionalCells, op: ReverseOperation):
}
};
-export const reverseCellsInPlace = (cells: TwoDimensionalCells, op: ReverseOperation) => {
- switch (op) {
- case "reverse-90":
- return cells.transposeInPlace();
- case "reverse-up-down":
- return cells.reverseRowWiseInPlace();
- case "reverse-left-right":
- return cells.reverseColumnWiseInPlace();
+export const multiReverse = (cells_: TwoDimensionalCells, rvOp: ReverseOperationPatterns) => {
+ let cells = cells_;
+
+ if (rvOp.hasReverse90) {
+ cells = cells.transpose();
+
+ if (rvOp.hasReverseLeftRight) {
+ cells = cells.reverseRowWise();
+ }
+
+ if (rvOp.hasReverseUpDown) {
+ cells = cells.reverseColumnWise();
+ }
+ } else {
+ if (rvOp.hasReverseLeftRight) {
+ cells = cells.reverseColumnWise();
+ }
+
+ if (rvOp.hasReverseUpDown) {
+ cells = cells.reverseRowWise();
+ }
}
+
+ return cells;
+};
+
+export const multiDereverse = (cells_: TwoDimensionalCells, rvOp: ReverseOperationPatterns) => {
+ let cells = cells_;
+
+ if (rvOp.hasReverse90) {
+ if (rvOp.hasReverseLeftRight) {
+ cells = cells.reverseRowWise();
+ }
+
+ if (rvOp.hasReverseUpDown) {
+ cells = cells.reverseColumnWise();
+ }
+
+ cells = cells.transpose();
+ } else {
+ if (rvOp.hasReverseLeftRight) {
+ cells = cells.reverseColumnWise();
+ }
+
+ if (rvOp.hasReverseUpDown) {
+ cells = cells.reverseRowWise();
+ }
+ }
+
+ return cells;
};
diff --git a/packages/algorithm/src/utils/board.ts b/packages/algorithm/src/utils/board.ts
index d481df1..bccb95f 100644
--- a/packages/algorithm/src/utils/board.ts
+++ b/packages/algorithm/src/utils/board.ts
@@ -1,5 +1,4 @@
import type { CellCounts } from "../types";
-import { dbg } from "../workers/log";
import type { TwoDimensionalCells } from "./arrays";
export const zip = (a: T[], b: U[]): Array<[T, U]> => a.map((e, i) => [e, b[i]]);
diff --git a/packages/algorithm/src/v1/index.ts b/packages/algorithm/src/v1/index.ts
index b2fa66b..1305588 100644
--- a/packages/algorithm/src/v1/index.ts
+++ b/packages/algorithm/src/v1/index.ts
@@ -22,6 +22,11 @@ export const createContext = (problem: Problem, self?: Worker): Context => {
patterns: problem.general.patterns.map(patternToInternal),
width: problem.board.width,
height: problem.board.height,
+ rvOp: {
+ hasReverse90: false,
+ hasReverseUpDown: false,
+ hasReverseLeftRight: false,
+ },
n: 0,
ops: [],
};
diff --git a/packages/algorithm/src/v2.test.ts b/packages/algorithm/src/v2.test.ts
new file mode 100644
index 0000000..a98ecb2
--- /dev/null
+++ b/packages/algorithm/src/v2.test.ts
@@ -0,0 +1,21 @@
+import { describe, expect, test } from "bun:test";
+import type { Answer, Problem } from "@data-maki/schemas";
+import typia from "typia";
+import dataExample from "../examples/input.json";
+import { solve } from "./workers/v2.master";
+
+describe("algorithm v2 tests", () => {
+ let problem: Problem;
+ let answer: Answer;
+
+ test("example data correctly solves", async () => {
+ problem = typia.assert(dataExample);
+
+ const expected = problem.board.goal;
+ const [actualAnswer, actual] = await solve(structuredClone(problem));
+
+ answer = actualAnswer;
+
+ expect(actual).toStrictEqual(expected);
+ });
+});
diff --git a/packages/algorithm/src/v2/evaluation.ts b/packages/algorithm/src/v2/evaluation.ts
new file mode 100644
index 0000000..3abb4c7
--- /dev/null
+++ b/packages/algorithm/src/v2/evaluation.ts
@@ -0,0 +1,35 @@
+import { katanukiBoard } from "../katanuki";
+import { type CellCounts, type Context, LEFT, type Point, UP } from "../types";
+import { countElementsColumnWise, getDelta } from "../utils/board";
+import { dbg } from "../workers/log";
+
+export const evaluateRow = (c: Context, p: number, pp: Point, goalElementCounts: CellCounts) => {
+ const board = katanukiBoard(c, p, pp.x, pp.y, UP);
+ const elementCounts = countElementsColumnWise(board, c.worker);
+ const delta = getDelta(elementCounts[c.height - 1], goalElementCounts);
+
+ let value = 0;
+
+ for (const i of delta) {
+ if (i >= 0) value += i;
+ }
+
+ dbg(c.worker, `evaluate: p = ${p}, evaluation = ${value}`);
+
+ return value;
+};
+
+export const evaluateColumnPiece = (c: Context, p: number, pp: Point, goalColumn: number[]) => {
+ const board = katanukiBoard(c, p, pp.x, pp.y, LEFT);
+ const column = board.getColumn(c.width - 1);
+
+ let value = 0;
+
+ for (let i = 0; i < c.height; i++) {
+ if (column[i] !== goalColumn[i]) value++;
+ }
+
+ dbg(c.worker, `evaluate: p = ${p}, evaluation = ${value}`);
+
+ return value;
+};
diff --git a/packages/algorithm/src/v2/index.ts b/packages/algorithm/src/v2/index.ts
new file mode 100644
index 0000000..7400e2b
--- /dev/null
+++ b/packages/algorithm/src/v2/index.ts
@@ -0,0 +1,370 @@
+import type { Answer } from "@data-maki/schemas";
+import { shallowEqual } from "fast-equals";
+import type { FixedLengthArray } from "type-fest";
+import { katanuki } from "../katanuki";
+import { cellsToBoard } from "../models/answer";
+import { type CellCounts, type Context, DOWN, LEFT, type Point, RIGHT, UP } from "../types";
+import { dbgCells, multiDereverse, multiReverse } from "../utils/arrays";
+import { countElementsColumnWise, getDelta } from "../utils/board";
+import { dbg } from "../workers/log";
+import { evaluateColumnPiece, evaluateRow } from "./evaluation";
+
+export const solve = (c: Context): [answer: Answer, board: string[]] => {
+ c.board = multiReverse(c.board, c.rvOp);
+ c.goalBoard = multiReverse(c.goalBoard, c.rvOp);
+
+ c.currentElementCounts = countElementsColumnWise(c.board);
+ const goalElementCounts = countElementsColumnWise(c.goalBoard);
+
+ dbg(c.worker, "My rvOp", c.rvOp);
+
+ dbgCells(c.board, c.worker);
+
+ const rvUl = c.rvOp.hasReverse90;
+ const rvUd = c.rvOp.hasReverseUpDown;
+ const rvLr = c.rvOp.hasReverseLeftRight;
+
+ let delta: CellCounts;
+ let cntUnmoved = 0;
+
+ for (let i = c.height - 1; i > -1; i--) {
+ let completedRows = c.height - i - 1 - cntUnmoved;
+
+ delta = getDelta(c.currentElementCounts[c.height - 1 - cntUnmoved], goalElementCounts[i]);
+
+ dbg(c.worker, "delta", delta);
+
+ const unfilled = [];
+
+ // Only stripe
+ for (const j of Array(c.width).keys()) {
+ if (shallowEqual(delta, [0, 0, 0, 0])) break;
+
+ if (cntUnmoved > 0) {
+ katanuki(c, 22, 0, c.height - cntUnmoved, DOWN);
+
+ completedRows += cntUnmoved;
+ cntUnmoved = 0;
+ }
+
+ let lookingCell = c.board.get(c.height - 1, j);
+
+ if (delta[lookingCell] <= 0) continue;
+
+ let isFilled = false;
+
+ for (let k = c.height - 2; k > completedRows - 1; k--) {
+ lookingCell = c.board.get(k, j);
+
+ if (delta[lookingCell] < 0) {
+ dbg(c.worker, `looking at ${lookingCell} at x: ${j} y: ${k}`);
+
+ let cnt = 0;
+ let value: FixedLengthArray = [0, 0, 0, 256];
+
+ while (k - (1 << cnt) + 1 >= completedRows) {
+ const x = j;
+ const y = k - (1 << cnt) + 1;
+
+ let p = 0;
+ let pp: Point = { x: 0, y: 0 };
+
+ let evaluation = 0;
+
+ if (cnt !== 0) {
+ p = 3 * cnt - 1;
+ pp.x = rvUl && rvUd ? x - 1 : x;
+ pp.y = !rvUl && !rvUd ? y + 1 : y;
+
+ evaluation = evaluateRow(c, p, pp, goalElementCounts[i]);
+
+ if (value[3] > evaluation) {
+ value = [p, pp.x, pp.y, evaluation];
+ }
+
+ p = 3 * cnt;
+ pp.x = !rvUl && rvLr ? x - 1 : x;
+ pp.y = rvUl && !rvLr ? y + 1 : y;
+
+ evaluation = evaluateRow(c, p, pp, goalElementCounts[i]);
+
+ if (value[3] > evaluation) {
+ value = [p, pp.x, pp.y, evaluation];
+ }
+ } else {
+ p = 0;
+ pp = { x, y };
+
+ evaluation = evaluateRow(c, p, pp, goalElementCounts[i]);
+
+ if (value[3] > evaluation) {
+ value = [p, pp.x, pp.y, evaluation];
+ }
+ }
+
+ cnt++;
+ }
+
+ katanuki(c, value[0], value[1], value[2], UP);
+
+ isFilled = true;
+
+ delta = getDelta(c.currentElementCounts[c.height - 1], goalElementCounts[i]);
+
+ break;
+ }
+ }
+
+ if (!isFilled) unfilled.push(j);
+ }
+
+ dbg(c.worker, "unfilled", unfilled);
+
+ // unfilled
+ for (const j of unfilled) {
+ let isFilled = false;
+
+ const lookingCell = c.board.get(c.height - 1, j);
+
+ if (delta[lookingCell] <= 0) continue;
+
+ dbg(c.worker, "fill column", j);
+
+ for (let k = 1; k < Math.max(j, c.width - j - 1) + 1; k++) {
+ // right side
+ let rx = j + k;
+
+ if (rx < c.width) {
+ dbg(c.worker, "check column", rx);
+
+ for (let m = c.height - 2; m > completedRows - 1; m--) {
+ const lookingCell = c.board.get(m, rx);
+
+ if (delta[lookingCell] < 0) {
+ let y = m;
+ let ln = k;
+ let irregular = false;
+
+ dbg(c.worker, `bring ${lookingCell} from ${rx} ${y}`);
+
+ // If border nukigata confuse
+ if (m % 2 === (c.height - 1) % 2) {
+ dbg(c.worker, "protect confusion");
+
+ // Move lookingCell to the place which is confused and move the deepest cell to the place which is not confused
+ katanuki(c, rvUl ? 2 : 3, (!rvUl && !rvLr) || (rvUl && !rvUd) ? rx : rx - 1, y, UP);
+
+ irregular = true;
+ y = c.height - 2;
+ }
+
+ let cnt = 0;
+
+ while (ln > 0) {
+ if (ln % 2 === 1) {
+ // border nukigata (else: 1 * 1)
+ katanuki(
+ c,
+ cnt === 0 ? 0 : rvUl ? 3 * cnt : 3 * cnt - 1,
+ rx - (1 << cnt),
+ (!rvUd && !rvUl) || (!rvLr && rvUl) || cnt === 0 ? y : y - 1,
+ LEFT,
+ );
+
+ rx -= 1 << cnt;
+ }
+
+ ln >>= 1;
+ cnt++;
+ }
+
+ katanuki(c, 0, rx, y, UP);
+
+ if (irregular) {
+ katanuki(c, 0, j + k, c.height - 3, UP);
+ }
+
+ delta = getDelta(c.currentElementCounts[c.height - 1], goalElementCounts[i]);
+
+ isFilled = true;
+
+ break;
+ }
+ }
+ }
+
+ if (isFilled) break;
+
+ // left side
+ let lx = j - k;
+
+ if (lx >= 0) {
+ dbg(c.worker, "check column", lx);
+
+ for (let m = c.height - 2; m > completedRows - 1; m--) {
+ const lookingCell = c.board.get(m, lx);
+
+ if (delta[lookingCell] < 0) {
+ let y = m;
+ let ln = k;
+ let irregular = false;
+
+ dbg(c.worker, `bring ${lookingCell} from ${lx} ${y}`);
+
+ // If border nukigata confuse
+ if (m % 2 === (c.height - 1) % 2) {
+ dbg(c.worker, "protect confusion");
+
+ // Move lookingCell to the place which is confused and move the deepest cell to the place which is not confused
+ katanuki(c, rvUl ? 2 : 3, (!rvUl && !rvLr) || (rvUl && !rvUd) ? lx : lx - 1, y, UP);
+
+ irregular = true;
+ y = c.height - 2;
+ }
+
+ let cnt = 0;
+
+ while (ln > 0) {
+ if (ln % 2 === 1) {
+ // border nukigata (else: 1 * 1)
+ katanuki(
+ c,
+ cnt === 0 ? 0 : rvUl ? 3 * cnt : 3 * cnt - 1,
+ lx + 1,
+ (!rvUd && !rvUl) || (!rvLr && rvUl) || cnt === 0 ? y : y - 1,
+ RIGHT,
+ );
+
+ lx += 1 << cnt;
+ }
+
+ ln >>= 1;
+ cnt++;
+ }
+
+ katanuki(c, 0, lx, y, UP);
+
+ if (irregular) {
+ katanuki(c, 0, j - k, c.height - 3, UP);
+ }
+
+ delta = getDelta(c.currentElementCounts[c.height - 1], goalElementCounts[i]);
+
+ isFilled = true;
+
+ break;
+ }
+ }
+ }
+
+ if (isFilled) break;
+ }
+ }
+
+ cntUnmoved++;
+
+ if (i === 0) {
+ katanuki(c, 22, 0, c.height - cntUnmoved, DOWN);
+ }
+ }
+
+ cntUnmoved = 0;
+
+ for (let i = c.width - 1; i > -1; i--) {
+ let completedColumns = c.width - i - 1 - cntUnmoved;
+ const goalColumn = c.goalBoard.getColumn(i);
+
+ // Only stripe
+ for (const j of Array(c.height).keys()) {
+ const correctCell = goalColumn[j];
+
+ if (c.board.get(j, c.width - 1 - cntUnmoved) === correctCell) continue;
+
+ if (cntUnmoved > 0) {
+ katanuki(c, 22, c.width - cntUnmoved, 0, RIGHT);
+
+ completedColumns += cntUnmoved;
+ cntUnmoved = 0;
+ }
+
+ for (let k = c.width - 2; k > completedColumns - 1; k--) {
+ const lookingCell = c.board.get(j, k);
+
+ if (lookingCell === correctCell) {
+ let cnt = 0;
+ let value: FixedLengthArray = [0, 0, 0, 256];
+
+ while (k - (1 << cnt) + 1 >= completedColumns) {
+ const x = k - (1 << cnt) + 1;
+ const y = j;
+
+ let p = 0;
+ let pp: Point = { x: 0, y: 0 };
+
+ let evaluation = 0;
+
+ if (cnt !== 0) {
+ p = 3 * cnt - 1;
+ pp.x = rvUl && !rvUd ? x + 1 : x;
+ pp.y = !rvUl && rvUd ? y - 1 : y;
+
+ evaluation = evaluateColumnPiece(c, p, pp, goalColumn);
+
+ if (value[3] > evaluation) {
+ value = [p, pp.x, pp.y, evaluation];
+ }
+
+ p = 3 * cnt;
+ pp.x = !rvUl && !rvLr ? x + 1 : x;
+ pp.y = rvUl && rvLr ? y - 1 : y;
+
+ evaluation = evaluateColumnPiece(c, p, pp, goalColumn);
+
+ if (value[3] > evaluation) {
+ value = [p, pp.x, pp.y, evaluation];
+ }
+ } else {
+ p = 0;
+ pp = { x, y };
+
+ evaluation = evaluateColumnPiece(c, p, pp, goalColumn);
+
+ if (value[3] > evaluation) {
+ value = [p, pp.x, pp.y, evaluation];
+ }
+ }
+
+ cnt++;
+ }
+
+ katanuki(c, value[0], value[1], value[2], LEFT);
+
+ break;
+ }
+ }
+ }
+
+ cntUnmoved++;
+
+ if (i === 0) {
+ katanuki(c, 22, c.width - cntUnmoved, 0, RIGHT);
+ }
+ }
+
+ dbg(c.worker, "dereverse");
+
+ c.board = multiDereverse(c.board, c.rvOp);
+ c.goalBoard = multiDereverse(c.goalBoard, c.rvOp);
+
+ dbgCells(c.board, c.worker);
+
+ dbg(c.worker, "turns", c.n);
+
+ return [
+ {
+ n: c.n,
+ ops: c.ops,
+ },
+ cellsToBoard(c.board),
+ ] as const;
+};
diff --git a/packages/algorithm/src/workers/v1.master.ts b/packages/algorithm/src/workers/v1.master.ts
index 23171d9..7940307 100644
--- a/packages/algorithm/src/workers/v1.master.ts
+++ b/packages/algorithm/src/workers/v1.master.ts
@@ -11,7 +11,7 @@ export const solve: SolveFunc = async (problem, onStartWorker, onWorkerFinish) =
const result = await worker.solve(problem, barrier);
- onWorkerFinish?.(0, result[0].n);
+ onWorkerFinish?.(0, result[0]);
return result;
};
diff --git a/packages/algorithm/src/workers/v2.master.ts b/packages/algorithm/src/workers/v2.master.ts
new file mode 100644
index 0000000..9ddccf1
--- /dev/null
+++ b/packages/algorithm/src/workers/v2.master.ts
@@ -0,0 +1,47 @@
+import * as Comlink from "comlink";
+import type { FixedLengthArray } from "type-fest";
+import type { SolveFunc } from "../index";
+import { Barrier } from "../utils/concurrency/barrier";
+import { spawnWorker } from "../workers";
+
+const TOTAL_WORKERS = 8;
+
+type Flags = FixedLengthArray, typeof TOTAL_WORKERS>;
+
+export const solve: SolveFunc = async (problem, onStartWorker, onWorkerFinish) => {
+ const barrier = new Barrier(TOTAL_WORKERS);
+
+ onStartWorker?.(TOTAL_WORKERS);
+
+ const flags = Array.from({ length: TOTAL_WORKERS }, () => [false, false, false]) as unknown as Flags;
+
+ for (let i = 0; i < TOTAL_WORKERS; i++) {
+ for (let j = 0; j < 3; j++) {
+ if ((i >> j) & 1) {
+ flags[i][2 - j] = true;
+ }
+ }
+ }
+
+ const workers = Array(TOTAL_WORKERS)
+ .fill(null)
+ .map(() => Comlink.wrap(spawnWorker("v2.worker.ts")));
+
+ const results = await Promise.all(
+ workers.map((worker, i) =>
+ worker
+ .solve(problem, barrier, {
+ hasReverse90: flags[i][0],
+ hasReverseUpDown: flags[i][1],
+ hasReverseLeftRight: flags[i][2],
+ })
+ .then((result) => {
+ onWorkerFinish?.(i, result[0]);
+
+ return result;
+ }),
+ ),
+ );
+
+ return results.toSorted((a, b) => a[0].n - b[0].n)[0];
+};
diff --git a/packages/algorithm/src/workers/v2.worker.ts b/packages/algorithm/src/workers/v2.worker.ts
new file mode 100644
index 0000000..a78d1fa
--- /dev/null
+++ b/packages/algorithm/src/workers/v2.worker.ts
@@ -0,0 +1,29 @@
+import type { Problem } from "@data-maki/schemas";
+import * as Comlink from "comlink";
+import type { ReverseOperationPatterns } from "../types";
+import { multiReverse } from "../utils/arrays";
+import { Barrier } from "../utils/concurrency/barrier";
+import { createContext } from "../v1";
+import { solve as solveFunc } from "../v2";
+
+declare const self: Worker;
+
+export const solve = (problem: Problem, parentBarrier: Barrier, rvOp: ReverseOperationPatterns) => {
+ const barrier = Barrier.connect(parentBarrier);
+ const c = createContext(problem, self);
+
+ c.rvOp = rvOp;
+
+ if (c.rvOp.hasReverse90) {
+ c.width = problem.board.height;
+ c.height = problem.board.width;
+ }
+
+ // Other data is modified in the beginning of solveFunc
+
+ barrier.wait();
+
+ return solveFunc(c);
+};
+
+Comlink.expose({ solve });