🚀 refactor demo

This commit is contained in:
platane
2020-10-02 18:00:04 +02:00
committed by Platane
parent 9b92697ef9
commit 64f0b872aa
25 changed files with 561 additions and 605 deletions

View File

@@ -0,0 +1,33 @@
// @ts-ignore
import * as ParkMiller from "park-miller";
import { Color, createEmptyGrid, setColor } from "@snk/compute/grid";
import { fillRandomGrid } from "../generateGrid";
const colors = [1, 2, 3] as Color[];
// empty small grid
export const empty = createEmptyGrid(5, 5);
// empty small grid with a unique color at the middle
export const simple = createEmptyGrid(5, 5);
setColor(simple, 2, 2, 1 as Color);
// empty small grid with color at each corner
export const corner = createEmptyGrid(5, 5);
setColor(corner, 0, 4, 1 as Color);
setColor(corner, 4, 0, 1 as Color);
setColor(corner, 4, 4, 1 as Color);
setColor(corner, 0, 0, 1 as Color);
const create = (width: number, height: number, emptyP: number) => {
const grid = createEmptyGrid(width, height);
const random = new ParkMiller(10);
const rand = (a: number, b: number) => random.integerInRange(a, b - 1);
fillRandomGrid(grid, { colors, emptyP }, rand);
return grid;
};
// small realistic
export const small = create(10, 7, 3);
export const smallPacked = create(10, 7, 1);
export const smallFull = create(10, 7, 0);

View File

@@ -0,0 +1,9 @@
// @ts-ignore
import { createSnake } from "../snake";
const create = (length: number) =>
createSnake(Array.from({ length }, (_, i) => ({ x: i, y: -1 })));
export const snake1 = create(1);
export const snake3 = create(3);
export const snake7 = create(7);

View File

@@ -0,0 +1,19 @@
import { getBestRoute } from "../getBestRoute";
import { Color, createEmptyGrid, setColor } from "../grid";
import { createSnake, snakeToCells } from "../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, createSnake(snk0))!;
expect(snakeToCells(chain[0])[1]).toEqual({ x: 0, y: 0 });
expect(snakeToCells(chain[chain.length - 1])[0]).toEqual({ x: 3, y: 3 });
});

View File

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

View File

@@ -1,25 +1,21 @@
import { Grid, Color, setColor, createEmptyGrid } from "./grid";
import { Grid, Color, setColor, setColorEmpty } from "./grid";
const defaultRand = (a: number, b: number) =>
Math.floor(Math.random() * (b - a)) + a;
export const generateRandomGrid = (
width: number,
height: number,
options: { colors: Color[]; emptyP: number } = {
colors: [1, 2, 3],
emptyP: 2,
},
export const fillRandomGrid = (
grid: Grid,
{
colors = [1, 2, 3] as Color[],
emptyP = 2,
}: { colors?: Color[]; emptyP?: number } = {},
rand = defaultRand
): Grid => {
const grid = createEmptyGrid(width, height);
) => {
for (let x = grid.width; x--; )
for (let y = grid.height; y--; ) {
const k = rand(-emptyP, colors.length);
for (let x = width; x--; )
for (let y = height; y--; ) {
const k = rand(-options.emptyP, options.colors.length);
if (k >= 0) setColor(grid, x, y, options.colors[k]);
if (k >= 0) setColor(grid, x, y, colors[k]);
else setColorEmpty(grid, x, y);
}
return grid;
};

View File

@@ -1,4 +1,11 @@
import { Grid, isInsideLarge, getColor, isInside, Color } from "./grid";
import {
Grid,
isInsideLarge,
getColor,
isInside,
Color,
isEmpty,
} from "./grid";
import { around4 } from "./point";
import {
getHeadX,
@@ -41,12 +48,11 @@ export const getAvailableRoutes = (
if (!closeList.some((s) => snakeEquals(nsnake, s))) {
const color = isInside(grid, nx, ny) && getColor(grid, nx, ny);
const chain = [nsnake, ...c];
if (color) {
if (onSolution(chain, color)) return;
if (color && !isEmpty(color)) {
if (onSolution([nsnake, ...c.slice(0, -1)], color)) return;
} else {
if (!openList.some(([s]) => snakeEquals(nsnake, s))) {
const chain = [nsnake, ...c];
openList.push(chain);
openList.sort((a, b) => a.length - b.length);
}
@@ -57,7 +63,7 @@ export const getAvailableRoutes = (
}
};
export const getInterestingAvailableRoutes = (
export const getAvailableInterestingRoutes = (
grid: Grid,
snake0: Snake,
onSolution: (snakes: Snake[], color: Color) => boolean,

View File

@@ -1,152 +1,183 @@
import { Grid, Color, copyGrid, getColor, setColor } from "./grid";
import { Point } from "./point";
import { Snake } from "./snake";
import { getAvailableRoutes } from "./getAvailableRoutes";
import { getAvailableInterestingRoutes } from "./getAvailableRoutes";
import {
Color,
copyGrid,
getColor,
Grid,
gridEquals,
isEmpty,
setColorEmpty,
} from "./grid";
import { copySnake, getHeadX, getHeadY, Snake, snakeEquals } from "./snake";
const isGridEmpty = (grid: Grid) => grid.data.every((x) => x === null);
const createComputeHeuristic = (
grid0: Grid,
_snake0: Snake,
colors: Color[]
) => {
const colorCount: Record<Color, number> = {};
const createHeuristic = (grid0: Grid) => {
const colorCount: Record<Color, number> = [];
for (let x = grid0.width; x--; )
for (let y = grid0.height; y--; ) {
const c = getColor(grid0, x, y);
if (c !== null) colorCount[c] = 1 + (colorCount[c] || 0);
const color = getColor(grid0, x, y);
if (!isEmpty(color))
// @ts-ignore
colorCount[color] = (0 | colorCount[color]) + 1;
}
const values = colors
.map((k) => Array.from({ length: colorCount[k] }, () => k))
const target = Object.entries(colorCount)
.sort(([a], [b]) => +a - +b)
.map(([color, length]: any) => Array.from({ length }, () => +color))
.flat();
return (_grid: Grid, _snake: Snake, stack: Color[]) => {
let score = 0;
const getHeuristic = (_grid: Grid, _snake: Snake, stack: Color[]) =>
stack.reduce((s, x, i) => s + (target[i] === x ? 1 : 0), 0);
for (let i = 0; i < stack.length; i++) {
if (stack[i] === values[i]) {
score += 52;
} else {
const u = Math.abs(stack[i] - values[i]);
const getNextColorHeuristic = (
_grid: Grid,
_snake: Snake,
stack: Color[]
) => {
const x = target[stack.length];
score += 5 - u;
}
}
return score;
return (c: Color) => (x === c ? 1 : 0);
};
const isEnd = (_grid: Grid, _snake: Snake, stack: Color[]) =>
stack.length === target.length;
return { isEnd, getHeuristic, getNextColorHeuristic };
};
const computeKey = (grid: Grid, snake: Snake, stack: Color[]) =>
grid.data.map((x) => x || 0).join("") +
"|" +
snake.map((p) => p.x + "." + p.y).join(",") +
"|" +
stack.join("");
type I = {
h: number;
f: number;
w: number;
key: string;
snake: Snake;
type OpenListItem = {
grid: Grid;
snake: Snake;
chain: Snake[];
stack: Color[];
parent: I | null;
directions: Point[];
weight: number;
heuristic: number;
parent: OpenListItem | null;
};
export const getBestRoute = (
grid0: Grid,
snake0: Snake,
options: { maxSnakeLength: number; colors: Color[] },
maxIterations = 500
) => {
const computeHeuristic = createComputeHeuristic(
grid0,
snake0,
options.colors
);
const unroll = (o: OpenListItem | null): Snake[] =>
!o ? [] : [...unroll(o.parent), ...o.chain.slice().reverse()];
const closeList: Record<string, I> = {};
const openList: I[] = [];
const itemEquals = (
a: { grid: Grid; snake: Snake },
b: { grid: Grid; snake: Snake }
) => snakeEquals(a.snake, b.snake) && gridEquals(a.grid, b.grid);
{
const h = computeHeuristic(grid0, snake0, []);
const w = 0;
const f = h + w;
openList.push({
key: computeKey(grid0, snake0, []),
export const getBestRoute = (grid0: Grid, snake0: Snake) => {
const { isEnd, getNextColorHeuristic } = createHeuristic(grid0);
let grid = copyGrid(grid0);
let snake = copySnake(snake0);
let stack: Color[] = [];
const fullChain: Snake[] = [];
while (!isEnd(grid, snake, stack)) {
const getColorHeuristic = getNextColorHeuristic(grid, snake, stack);
let solution: {
heuristic: number;
chain: Snake[];
color: Color;
} | null = null;
getAvailableInterestingRoutes(
grid,
snake,
(chain: Snake[], color: Color) => {
const heuristic = getColorHeuristic(color);
if (!solution || solution.heuristic < heuristic)
solution = { heuristic, chain, color };
return solution.heuristic === 1;
},
2
);
if (!solution) return null;
const { chain, color } = solution!;
snake = chain[0];
const x = getHeadX(snake);
const y = getHeadY(snake);
setColorEmpty(grid, x, y);
stack.push(color);
for (let i = chain.length; i--; ) fullChain.push(chain[i]);
}
return fullChain;
};
export const getBestRoute2 = (grid0: Grid, snake0: Snake) => {
const { isEnd, getHeuristic, getNextColorHeuristic } = createHeuristic(grid0);
const closeList: { grid: Grid; snake: Snake }[] = [];
const openList: OpenListItem[] = [
{
grid: grid0,
snake: snake0,
stack: [],
snake: snake0,
parent: null,
f,
h,
w,
directions: [],
});
}
weight: 0,
heuristic: getHeuristic(grid0, snake0, []),
chain: [],
},
];
let best = openList[0];
while (openList.length) {
const parent = openList.shift()!;
while (openList.length && maxIterations-- > 0) {
openList.sort((a, b) => a.f - b.f);
const c = openList.shift()!;
if (isEnd(parent.grid, parent.snake, parent.stack)) return unroll(parent);
closeList[c.key] = c;
const solutions: { snake: Snake; chain: Snake[]; color: Color }[] = [];
const getColorHeuristic = getNextColorHeuristic(
parent.grid,
parent.snake,
parent.stack
);
if (c.f < best.f) best = c;
getAvailableInterestingRoutes(
parent.grid,
parent.snake,
(chain: Snake[], color: Color) => {
if (
!solutions[0] ||
getColorHeuristic(solutions[0].color) <= getColorHeuristic(color)
)
solutions.unshift({ snake: chain[0], chain, color });
if (!isGridEmpty(c.grid)) {
const availableRoutes = getAvailableRoutes(
c.grid,
c.snake,
options,
30,
1,
20,
500
);
return solutions.length >= 3;
},
2
);
for (const route of availableRoutes) {
const stack = c.stack.slice();
const grid = copyGrid(c.grid);
const snake = route.snakeN;
for (const { snake, chain, color } of solutions) {
const x = getHeadX(snake);
const y = getHeadY(snake);
const { x, y } = route.snakeN[0];
const grid = copyGrid(parent.grid);
setColorEmpty(grid, x, y);
stack.push(getColor(grid, x, y)!);
setColor(grid, x, y, null);
const stack = [...parent.stack, color];
const key = computeKey(grid, snake, stack);
const weight = parent.weight + chain.length;
const heuristic = getHeuristic(grid, snake, stack);
if (!closeList[key] && !openList.some((s) => s.key === key)) {
const h = computeHeuristic(grid, snake, stack);
const w = c.w + route.directions.length;
const f = w - h;
const item = { grid, stack, snake, chain, weight, heuristic, parent };
openList.push({
key,
grid,
snake,
stack,
parent: c,
h,
w,
f,
directions: route.directions,
});
}
}
if (!closeList.some((c) => itemEquals(c, item))) {
closeList.push(item);
openList.push(item);
} else console.log("hit");
}
openList.sort((a, b) => a.heuristic - b.heuristic);
}
return unwrap(best);
};
const unwrap = (o: I | null): Point[] => {
if (!o) return [];
return [...unwrap(o.parent), ...o.directions];
return null;
};

View File

@@ -1,4 +1,5 @@
export type Color = number;
export type Color = (1 | 2 | 3 | 4 | 5 | 6) & { _tag: "__Color__" };
export type Empty = 0 & { _tag: "__Empty__" };
export type Grid = {
width: number;
@@ -15,24 +16,48 @@ export const isInside = (grid: Grid, x: number, y: number) =>
export const isInsideLarge = (grid: Grid, m: number, x: number, y: number) =>
x >= -m && y >= -m && x < grid.width + m && y < grid.height + m;
export const getColor = (grid: Grid, x: number, y: number) =>
grid.data[getIndex(grid, x, y)];
export const copyGrid = ({ width, height, data }: Grid) => ({
width,
height,
data: Uint8Array.from(data),
});
export const getColor = (grid: Grid, x: number, y: number) =>
grid.data[getIndex(grid, x, y)] as Color | Empty;
export const isEmpty = (color: Color | Empty): color is Empty => color === 0;
export const setColor = (
grid: Grid,
x: number,
y: number,
color: Color | null
color: Color | Empty
) => {
grid.data[getIndex(grid, x, y)] = color || 0;
};
export const setColorEmpty = (grid: Grid, x: number, y: number) => {
setColor(grid, x, y, 0 as Empty);
};
export const isGridEmpty = (grid: Grid) => grid.data.every((x) => x === 0);
export const gridEquals = (a: Grid, b: Grid) =>
a.data.every((_, i) => a.data[i] === b.data[i]);
export const getGridKey = ({ data }: Grid) => {
let key = "";
const n = 5;
const radius = 1 << n;
for (let k = 0; k < data.length; k += n) {
let u = 0;
for (let i = n; i--; ) u += (1 << i) * +!!data[k + i];
key += u.toString(radius);
}
return key;
};
export const createEmptyGrid = (width: number, height: number) => ({
width,
height,

View File

@@ -42,3 +42,5 @@ export const createSnake = (points: Point[]) => {
}
return snake as Snake;
};
export const copySnake = (snake: Snake) => snake.slice() as Snake;

View File

@@ -1,48 +1,20 @@
import { Grid, Color, getColor, isInside, setColor } from "./grid";
import { Point } from "./point";
import {
Color,
getColor,
Grid,
isEmpty,
isInside,
setColorEmpty,
} from "./grid";
import { getHeadX, getHeadY, Snake } from "./snake";
export const moveSnake = (snake: Point[], headx: number, heady: number) => {
for (let k = snake.length - 1; k > 0; k--) {
snake[k].x = snake[k - 1].x;
snake[k].y = snake[k - 1].y;
}
snake[0].x = headx;
snake[0].y = heady;
};
export const step = (grid: Grid, stack: Color[], snake: Snake) => {
const x = getHeadX(snake);
const y = getHeadY(snake);
const color = getColor(grid, x, y);
export const stepSnake = (
snake: Point[],
direction: Point,
options: { maxSnakeLength: number }
) => {
const headx = snake[0].x + direction.x;
const heady = snake[0].y + direction.y;
if (snake.length === options.maxSnakeLength) {
moveSnake(snake, headx, heady);
} else {
snake.unshift({ x: headx, y: heady });
if (isInside(grid, x, y) && !isEmpty(color)) {
stack.push(color);
setColorEmpty(grid, x, y);
}
};
export const stepPicking = (grid: Grid, snake: Point[], stack: Color[]) => {
if (isInside(grid, snake[0].x, snake[0].y)) {
const c = getColor(grid, snake[0].x, snake[0].y);
if (c) {
setColor(grid, snake[0].x, snake[0].y, null);
stack.push(c);
}
}
};
export const step = (
grid: Grid,
snake: Point[],
stack: Color[],
direction: Point,
options: { maxSnakeLength: number }
) => {
stepSnake(snake, direction, options);
stepPicking(grid, snake, stack);
};