🚀 refactor get available routes

This commit is contained in:
platane
2020-09-29 10:34:12 +02:00
committed by Platane
parent 2499529b1d
commit 8d8956229c
15 changed files with 190 additions and 454 deletions

View File

@@ -1,79 +0,0 @@
// @ts-ignore
import * as ParkMiller from "park-miller";
import { generateRandomGrid } from "../generateGrid";
import { Snake } from "../snake";
import { Grid } from "../grid";
import { getBestRoute } from "../getBestRoute";
import { performance } from "perf_hooks";
const snake0 = [
{ x: 4, y: -1 },
{ x: 3, y: -1 },
{ x: 2, y: -1 },
{ x: 1, y: -1 },
{ x: 0, y: -1 },
];
const gameOptions = {
maxSnakeLength: 5,
colors: [1, 2, 3, 4],
};
const MAX_DURATION = 60 * 1000;
const MAX_ITERATION = 10;
const run = (grid: Grid, snake0: Snake, k: number) => {
const stats: number[] = [];
const s0 = performance.now();
let n = 0;
while (performance.now() - s0 < MAX_DURATION && n++ < MAX_ITERATION) {
const s = performance.now();
getBestRoute(grid, snake0, gameOptions, k);
stats.push(performance.now() - s);
}
return stats;
};
const report = (arr: number[]) => {
const average = (arr: number[]) =>
arr.reduce((s, x) => s + x, 0) / arr.length;
const spread = (arr: number[]) => {
const m = average(arr);
const v = average(arr.map((x) => Math.pow(x - m, 2)));
return Math.sqrt(v) / m;
};
const format = (x: number): string => {
const u = Math.floor(x / 1000);
const d = Math.floor(x % 1000).toString();
return u === 0 ? d : format(u) + " " + d.padEnd(3, "0");
};
return `${format(average(arr)).padStart(12)} ms ±${(spread(arr) * 100)
.toFixed(2)
.padStart(5)}% (x${arr.length.toString().padStart(3)})`;
};
[
//
[10, 10, 100],
[30, 7, 100],
[52, 7, 100],
[10, 10, 800],
[30, 7, 800],
[52, 7, 800],
].forEach(([w, h, k]) => {
const random = new ParkMiller(10);
const grid = generateRandomGrid(w, h, { ...gameOptions, emptyP: 3 }, (a, b) =>
random.integerInRange(a, b - 1)
);
const stats = run(grid, snake0, k);
console.log(`${w}x${h} : ${k}\n ${report(stats)}\n`);
});

View File

@@ -1,21 +0,0 @@
import { createEmptyGrid, setColor } from "../grid";
import { getAvailableRoutes } from "../getAvailableRoutes";
it("should find no routes in empty grid", () => {
const grid = createEmptyGrid(10, 10);
const snake = [{ x: 2, y: 2 }];
const options = { maxSnakeLength: 1 };
expect(getAvailableRoutes(grid, snake, options)).toEqual([]);
});
it("should find one route in single cell grid", () => {
const grid = createEmptyGrid(10, 10);
setColor(grid, 3, 2, 3);
const snake = [{ x: 2, y: 2 }];
const options = { maxSnakeLength: 1 };
expect(getAvailableRoutes(grid, snake, options)).toEqual([
{ color: 3, snakeN: [{ x: 3, y: 2 }], directions: [{ x: 1, y: 0 }] },
]);
});

View File

@@ -3,7 +3,7 @@ import { createEmptyGrid, setColor, getColor, isInside } from "../grid";
it("should set / get cell", () => {
const grid = createEmptyGrid(2, 3);
expect(getColor(grid, 0, 1)).toBe(null);
expect(getColor(grid, 0, 1)).toBe(0);
setColor(grid, 0, 1, 1);

View File

@@ -1,62 +1,43 @@
import { snakeSelfCollide, snakeWillSelfCollide } from "../snake";
import {
createSnake,
nextSnake,
snakeToCells,
snakeWillSelfCollide,
} from "../snake";
test.each([
[[{ x: 0, y: 0 }], false],
[
[
{ x: 0, y: 0 },
{ x: 0, y: 0 },
],
true,
],
[
[
{ x: 1, y: 7 },
{ x: 0, y: 6 },
{ x: 2, y: 8 },
{ x: 1, y: 7 },
{ x: 3, y: 9 },
],
true,
],
])("should report snake collision", (snake, collide) => {
expect(snakeSelfCollide(snake)).toBe(collide);
it("should convert to point", () => {
const snk0 = [
{ x: 1, y: -1 },
{ x: 1, y: 0 },
{ x: 0, y: 0 },
];
expect(snakeToCells(createSnake(snk0))).toEqual(snk0);
});
test.each([
[
[
{ x: 1, y: 7 },
{ x: 0, y: 7 },
{ x: 0, y: 8 },
{ x: 0, y: 9 },
{ x: 1, y: 9 },
],
{ x: 1, y: 7 },
true,
],
[
[
{ x: 1, y: 7 },
{ x: 0, y: 7 },
{ x: 0, y: 8 },
{ x: 0, y: 9 },
{ x: 1, y: 9 },
],
{ x: 1, y: 8 },
false,
],
[
[
{ x: 1, y: 7 },
{ x: 0, y: 7 },
{ x: 0, y: 8 },
{ x: 0, y: 9 },
{ x: 1, y: 9 },
],
{ x: 1, y: 8 },
false,
],
])("should report snake collision next", (snake, { x, y }, collide) => {
expect(snakeWillSelfCollide(snake, x, y)).toBe(collide);
it("should return next snake", () => {
const snk0 = [
{ x: 1, y: 1 },
{ x: 1, y: 0 },
{ x: 0, y: 0 },
];
const snk1 = [
{ x: 2, y: 1 },
{ x: 1, y: 1 },
{ x: 1, y: 0 },
];
expect(snakeToCells(nextSnake(createSnake(snk0), 1, 0))).toEqual(snk1);
});
it("should test snake collision", () => {
const snk0 = [
{ x: 1, y: 1 },
{ x: 1, y: 0 },
{ x: 0, y: 0 },
];
expect(snakeWillSelfCollide(createSnake(snk0), 1, 0)).toBe(false);
expect(snakeWillSelfCollide(createSnake(snk0), 0, -1)).toBe(true);
});

View File

@@ -1,93 +0,0 @@
import { step } from "../step";
import { around4 } from "../point";
import { createEmptyGrid, setColor, getColor } from "../grid";
it("should move snake", () => {
const grid = createEmptyGrid(4, 3);
const snake = [{ x: 1, y: 1 }];
const direction = around4[0];
const stack: number[] = [];
const options = { maxSnakeLength: 5 };
step(grid, snake, stack, direction, options);
expect(snake).toEqual([
{ x: 2, y: 1 },
{ x: 1, y: 1 },
]);
step(grid, snake, stack, direction, options);
expect(snake).toEqual([
{ x: 3, y: 1 },
{ x: 2, y: 1 },
{ x: 1, y: 1 },
]);
step(grid, snake, stack, direction, options);
expect(snake).toEqual([
{ x: 4, y: 1 },
{ x: 3, y: 1 },
{ x: 2, y: 1 },
{ x: 1, y: 1 },
]);
});
it("should move short snake", () => {
const grid = createEmptyGrid(8, 3);
const snake = [{ x: 1, y: 1 }];
const direction = around4[0];
const stack: number[] = [];
const options = { maxSnakeLength: 3 };
step(grid, snake, stack, direction, options);
expect(snake).toEqual([
{ x: 2, y: 1 },
{ x: 1, y: 1 },
]);
step(grid, snake, stack, direction, options);
expect(snake).toEqual([
{ x: 3, y: 1 },
{ x: 2, y: 1 },
{ x: 1, y: 1 },
]);
step(grid, snake, stack, direction, options);
expect(snake).toEqual([
{ x: 4, y: 1 },
{ x: 3, y: 1 },
{ x: 2, y: 1 },
]);
step(grid, snake, stack, direction, options);
expect(snake).toEqual([
{ x: 5, y: 1 },
{ x: 4, y: 1 },
{ x: 3, y: 1 },
]);
});
it("should pick up fruit", () => {
const grid = createEmptyGrid(4, 3);
const snake = [{ x: 1, y: 1 }];
const direction = around4[0];
const stack: number[] = [];
const options = { maxSnakeLength: 2 };
setColor(grid, 3, 1, 9);
step(grid, snake, stack, direction, options);
expect(getColor(grid, 3, 1)).toBe(9);
expect(stack).toEqual([]);
step(grid, snake, stack, direction, options);
expect(getColor(grid, 3, 1)).toBe(null);
expect(stack).toEqual([9]);
});

View File

@@ -1,111 +1,58 @@
import { Grid, isInsideLarge, getColor, isInside } from "./grid";
import { around4, Point, pointEquals } from "./point";
import { snakeWillSelfCollide, Snake } from "./snake";
const computeSnakeKey = (snake: Snake) => {
let key = "";
for (let i = 0; i < snake.length; i++) key += snake[i].x + "," + snake[i].y;
return key;
};
type I = {
// h: number;
// f: number;
w: number;
key: string;
snake: Snake;
parent: I | null;
};
const unwrap = (o: I | null, headN: Point): Point[] => {
if (!o) return [];
const head0 = o.snake[0];
return [
...unwrap(o.parent, head0),
{ x: headN.x - head0.x, y: headN.y - head0.y },
];
};
const snakeEquals = (a: Snake, b: Snake, n = 99999) => {
for (let i = 0; i < Math.min(a.length, b.length, n); i++)
if (!pointEquals(a[i], b[i])) return false;
return true;
};
import { Grid, isInsideLarge, getColor, isInside, Color } from "./grid";
import { around4 } from "./point";
import {
getHeadX,
getHeadY,
nextSnake,
Snake,
snakeEquals,
snakeWillSelfCollide,
} from "./snake";
export const getAvailableRoutes = (
grid: Grid,
snake0: Snake,
options: { maxSnakeLength: number },
maxSolutions = 10,
maxLengthEquality = 1,
maxWeight = 30,
maxIterations = 500
onSolution: (snakes: Snake[], color: Color) => boolean
) => {
const openList: I[] = [
{
key: computeSnakeKey(snake0),
snake: snake0,
w: 0,
parent: null,
},
];
const closeList: Record<string, I> = {};
const openList: Snake[][] = [[snake0]];
const closeList: Snake[] = [];
const solutions: { snakeN: Snake; directions: Point[] }[] = [];
while (
openList.length &&
maxIterations-- > 0 &&
openList[0].w <= maxWeight &&
solutions.length < maxSolutions
) {
openList.sort((a, b) => a.w - b.w);
while (openList.length) {
const c = openList.shift()!;
const [snake] = c;
closeList[c.key] = c;
closeList.push(snake);
const [head] = c.snake;
const color =
isInside(grid, head.x, head.y) && getColor(grid, head.x, head.y);
const cx = getHeadX(snake);
const cy = getHeadY(snake);
if (color) {
const s0 = solutions.find((s) =>
snakeEquals(s.snakeN, c.snake, maxLengthEquality + 1)
);
for (let i = 0; i < around4.length; i++) {
const { x: dx, y: dy } = around4[i];
const directions = unwrap(c.parent, c.snake[0]);
const nx = cx + dx;
const ny = cy + dy;
if (!s0 || directions.length < s0.directions.length)
solutions.push({ snakeN: c.snake, directions });
} else {
for (let i = 0; i < around4.length; i++) {
const x = head.x + around4[i].x;
const y = head.y + around4[i].y;
if (
isInsideLarge(grid, 1, nx, ny) &&
!snakeWillSelfCollide(snake, dx, dy)
) {
const nsnake = nextSnake(snake, dx, dy);
if (
isInsideLarge(grid, 1, x, y) &&
!snakeWillSelfCollide(c.snake, x, y)
) {
const snake = c.snake.slice(0, options.maxSnakeLength - 1);
snake.unshift({ x, y });
if (!closeList.some((s) => snakeEquals(nsnake, s))) {
const color = isInside(grid, nx, ny) && getColor(grid, nx, ny);
const key = computeSnakeKey(snake);
const chain = [nsnake, ...c];
if (!closeList[key] && !openList.some((s) => s.key === key)) {
const w = 1 + c.w;
openList.push({ key, snake, w, parent: c });
if (color) {
if (onSolution(chain, color)) return;
} else {
// console.log(key, closeList);
// debugger;
if (!openList.some(([s]) => snakeEquals(nsnake, s))) {
openList.push(chain);
openList.sort((a, b) => a.length - b.length);
}
}
}
}
}
}
return solutions;
};
export const snakeSteps: Snake[] = [];

View File

@@ -3,7 +3,7 @@ export type Color = number;
export type Grid = {
width: number;
height: number;
data: (Color | null)[];
data: Uint8Array;
};
export const getIndex = (grid: Grid, x: number, y: number) =>
@@ -18,7 +18,11 @@ export const isInsideLarge = (grid: Grid, m: number, x: number, y: number) =>
export const getColor = (grid: Grid, x: number, y: number) =>
grid.data[getIndex(grid, x, y)];
export const copyGrid = (grid: Grid) => ({ ...grid, data: grid.data.slice() });
export const copyGrid = ({ width, height, data }: Grid) => ({
width,
height,
data: Uint8Array.from(data),
});
export const setColor = (
grid: Grid,
@@ -26,11 +30,11 @@ export const setColor = (
y: number,
color: Color | null
) => {
grid.data[getIndex(grid, x, y)] = color;
grid.data[getIndex(grid, x, y)] = color || 0;
};
export const createEmptyGrid = (width: number, height: number) => ({
width,
height,
data: Array.from({ length: width * height }, () => null),
data: new Uint8Array(width * height),
});

View File

@@ -5,6 +5,6 @@ export const around4 = [
{ x: 0, y: -1 },
{ x: -1, y: 0 },
{ x: 0, y: 1 },
];
] as const;
export const pointEquals = (a: Point, b: Point) => a.x === b.x && a.y === b.y;

View File

@@ -1,37 +1,44 @@
import { Point } from "./point";
export type Snake = Point[];
export type Snake = Uint8Array & { _tag: "__Snake__" };
export const snakeSelfCollideNext = (
snake: Snake,
direction: Point,
options: { maxSnakeLength: number }
) => {
const hx = snake[0].x + direction.x;
const hy = snake[0].y + direction.y;
export const getHeadX = (snake: Snake) => snake[0] - 2;
export const getHeadY = (snake: Snake) => snake[1] - 2;
for (let i = 0; i < Math.min(options.maxSnakeLength, snake.length); i++)
if (snake[i].x === hx && snake[i].y === hy) return true;
export const snakeEquals = (a: Snake, b: Snake) => {
for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;
return true;
};
export const nextSnake = (snake: Snake, dx: number, dy: number) => {
const copy = new Uint8Array(snake.length);
for (let i = 2; i < snake.length; i++) copy[i] = snake[i - 2];
copy[0] = snake[0] + dx;
copy[1] = snake[1] + dy;
return copy as Snake;
};
export const snakeWillSelfCollide = (snake: Snake, dx: number, dy: number) => {
const nx = snake[0] + dx;
const ny = snake[1] + dy;
for (let i = 2; i < snake.length - 2; i += 2)
if (snake[i + 0] === nx && snake[i + 1] === ny) return true;
return false;
};
export const snakeWillSelfCollide = (
snake: Snake,
headx: number,
heady: number
) => {
for (let i = 0; i < snake.length - 1; i++)
if (snake[i].x === headx && snake[i].y === heady) return true;
export const snakeToCells = (snake: Snake) =>
Array.from({ length: snake.length / 2 }, (_, i) => ({
x: snake[i * 2 + 0] - 2,
y: snake[i * 2 + 1] - 2,
}));
return false;
export const createSnake = (points: Point[]) => {
const snake = new Uint8Array(points.length * 2);
for (let i = points.length; i--; ) {
snake[i * 2 + 0] = points[i].x + 2;
snake[i * 2 + 1] = points[i].y + 2;
}
return snake as Snake;
};
export const snakeSelfCollide = (snake: Snake) => {
for (let i = 1; i < snake.length; i++)
if (snake[i].x === snake[0].x && snake[i].y === snake[0].y) return true;
return false;
};
export const copySnake = (x: Snake) => x.map((p) => ({ ...p }));