🔨 rename package compute -> solver
This commit is contained in:
33
packages/solver/README.md
Normal file
33
packages/solver/README.md
Normal file
@@ -0,0 +1,33 @@
|
||||
# @snk/solver
|
||||
|
||||
Contains the algorithm to compute the best route given a grid and a starting position for the snake.
|
||||
|
||||
## Implementation
|
||||
|
||||
- for each color in the grid
|
||||
|
||||
- 1\ **clear residual color** phase
|
||||
|
||||
- find all the cells of a previous color that are "tunnel-able" ( ie: the snake can find a path from the outside of the grid to the cell, and can go back to the outside without colliding ). The snake is allowed to pass thought current and previous color. Higher colors are walls
|
||||
|
||||
- sort the "tunnel-able" cell, there is penalty for passing through current color, as previous color should be eliminated as soon as possible.
|
||||
|
||||
- for cells with the same score, take the closest one ( determined with a quick mathematic distance, which is not accurate but fast at least )
|
||||
|
||||
- navigate to the cell, and through the tunnel.
|
||||
|
||||
- re-compute the list of tunnel-able cells ( as eating cells might have freed better tunnel ) as well as the score
|
||||
|
||||
- iterate
|
||||
|
||||
- 2\ **clear clean color** phase
|
||||
|
||||
- find all the cells of the current color that are "tunnel-able"
|
||||
|
||||
- no need to consider scoring here. In order to improve efficiency, get the closest cell by doing a tree search ( instead of a simple mathematic distance like in the previous phase )
|
||||
|
||||
- navigate to the cell, and through the tunnel.
|
||||
|
||||
- iterate
|
||||
|
||||
- go back to the starting point
|
||||
48
packages/solver/__tests__/getBestRoute-fuzz.spec.ts
Normal file
48
packages/solver/__tests__/getBestRoute-fuzz.spec.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { getBestRoute } from "../getBestRoute";
|
||||
import { snake3, snake4 } from "@snk/types/__fixtures__/snake";
|
||||
import {
|
||||
getHeadX,
|
||||
getHeadY,
|
||||
getSnakeLength,
|
||||
Snake,
|
||||
snakeWillSelfCollide,
|
||||
} from "@snk/types/snake";
|
||||
import { createFromSeed } from "@snk/types/__fixtures__/createFromSeed";
|
||||
|
||||
const n = 1000;
|
||||
|
||||
for (const { width, height, snake } of [
|
||||
{ width: 5, height: 5, snake: snake3 },
|
||||
{ width: 5, height: 5, snake: snake4 },
|
||||
])
|
||||
it(`should find solution for ${n} ${width}x${height} generated grids for ${getSnakeLength(
|
||||
snake
|
||||
)} length snake`, () => {
|
||||
const results = Array.from({ length: n }, (_, seed) => {
|
||||
const grid = createFromSeed(seed, width, height);
|
||||
|
||||
try {
|
||||
const chain = getBestRoute(grid, snake);
|
||||
|
||||
assertValidPath(chain);
|
||||
|
||||
return { seed };
|
||||
} catch (error) {
|
||||
return { seed, error };
|
||||
}
|
||||
});
|
||||
|
||||
expect(results.filter((x) => x.error)).toEqual([]);
|
||||
});
|
||||
|
||||
const assertValidPath = (chain: Snake[]) => {
|
||||
for (let i = 0; i < chain.length - 1; i++) {
|
||||
const dx = getHeadX(chain[i + 1]) - getHeadX(chain[i]);
|
||||
const dy = getHeadY(chain[i + 1]) - getHeadY(chain[i]);
|
||||
|
||||
if (!((Math.abs(dx) === 1 && dy == 0) || (Math.abs(dy) === 1 && dx == 0)))
|
||||
throw new Error(`unexpected direction ${dx},${dy}`);
|
||||
|
||||
if (snakeWillSelfCollide(chain[i], dx, dy)) throw new Error(`self collide`);
|
||||
}
|
||||
};
|
||||
26
packages/solver/__tests__/getBestRoute.spec.ts
Normal file
26
packages/solver/__tests__/getBestRoute.spec.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { getBestRoute } from "../getBestRoute";
|
||||
import { Color, createEmptyGrid, setColor } from "@snk/types/grid";
|
||||
import { createSnakeFromCells, snakeToCells } from "@snk/types/snake";
|
||||
import * as grids from "@snk/types/__fixtures__/grid";
|
||||
import { snake3 } from "@snk/types/__fixtures__/snake";
|
||||
|
||||
it("should find best route", () => {
|
||||
const snk0 = [
|
||||
{ x: 0, y: 0 },
|
||||
{ x: 1, y: 0 },
|
||||
];
|
||||
|
||||
const grid = createEmptyGrid(5, 5);
|
||||
setColor(grid, 3, 3, 1 as Color);
|
||||
|
||||
const chain = getBestRoute(grid, createSnakeFromCells(snk0))!;
|
||||
|
||||
expect(snakeToCells(chain[1])[1]).toEqual({ x: 0, y: 0 });
|
||||
|
||||
expect(snakeToCells(chain[chain.length - 1])[0]).toEqual({ x: 3, y: 3 });
|
||||
});
|
||||
|
||||
for (const [gridName, grid] of Object.entries(grids))
|
||||
it(`should find a solution for ${gridName}`, () => {
|
||||
getBestRoute(grid, snake3);
|
||||
});
|
||||
12
packages/solver/__tests__/getPathTo.spec.ts
Normal file
12
packages/solver/__tests__/getPathTo.spec.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { createEmptyGrid } from "@snk/types/grid";
|
||||
import { getHeadX, getHeadY } from "@snk/types/snake";
|
||||
import { snake3 } from "@snk/types/__fixtures__/snake";
|
||||
import { getPathTo } from "../getPathTo";
|
||||
|
||||
it("should find it's way in vaccum", () => {
|
||||
const grid = createEmptyGrid(5, 0);
|
||||
|
||||
const path = getPathTo(grid, snake3, 5, -1)!;
|
||||
|
||||
expect([getHeadX(path[0]), getHeadY(path[0])]).toEqual([5, -1]);
|
||||
});
|
||||
19
packages/solver/__tests__/getPathToPose.spec.ts
Normal file
19
packages/solver/__tests__/getPathToPose.spec.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { createSnakeFromCells } from "@snk/types/snake";
|
||||
import { getPathToPose } from "../getPathToPose";
|
||||
|
||||
it("should fing path to pose", () => {
|
||||
const snake0 = createSnakeFromCells([
|
||||
{ x: 0, y: 0 },
|
||||
{ x: 1, y: 0 },
|
||||
{ x: 2, y: 0 },
|
||||
]);
|
||||
const target = createSnakeFromCells([
|
||||
{ x: 1, y: 0 },
|
||||
{ x: 2, y: 0 },
|
||||
{ x: 3, y: 0 },
|
||||
]);
|
||||
|
||||
const path = getPathToPose(snake0, target);
|
||||
|
||||
expect(path).toBeDefined();
|
||||
});
|
||||
86
packages/solver/__tests__/sortPush.spec.ts
Normal file
86
packages/solver/__tests__/sortPush.spec.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import { sortPush } from "../utils/sortPush";
|
||||
|
||||
const sortFn = (a: number, b: number) => a - b;
|
||||
|
||||
it("should sort push length=0", () => {
|
||||
const a: any[] = [];
|
||||
const x = -1;
|
||||
const res = [...a, x].sort(sortFn);
|
||||
|
||||
sortPush(a, x, sortFn);
|
||||
|
||||
expect(a).toEqual(res);
|
||||
});
|
||||
|
||||
it("should sort push under", () => {
|
||||
const a = [1, 2, 3, 4, 5];
|
||||
const x = -1;
|
||||
const res = [...a, x].sort(sortFn);
|
||||
|
||||
sortPush(a, x, sortFn);
|
||||
|
||||
expect(a).toEqual(res);
|
||||
});
|
||||
|
||||
it("should sort push 0", () => {
|
||||
const a = [1, 2, 3, 4, 5];
|
||||
const x = 1;
|
||||
const res = [...a, x].sort(sortFn);
|
||||
|
||||
sortPush(a, x, sortFn);
|
||||
|
||||
expect(a).toEqual(res);
|
||||
});
|
||||
|
||||
it("should sort push end", () => {
|
||||
const a = [1, 2, 3, 4, 5];
|
||||
const x = 5;
|
||||
const res = [...a, x].sort(sortFn);
|
||||
|
||||
sortPush(a, x, sortFn);
|
||||
|
||||
expect(a).toEqual(res);
|
||||
});
|
||||
|
||||
it("should sort push over", () => {
|
||||
const a = [1, 2, 3, 4, 5];
|
||||
const x = 10;
|
||||
const res = [...a, x].sort(sortFn);
|
||||
|
||||
sortPush(a, x, sortFn);
|
||||
|
||||
expect(a).toEqual(res);
|
||||
});
|
||||
|
||||
it("should sort push inside", () => {
|
||||
const a = [1, 2, 3, 4, 5];
|
||||
const x = 1.5;
|
||||
const res = [...a, x].sort(sortFn);
|
||||
|
||||
sortPush(a, x, sortFn);
|
||||
|
||||
expect(a).toEqual(res);
|
||||
});
|
||||
|
||||
describe("benchmark", () => {
|
||||
const n = 200;
|
||||
|
||||
const samples = Array.from({ length: 5000 }, () => [
|
||||
Math.random(),
|
||||
Array.from({ length: n }, () => Math.random()),
|
||||
]);
|
||||
const s0 = samples.map(([x, arr]: any) => [x, arr.slice()]);
|
||||
const s1 = samples.map(([x, arr]: any) => [x, arr.slice()]);
|
||||
|
||||
it("push + sort", () => {
|
||||
for (const [x, arr] of s0) {
|
||||
arr.push(x);
|
||||
arr.sort(sortFn);
|
||||
}
|
||||
});
|
||||
it("sortPush", () => {
|
||||
for (const [x, arr] of s1) {
|
||||
sortPush(arr, x, sortFn);
|
||||
}
|
||||
});
|
||||
});
|
||||
130
packages/solver/clearCleanColoredLayer.ts
Normal file
130
packages/solver/clearCleanColoredLayer.ts
Normal file
@@ -0,0 +1,130 @@
|
||||
import {
|
||||
getColor,
|
||||
isEmpty,
|
||||
isInside,
|
||||
isInsideLarge,
|
||||
setColorEmpty,
|
||||
} from "@snk/types/grid";
|
||||
import {
|
||||
getHeadX,
|
||||
getHeadY,
|
||||
getSnakeLength,
|
||||
nextSnake,
|
||||
snakeEquals,
|
||||
snakeWillSelfCollide,
|
||||
} from "@snk/types/snake";
|
||||
import { around4, Point } from "@snk/types/point";
|
||||
import { getBestTunnel } from "./getBestTunnel";
|
||||
import { fillOutside } from "./outside";
|
||||
import type { Outside } from "./outside";
|
||||
import type { Snake } from "@snk/types/snake";
|
||||
import type { Color, Empty, Grid } from "@snk/types/grid";
|
||||
|
||||
export const clearCleanColoredLayer = (
|
||||
grid: Grid,
|
||||
outside: Outside,
|
||||
snake0: Snake,
|
||||
color: Color
|
||||
) => {
|
||||
const snakeN = getSnakeLength(snake0);
|
||||
|
||||
const points = getTunnellablePoints(grid, outside, snakeN, color);
|
||||
|
||||
const chain: Snake[] = [snake0];
|
||||
|
||||
while (points.length) {
|
||||
const path = getPathToNextPoint(grid, chain[0], color, points)!;
|
||||
path.pop();
|
||||
|
||||
for (const snake of path)
|
||||
setEmptySafe(grid, getHeadX(snake), getHeadY(snake));
|
||||
|
||||
chain.unshift(...path);
|
||||
}
|
||||
|
||||
fillOutside(outside, grid);
|
||||
|
||||
chain.pop();
|
||||
return chain;
|
||||
};
|
||||
|
||||
type M = { snake: Snake; parent: M | null };
|
||||
const unwrap = (m: M | null): Snake[] =>
|
||||
!m ? [] : [m.snake, ...unwrap(m.parent)];
|
||||
const getPathToNextPoint = (
|
||||
grid: Grid,
|
||||
snake0: Snake,
|
||||
color: Color,
|
||||
points: Point[]
|
||||
) => {
|
||||
const closeList: Snake[] = [];
|
||||
const openList: M[] = [{ snake: snake0 } as any];
|
||||
|
||||
while (openList.length) {
|
||||
const o = openList.shift()!;
|
||||
|
||||
const x = getHeadX(o.snake);
|
||||
const y = getHeadY(o.snake);
|
||||
|
||||
const i = points.findIndex((p) => p.x === x && p.y === y);
|
||||
if (i >= 0) {
|
||||
points.splice(i, 1);
|
||||
return unwrap(o);
|
||||
}
|
||||
|
||||
for (const { x: dx, y: dy } of around4) {
|
||||
if (
|
||||
isInsideLarge(grid, 2, x + dx, y + dy) &&
|
||||
!snakeWillSelfCollide(o.snake, dx, dy) &&
|
||||
getColorSafe(grid, x + dx, y + dy) <= color
|
||||
) {
|
||||
const snake = nextSnake(o.snake, dx, dy);
|
||||
|
||||
if (!closeList.some((s0) => snakeEquals(s0, snake))) {
|
||||
closeList.push(snake);
|
||||
openList.push({ snake, parent: o });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* get all cells that are tunnellable
|
||||
*/
|
||||
export const getTunnellablePoints = (
|
||||
grid: Grid,
|
||||
outside: Outside,
|
||||
snakeN: number,
|
||||
color: Color
|
||||
) => {
|
||||
const points: Point[] = [];
|
||||
|
||||
for (let x = grid.width; x--; )
|
||||
for (let y = grid.height; y--; ) {
|
||||
const c = getColor(grid, x, y);
|
||||
if (
|
||||
!isEmpty(c) &&
|
||||
c <= color &&
|
||||
!points.some((p) => p.x === x && p.y === y)
|
||||
) {
|
||||
const tunnel = getBestTunnel(grid, outside, x, y, color, snakeN);
|
||||
|
||||
if (tunnel)
|
||||
for (const p of tunnel)
|
||||
if (!isEmptySafe(grid, p.x, p.y)) points.push(p);
|
||||
}
|
||||
}
|
||||
|
||||
return points;
|
||||
};
|
||||
|
||||
const getColorSafe = (grid: Grid, x: number, y: number) =>
|
||||
isInside(grid, x, y) ? getColor(grid, x, y) : (0 as Empty);
|
||||
|
||||
const setEmptySafe = (grid: Grid, x: number, y: number) => {
|
||||
if (isInside(grid, x, y)) setColorEmpty(grid, x, y);
|
||||
};
|
||||
|
||||
const isEmptySafe = (grid: Grid, x: number, y: number) =>
|
||||
!isInside(grid, x, y) && isEmpty(getColor(grid, x, y));
|
||||
152
packages/solver/clearResidualColoredLayer.ts
Normal file
152
packages/solver/clearResidualColoredLayer.ts
Normal file
@@ -0,0 +1,152 @@
|
||||
import {
|
||||
Empty,
|
||||
getColor,
|
||||
isEmpty,
|
||||
isInside,
|
||||
setColorEmpty,
|
||||
} from "@snk/types/grid";
|
||||
import { getHeadX, getHeadY, getSnakeLength } from "@snk/types/snake";
|
||||
import { getBestTunnel } from "./getBestTunnel";
|
||||
import { fillOutside, Outside } from "./outside";
|
||||
import { getTunnelPath } from "./tunnel";
|
||||
import { getPathTo } from "./getPathTo";
|
||||
import type { Snake } from "@snk/types/snake";
|
||||
import type { Color, Grid } from "@snk/types/grid";
|
||||
import type { Point } from "@snk/types/point";
|
||||
|
||||
type T = Point & { tunnel: Point[]; priority: number };
|
||||
|
||||
export const clearResidualColoredLayer = (
|
||||
grid: Grid,
|
||||
outside: Outside,
|
||||
snake0: Snake,
|
||||
color: Color
|
||||
) => {
|
||||
const snakeN = getSnakeLength(snake0);
|
||||
|
||||
const tunnels = getTunnellablePoints(grid, outside, snakeN, color);
|
||||
|
||||
// sort
|
||||
tunnels.sort((a, b) => b.priority - a.priority);
|
||||
|
||||
const chain: Snake[] = [snake0];
|
||||
|
||||
while (tunnels.length) {
|
||||
// get the best next tunnel
|
||||
let t = getNextTunnel(tunnels, chain[0]);
|
||||
|
||||
// goes to the start of the tunnel
|
||||
chain.unshift(...getPathTo(grid, chain[0], t[0].x, t[0].y)!);
|
||||
|
||||
// goes to the end of the tunnel
|
||||
chain.unshift(...getTunnelPath(chain[0], t));
|
||||
|
||||
// update grid
|
||||
for (const { x, y } of t) setEmptySafe(grid, x, y);
|
||||
|
||||
// update outside
|
||||
fillOutside(outside, grid);
|
||||
|
||||
// update tunnels
|
||||
for (let i = tunnels.length; i--; )
|
||||
if (isEmpty(getColor(grid, tunnels[i].x, tunnels[i].y)))
|
||||
tunnels.splice(i, 1);
|
||||
else {
|
||||
const t = tunnels[i];
|
||||
const tunnel = getBestTunnel(grid, outside, t.x, t.y, color, snakeN);
|
||||
|
||||
if (!tunnel) tunnels.splice(i, 1);
|
||||
else {
|
||||
t.tunnel = tunnel;
|
||||
t.priority = getPriority(grid, color, tunnel);
|
||||
}
|
||||
}
|
||||
|
||||
// re-sort
|
||||
tunnels.sort((a, b) => b.priority - a.priority);
|
||||
}
|
||||
|
||||
chain.pop();
|
||||
return chain;
|
||||
};
|
||||
|
||||
const getNextTunnel = (ts: T[], snake: Snake) => {
|
||||
let minDistance = Infinity;
|
||||
let closestTunnel: Point[] | null = null;
|
||||
|
||||
const x = getHeadX(snake);
|
||||
const y = getHeadY(snake);
|
||||
|
||||
const priority = ts[0].priority;
|
||||
|
||||
for (let i = 0; ts[i] && ts[i].priority === priority; i++) {
|
||||
const t = ts[i].tunnel;
|
||||
|
||||
const d = distanceSq(t[0].x, t[0].y, x, y);
|
||||
if (d < minDistance) {
|
||||
minDistance = d;
|
||||
closestTunnel = t;
|
||||
}
|
||||
}
|
||||
|
||||
return closestTunnel!;
|
||||
};
|
||||
|
||||
/**
|
||||
* get all the tunnels for all the cells accessible
|
||||
*/
|
||||
export const getTunnellablePoints = (
|
||||
grid: Grid,
|
||||
outside: Outside,
|
||||
snakeN: number,
|
||||
color: Color
|
||||
) => {
|
||||
const points: T[] = [];
|
||||
|
||||
for (let x = grid.width; x--; )
|
||||
for (let y = grid.height; y--; ) {
|
||||
const c = getColor(grid, x, y);
|
||||
if (!isEmpty(c) && c < color) {
|
||||
const tunnel = getBestTunnel(grid, outside, x, y, color, snakeN);
|
||||
if (tunnel) {
|
||||
const priority = getPriority(grid, color, tunnel);
|
||||
points.push({ x, y, priority, tunnel });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return points;
|
||||
};
|
||||
|
||||
/**
|
||||
* get the score of the tunnel
|
||||
* prioritize tunnel with maximum color smaller than <color> and with minimum <color>
|
||||
* with some tweaks
|
||||
*/
|
||||
export const getPriority = (grid: Grid, color: Color, tunnel: Point[]) => {
|
||||
let nColor = 0;
|
||||
let nLess = 0;
|
||||
|
||||
for (let i = 0; i < tunnel.length; i++) {
|
||||
const { x, y } = tunnel[i];
|
||||
const c = getColorSafe(grid, x, y);
|
||||
|
||||
if (!isEmpty(c) && i === tunnel.findIndex((p) => p.x === x && p.y === y)) {
|
||||
if (c === color) nColor += 1;
|
||||
else nLess += color - c;
|
||||
}
|
||||
}
|
||||
|
||||
if (nColor === 0) return 99999;
|
||||
return nLess / nColor;
|
||||
};
|
||||
|
||||
const distanceSq = (ax: number, ay: number, bx: number, by: number) =>
|
||||
(ax - bx) ** 2 + (ay - by) ** 2;
|
||||
|
||||
const getColorSafe = (grid: Grid, x: number, y: number) =>
|
||||
isInside(grid, x, y) ? getColor(grid, x, y) : (0 as Empty);
|
||||
|
||||
const setEmptySafe = (grid: Grid, x: number, y: number) => {
|
||||
if (isInside(grid, x, y)) setColorEmpty(grid, x, y);
|
||||
};
|
||||
28
packages/solver/getBestRoute.ts
Normal file
28
packages/solver/getBestRoute.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { copyGrid } from "@snk/types/grid";
|
||||
import { createOutside } from "./outside";
|
||||
import { clearResidualColoredLayer } from "./clearResidualColoredLayer";
|
||||
import { clearCleanColoredLayer } from "./clearCleanColoredLayer";
|
||||
import type { Color, Grid } from "@snk/types/grid";
|
||||
import type { Snake } from "@snk/types/snake";
|
||||
|
||||
export const getBestRoute = (grid0: Grid, snake0: Snake) => {
|
||||
const grid = copyGrid(grid0);
|
||||
const outside = createOutside(grid);
|
||||
const chain: Snake[] = [snake0];
|
||||
|
||||
for (const color of extractColors(grid)) {
|
||||
if (color > 1)
|
||||
chain.unshift(
|
||||
...clearResidualColoredLayer(grid, outside, chain[0], color)
|
||||
);
|
||||
chain.unshift(...clearCleanColoredLayer(grid, outside, chain[0], color));
|
||||
}
|
||||
|
||||
return chain.reverse();
|
||||
};
|
||||
|
||||
const extractColors = (grid: Grid): Color[] => {
|
||||
// @ts-ignore
|
||||
let maxColor = Math.max(...grid.data);
|
||||
return Array.from({ length: maxColor }, (_, i) => (i + 1) as Color);
|
||||
};
|
||||
113
packages/solver/getBestTunnel.ts
Normal file
113
packages/solver/getBestTunnel.ts
Normal file
@@ -0,0 +1,113 @@
|
||||
import { copyGrid, getColor, isInside, setColorEmpty } from "@snk/types/grid";
|
||||
import { around4 } from "@snk/types/point";
|
||||
import { sortPush } from "./utils/sortPush";
|
||||
import {
|
||||
createSnakeFromCells,
|
||||
getHeadX,
|
||||
getHeadY,
|
||||
nextSnake,
|
||||
snakeEquals,
|
||||
snakeWillSelfCollide,
|
||||
} from "@snk/types/snake";
|
||||
import { isOutside } from "./outside";
|
||||
import { trimTunnelEnd, trimTunnelStart } from "./tunnel";
|
||||
import type { Outside } from "./outside";
|
||||
import type { Snake } from "@snk/types/snake";
|
||||
import type { Empty, Color, Grid } from "@snk/types/grid";
|
||||
import type { Point } from "@snk/types/point";
|
||||
|
||||
const getColorSafe = (grid: Grid, x: number, y: number) =>
|
||||
isInside(grid, x, y) ? getColor(grid, x, y) : (0 as Empty);
|
||||
|
||||
const setEmptySafe = (grid: Grid, x: number, y: number) => {
|
||||
if (isInside(grid, x, y)) setColorEmpty(grid, x, y);
|
||||
};
|
||||
|
||||
type M = { snake: Snake; parent: M | null; w: number };
|
||||
|
||||
const unwrap = (m: M | null): Point[] =>
|
||||
!m
|
||||
? []
|
||||
: [...unwrap(m.parent), { x: getHeadX(m.snake), y: getHeadY(m.snake) }];
|
||||
|
||||
/**
|
||||
* returns the path to reach the outside which contains the least color cell
|
||||
*/
|
||||
const getSnakeEscapePath = (
|
||||
grid: Grid,
|
||||
outside: Outside,
|
||||
snake0: Snake,
|
||||
color: Color
|
||||
) => {
|
||||
const openList: M[] = [{ snake: snake0, w: 0 } as any];
|
||||
const closeList: Snake[] = [];
|
||||
|
||||
while (openList[0]) {
|
||||
const o = openList.shift()!;
|
||||
|
||||
const x = getHeadX(o.snake);
|
||||
const y = getHeadY(o.snake);
|
||||
|
||||
if (isOutside(outside, x, y)) return unwrap(o);
|
||||
|
||||
for (const a of around4) {
|
||||
const c = getColorSafe(grid, x + a.x, y + a.y);
|
||||
|
||||
if (c <= color && !snakeWillSelfCollide(o.snake, a.x, a.y)) {
|
||||
const snake = nextSnake(o.snake, a.x, a.y);
|
||||
|
||||
if (!closeList.some((s0) => snakeEquals(s0, snake))) {
|
||||
const w = o.w + 1 + +(c === color) * 1000;
|
||||
sortPush(openList, { snake, w, parent: o }, (a, b) => a.w - b.w);
|
||||
closeList.push(snake);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
/**
|
||||
* compute the best tunnel to get to the cell and back to the outside ( best = less usage of <color> )
|
||||
*
|
||||
* notice that it's one of the best tunnels, more with the same score could exist
|
||||
*/
|
||||
export const getBestTunnel = (
|
||||
grid: Grid,
|
||||
outside: Outside,
|
||||
x: number,
|
||||
y: number,
|
||||
color: Color,
|
||||
snakeN: number
|
||||
) => {
|
||||
const c = { x, y };
|
||||
const snake0 = createSnakeFromCells(Array.from({ length: snakeN }, () => c));
|
||||
|
||||
const one = getSnakeEscapePath(grid, outside, snake0, color);
|
||||
|
||||
if (!one) return null;
|
||||
|
||||
// get the position of the snake if it was going to leave the x,y cell
|
||||
const snakeICells = one.slice(0, snakeN);
|
||||
while (snakeICells.length < snakeN)
|
||||
snakeICells.push(snakeICells[snakeICells.length - 1]);
|
||||
const snakeI = createSnakeFromCells(snakeICells);
|
||||
|
||||
// remove from the grid the colors that one eat
|
||||
const gridI = copyGrid(grid);
|
||||
for (const { x, y } of one) setEmptySafe(gridI, x, y);
|
||||
|
||||
const two = getSnakeEscapePath(gridI, outside, snakeI, color);
|
||||
|
||||
if (!two) return null;
|
||||
|
||||
one.shift();
|
||||
one.reverse();
|
||||
one.push(...two);
|
||||
|
||||
trimTunnelStart(grid, one);
|
||||
trimTunnelEnd(grid, one);
|
||||
|
||||
return one;
|
||||
};
|
||||
66
packages/solver/getPathTo.ts
Normal file
66
packages/solver/getPathTo.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import { isInsideLarge, getColor, isInside, isEmpty } from "@snk/types/grid";
|
||||
import { around4 } from "@snk/types/point";
|
||||
import {
|
||||
getHeadX,
|
||||
getHeadY,
|
||||
nextSnake,
|
||||
snakeEquals,
|
||||
snakeWillSelfCollide,
|
||||
} from "@snk/types/snake";
|
||||
import { sortPush } from "./utils/sortPush";
|
||||
import type { Snake } from "@snk/types/snake";
|
||||
import type { Grid } from "@snk/types/grid";
|
||||
|
||||
type M = { parent: M | null; snake: Snake; w: number; h: number; f: number };
|
||||
|
||||
/**
|
||||
* starting from snake0, get to the cell x,y
|
||||
* return the snake chain (reversed)
|
||||
*/
|
||||
export const getPathTo = (grid: Grid, snake0: Snake, x: number, y: number) => {
|
||||
const openList: M[] = [{ snake: snake0, w: 0 } as any];
|
||||
const closeList: Snake[] = [];
|
||||
|
||||
while (openList.length) {
|
||||
const c = openList.shift()!;
|
||||
|
||||
const cx = getHeadX(c.snake);
|
||||
const cy = getHeadY(c.snake);
|
||||
|
||||
for (let i = 0; i < around4.length; i++) {
|
||||
const { x: dx, y: dy } = around4[i];
|
||||
|
||||
const nx = cx + dx;
|
||||
const ny = cy + dy;
|
||||
|
||||
if (nx === x && ny === y) {
|
||||
// unwrap
|
||||
const path = [nextSnake(c.snake, dx, dy)];
|
||||
let e: M["parent"] = c;
|
||||
while (e.parent) {
|
||||
path.push(e.snake);
|
||||
e = e.parent;
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
if (
|
||||
isInsideLarge(grid, 2, nx, ny) &&
|
||||
!snakeWillSelfCollide(c.snake, dx, dy) &&
|
||||
(!isInside(grid, nx, ny) || isEmpty(getColor(grid, nx, ny)))
|
||||
) {
|
||||
const nsnake = nextSnake(c.snake, dx, dy);
|
||||
|
||||
if (!closeList.some((s) => snakeEquals(nsnake, s))) {
|
||||
const w = c.w + 1;
|
||||
const h = Math.abs(nx - x) + Math.abs(ny - y);
|
||||
const f = w + h;
|
||||
const o = { snake: nsnake, parent: c, w, h, f };
|
||||
|
||||
sortPush(openList, o, (a, b) => a.f - b.f);
|
||||
closeList.push(nsnake);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
99
packages/solver/getPathToPose.ts
Normal file
99
packages/solver/getPathToPose.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
import {
|
||||
getHeadX,
|
||||
getHeadY,
|
||||
getSnakeLength,
|
||||
nextSnake,
|
||||
snakeEquals,
|
||||
snakeToCells,
|
||||
snakeWillSelfCollide,
|
||||
} from "@snk/types/snake";
|
||||
import type { Snake } from "@snk/types/snake";
|
||||
import {
|
||||
getColor,
|
||||
Grid,
|
||||
isEmpty,
|
||||
isInside,
|
||||
isInsideLarge,
|
||||
} from "@snk/types/grid";
|
||||
import { getTunnelPath } from "./tunnel";
|
||||
import { around4 } from "@snk/types/point";
|
||||
import { sortPush } from "./utils/sortPush";
|
||||
|
||||
const isEmptySafe = (grid: Grid, x: number, y: number) =>
|
||||
!isInside(grid, x, y) || isEmpty(getColor(grid, x, y));
|
||||
|
||||
type M = { snake: Snake; parent: M | null; w: number; f: number };
|
||||
export const getPathToPose = (snake0: Snake, target: Snake, grid?: Grid) => {
|
||||
if (snakeEquals(snake0, target)) return [];
|
||||
|
||||
const targetCells = snakeToCells(target).reverse();
|
||||
|
||||
const snakeN = getSnakeLength(snake0);
|
||||
const box = {
|
||||
min: {
|
||||
x: Math.min(getHeadX(snake0), getHeadX(target)) - snakeN - 1,
|
||||
y: Math.min(getHeadY(snake0), getHeadY(target)) - snakeN - 1,
|
||||
},
|
||||
max: {
|
||||
x: Math.max(getHeadX(snake0), getHeadX(target)) + snakeN + 1,
|
||||
y: Math.max(getHeadY(snake0), getHeadY(target)) + snakeN + 1,
|
||||
},
|
||||
};
|
||||
|
||||
const [t0, ...forbidden] = targetCells;
|
||||
|
||||
forbidden.slice(0, 3);
|
||||
|
||||
const openList: M[] = [{ snake: snake0, w: 0 } as any];
|
||||
const closeList: Snake[] = [];
|
||||
|
||||
while (openList.length) {
|
||||
const o = openList.shift()!;
|
||||
|
||||
const x = getHeadX(o.snake);
|
||||
const y = getHeadY(o.snake);
|
||||
|
||||
if (x === t0.x && y === t0.y) {
|
||||
const path: Snake[] = [];
|
||||
let e: M["parent"] = o;
|
||||
while (e) {
|
||||
path.push(e.snake);
|
||||
e = e.parent;
|
||||
}
|
||||
path.unshift(...getTunnelPath(path[0], targetCells));
|
||||
path.pop();
|
||||
path.reverse();
|
||||
return path;
|
||||
}
|
||||
|
||||
for (let i = 0; i < around4.length; i++) {
|
||||
const { x: dx, y: dy } = around4[i];
|
||||
|
||||
const nx = x + dx;
|
||||
const ny = y + dy;
|
||||
|
||||
if (
|
||||
!snakeWillSelfCollide(o.snake, dx, dy) &&
|
||||
(!grid || isEmptySafe(grid, nx, ny)) &&
|
||||
(grid
|
||||
? isInsideLarge(grid, 2, nx, ny)
|
||||
: box.min.x <= nx &&
|
||||
nx <= box.max.x &&
|
||||
box.min.y <= ny &&
|
||||
ny <= box.max.y) &&
|
||||
!forbidden.some((p) => p.x === nx && p.y === ny)
|
||||
) {
|
||||
const snake = nextSnake(o.snake, dx, dy);
|
||||
|
||||
if (!closeList.some((s) => snakeEquals(snake, s))) {
|
||||
const w = o.w + 1;
|
||||
const h = Math.abs(nx - x) + Math.abs(ny - y);
|
||||
const f = w + h;
|
||||
|
||||
sortPush(openList, { f, w, snake, parent: o }, (a, b) => a.f - b.f);
|
||||
closeList.push(snake);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
48
packages/solver/outside.ts
Normal file
48
packages/solver/outside.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import {
|
||||
createEmptyGrid,
|
||||
getColor,
|
||||
isEmpty,
|
||||
isInside,
|
||||
setColor,
|
||||
setColorEmpty,
|
||||
} from "@snk/types/grid";
|
||||
import { around4 } from "@snk/types/point";
|
||||
import type { Color, Grid } from "@snk/types/grid";
|
||||
|
||||
export type Outside = Grid & { __outside: true };
|
||||
|
||||
export const createOutside = (grid: Grid, color: Color = 0 as Color) => {
|
||||
const outside = createEmptyGrid(grid.width, grid.height) as Outside;
|
||||
for (let x = outside.width; x--; )
|
||||
for (let y = outside.height; y--; ) setColor(outside, x, y, 1 as Color);
|
||||
|
||||
fillOutside(outside, grid, color);
|
||||
|
||||
return outside;
|
||||
};
|
||||
|
||||
export const fillOutside = (
|
||||
outside: Outside,
|
||||
grid: Grid,
|
||||
color: Color = 0 as Color
|
||||
) => {
|
||||
let changed = true;
|
||||
while (changed) {
|
||||
changed = false;
|
||||
for (let x = outside.width; x--; )
|
||||
for (let y = outside.height; y--; )
|
||||
if (
|
||||
getColor(grid, x, y) <= color &&
|
||||
!isOutside(outside, x, y) &&
|
||||
around4.some((a) => isOutside(outside, x + a.x, y + a.y))
|
||||
) {
|
||||
changed = true;
|
||||
setColorEmpty(outside, x, y);
|
||||
}
|
||||
}
|
||||
|
||||
return outside;
|
||||
};
|
||||
|
||||
export const isOutside = (outside: Outside, x: number, y: number) =>
|
||||
!isInside(outside, x, y) || isEmpty(getColor(outside, x, y));
|
||||
7
packages/solver/package.json
Normal file
7
packages/solver/package.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"name": "@snk/solver",
|
||||
"version": "1.0.0",
|
||||
"devDependencies": {
|
||||
"park-miller": "1.1.0"
|
||||
}
|
||||
}
|
||||
20
packages/solver/step.ts
Normal file
20
packages/solver/step.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import {
|
||||
Color,
|
||||
getColor,
|
||||
Grid,
|
||||
isEmpty,
|
||||
isInside,
|
||||
setColorEmpty,
|
||||
} from "@snk/types/grid";
|
||||
import { getHeadX, getHeadY, Snake } from "@snk/types/snake";
|
||||
|
||||
export const step = (grid: Grid, stack: Color[], snake: Snake) => {
|
||||
const x = getHeadX(snake);
|
||||
const y = getHeadY(snake);
|
||||
const color = getColor(grid, x, y);
|
||||
|
||||
if (isInside(grid, x, y) && !isEmpty(color)) {
|
||||
stack.push(color);
|
||||
setColorEmpty(grid, x, y);
|
||||
}
|
||||
};
|
||||
81
packages/solver/tunnel.ts
Normal file
81
packages/solver/tunnel.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
import { getColor, isEmpty, isInside } from "@snk/types/grid";
|
||||
import { getHeadX, getHeadY, nextSnake } from "@snk/types/snake";
|
||||
import type { Snake } from "@snk/types/snake";
|
||||
import type { Grid } from "@snk/types/grid";
|
||||
import type { Point } from "@snk/types/point";
|
||||
|
||||
/**
|
||||
* get the sequence of snake to cross the tunnel
|
||||
*/
|
||||
export const getTunnelPath = (snake0: Snake, tunnel: Point[]) => {
|
||||
const chain: Snake[] = [];
|
||||
let snake = snake0;
|
||||
|
||||
for (let i = 1; i < tunnel.length; i++) {
|
||||
const dx = tunnel[i].x - getHeadX(snake);
|
||||
const dy = tunnel[i].y - getHeadY(snake);
|
||||
snake = nextSnake(snake, dx, dy);
|
||||
chain.unshift(snake);
|
||||
}
|
||||
|
||||
return chain;
|
||||
};
|
||||
|
||||
/**
|
||||
* assuming the grid change and the colors got deleted, update the tunnel
|
||||
*/
|
||||
export const updateTunnel = (
|
||||
grid: Grid,
|
||||
tunnel: Point[],
|
||||
toDelete: Point[]
|
||||
) => {
|
||||
while (tunnel.length) {
|
||||
const { x, y } = tunnel[0];
|
||||
if (
|
||||
isEmptySafe(grid, x, y) ||
|
||||
toDelete.some((p) => p.x === x && p.y === y)
|
||||
) {
|
||||
tunnel.shift();
|
||||
} else break;
|
||||
}
|
||||
|
||||
while (tunnel.length) {
|
||||
const { x, y } = tunnel[tunnel.length - 1];
|
||||
if (
|
||||
isEmptySafe(grid, x, y) ||
|
||||
toDelete.some((p) => p.x === x && p.y === y)
|
||||
) {
|
||||
tunnel.pop();
|
||||
} else break;
|
||||
}
|
||||
};
|
||||
|
||||
const isEmptySafe = (grid: Grid, x: number, y: number) =>
|
||||
!isInside(grid, x, y) || isEmpty(getColor(grid, x, y));
|
||||
|
||||
/**
|
||||
* remove empty cell from start
|
||||
*/
|
||||
export const trimTunnelStart = (grid: Grid, tunnel: Point[]) => {
|
||||
while (tunnel.length) {
|
||||
const { x, y } = tunnel[0];
|
||||
if (isEmptySafe(grid, x, y)) tunnel.shift();
|
||||
else break;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* remove empty cell from end
|
||||
*/
|
||||
export const trimTunnelEnd = (grid: Grid, tunnel: Point[]) => {
|
||||
while (tunnel.length) {
|
||||
const i = tunnel.length - 1;
|
||||
const { x, y } = tunnel[i];
|
||||
if (
|
||||
isEmptySafe(grid, x, y) ||
|
||||
tunnel.findIndex((p) => p.x === x && p.y === y) < i
|
||||
)
|
||||
tunnel.pop();
|
||||
else break;
|
||||
}
|
||||
};
|
||||
2
packages/solver/utils/array.ts
Normal file
2
packages/solver/utils/array.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export const arrayEquals = <T>(a: T[], b: T[]) =>
|
||||
a.length === b.length && a.every((_, i) => a[i] === b[i]);
|
||||
22
packages/solver/utils/sortPush.ts
Normal file
22
packages/solver/utils/sortPush.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
export const sortPush = <T>(arr: T[], x: T, sortFn: (a: T, b: T) => number) => {
|
||||
let a = 0;
|
||||
let b = arr.length;
|
||||
|
||||
if (arr.length === 0 || sortFn(x, arr[a]) <= 0) {
|
||||
arr.unshift(x);
|
||||
return;
|
||||
}
|
||||
|
||||
while (b - a > 1) {
|
||||
const e = Math.ceil((a + b) / 2);
|
||||
|
||||
const s = sortFn(x, arr[e]);
|
||||
|
||||
if (s === 0) a = b = e;
|
||||
else if (s > 0) a = e;
|
||||
else b = e;
|
||||
}
|
||||
|
||||
const e = Math.ceil((a + b) / 2);
|
||||
arr.splice(e, 0, x);
|
||||
};
|
||||
Reference in New Issue
Block a user