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 });