🚀 refactor demo
This commit is contained in:
@@ -1,14 +1,15 @@
|
||||
import { getGithubUserContribution, Cell } from "@snk/github-user-contribution";
|
||||
import { setColor, createEmptyGrid } from "@snk/compute/grid";
|
||||
import { setColor, createEmptyGrid, Color } from "@snk/compute/grid";
|
||||
import { createGif } from "@snk/gif-creator";
|
||||
import { getBestRoute } from "@snk/compute/getBestRoute";
|
||||
import { createSnake } from "@snk/compute/snake";
|
||||
|
||||
export const userContributionToGrid = (cells: Cell[]) => {
|
||||
const width = Math.max(0, ...cells.map((c) => c.x)) + 1;
|
||||
const height = Math.max(0, ...cells.map((c) => c.y)) + 1;
|
||||
|
||||
const grid = createEmptyGrid(width, height);
|
||||
for (const c of cells) setColor(grid, c.x, c.y, c.k === 0 ? null : c.k);
|
||||
for (const c of cells) if (c.k) setColor(grid, c.x, c.y, c.k as Color);
|
||||
|
||||
return grid;
|
||||
};
|
||||
@@ -18,13 +19,13 @@ export const generateContributionSnake = async (userName: string) => {
|
||||
|
||||
const grid0 = userContributionToGrid(cells);
|
||||
|
||||
const snake0 = [
|
||||
const snake0 = createSnake([
|
||||
{ x: 4, y: -1 },
|
||||
{ x: 3, y: -1 },
|
||||
{ x: 2, y: -1 },
|
||||
{ x: 1, y: -1 },
|
||||
{ x: 0, y: -1 },
|
||||
];
|
||||
]);
|
||||
|
||||
const drawOptions = {
|
||||
sizeBorderRadius: 2,
|
||||
@@ -36,23 +37,11 @@ export const generateContributionSnake = async (userName: string) => {
|
||||
colorSnake: "purple",
|
||||
};
|
||||
|
||||
const gameOptions = {
|
||||
maxSnakeLength: 5,
|
||||
colors: Array.from({ length: colorScheme.length - 1 }, (_, i) => i + 1),
|
||||
};
|
||||
|
||||
const gifOptions = { delay: 3 };
|
||||
|
||||
const commands = getBestRoute(grid0, snake0, gameOptions, 600);
|
||||
const chain = getBestRoute(grid0, snake0)!;
|
||||
|
||||
const buffer = await createGif(
|
||||
grid0,
|
||||
snake0,
|
||||
commands,
|
||||
drawOptions,
|
||||
gameOptions,
|
||||
gifOptions
|
||||
);
|
||||
const buffer = await createGif(grid0, chain, drawOptions, gifOptions);
|
||||
|
||||
return buffer;
|
||||
};
|
||||
|
||||
33
packages/compute/__fixtures__/grid.ts
Normal file
33
packages/compute/__fixtures__/grid.ts
Normal 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);
|
||||
9
packages/compute/__fixtures__/snake.ts
Normal file
9
packages/compute/__fixtures__/snake.ts
Normal 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);
|
||||
19
packages/compute/__tests__/getBestRoute.spec.ts
Normal file
19
packages/compute/__tests__/getBestRoute.spec.ts
Normal 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 });
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -42,3 +42,5 @@ export const createSnake = (points: Point[]) => {
|
||||
}
|
||||
return snake as Snake;
|
||||
};
|
||||
|
||||
export const copySnake = (snake: Snake) => snake.slice() as Snake;
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -1,33 +1,19 @@
|
||||
import { createCanvas } from "./canvas";
|
||||
import { samples } from "./samples";
|
||||
import { getInterestingAvailableRoutes } from "@snk/compute/getAvailableRoutes";
|
||||
import { createSnake, Snake, snakeToCells } from "@snk/compute/snake";
|
||||
import { Snake, snakeToCells } from "@snk/compute/snake";
|
||||
import { GUI } from "dat.gui";
|
||||
import { Point } from "@snk/compute/point";
|
||||
|
||||
//
|
||||
// init
|
||||
|
||||
const label = new URLSearchParams(window.location.search).get("sample");
|
||||
const { grid: grid0 } = samples.find((s) => s.label === label) || samples[0];
|
||||
import { grid, snake } from "./sample";
|
||||
import { getAvailableInterestingRoutes } from "@snk/compute/getAvailableRoutes";
|
||||
import type { Point } from "@snk/compute/point";
|
||||
|
||||
//
|
||||
// compute
|
||||
|
||||
const snake0 = createSnake([
|
||||
//
|
||||
{ x: -1, y: -1 },
|
||||
{ x: -1, y: 0 },
|
||||
{ x: -1, y: 1 },
|
||||
{ x: -1, y: 2 },
|
||||
{ x: -1, y: 3 },
|
||||
]);
|
||||
const routes: Snake[][] = [];
|
||||
getInterestingAvailableRoutes(
|
||||
grid0,
|
||||
snake0,
|
||||
(snakes) => {
|
||||
routes.push(snakes);
|
||||
getAvailableInterestingRoutes(
|
||||
grid,
|
||||
snake,
|
||||
(chain) => {
|
||||
routes.push(chain);
|
||||
return routes.length > 10;
|
||||
},
|
||||
2
|
||||
@@ -38,10 +24,10 @@ const config = { routeN: 0, routeK: 0 };
|
||||
//
|
||||
// draw
|
||||
|
||||
const { canvas, ctx, draw } = createCanvas(grid0);
|
||||
const { canvas, ctx, draw } = createCanvas(grid);
|
||||
document.body.appendChild(canvas);
|
||||
|
||||
draw(grid0, snake0, []);
|
||||
draw(grid, snake, []);
|
||||
|
||||
let cancel: number;
|
||||
|
||||
@@ -53,9 +39,9 @@ const onChange = () => {
|
||||
cancelAnimationFrame(cancel);
|
||||
cancel = requestAnimationFrame(onChange);
|
||||
|
||||
const chain = routes[config.routeN] || [snake0];
|
||||
const chain = routes[config.routeN] || [snake];
|
||||
|
||||
draw(grid0, chain[mod(-t, chain.length)], []);
|
||||
draw(grid, chain[mod(-t, chain.length)], []);
|
||||
|
||||
const cells: Point[] = [];
|
||||
chain.forEach((s) => cells.push(...snakeToCells(s)));
|
||||
|
||||
@@ -1,48 +1,45 @@
|
||||
import { copyGrid } from "@snk/compute/grid";
|
||||
import { copySnake } from "@snk/compute/snake";
|
||||
import { createCanvas } from "./canvas";
|
||||
import { getBestRoute } from "../compute/getBestRoute";
|
||||
import { Color, copyGrid } from "../compute/grid";
|
||||
import { grid, snake } from "./sample";
|
||||
import { step } from "@snk/compute/step";
|
||||
import { getBestRoute } from "@snk/compute/getBestRoute";
|
||||
import { samples } from "./samples";
|
||||
|
||||
//
|
||||
// init
|
||||
|
||||
const label = new URLSearchParams(window.location.search).get("sample");
|
||||
const { grid: grid0, snake: snake0, gameOptions } =
|
||||
samples.find((s) => s.label === label) || samples[0];
|
||||
|
||||
//
|
||||
// compute
|
||||
|
||||
const s0 = Date.now();
|
||||
const bestRoute = getBestRoute(grid0, snake0, gameOptions);
|
||||
console.log(`computed in ${Date.now() - s0}ms`);
|
||||
const chain = [snake, ...getBestRoute(grid, snake)!];
|
||||
|
||||
//
|
||||
// draw
|
||||
|
||||
const { draw } = createCanvas(grid0);
|
||||
let k = 0;
|
||||
|
||||
//
|
||||
// controls
|
||||
const { canvas, draw } = createCanvas(grid);
|
||||
document.body.appendChild(canvas);
|
||||
|
||||
const inputK: any = document.createElement("input");
|
||||
inputK.type = "range";
|
||||
inputK.style.width = "100%";
|
||||
inputK.min = 0;
|
||||
inputK.max = bestRoute.length;
|
||||
inputK.step = 1;
|
||||
inputK.value = 0;
|
||||
inputK.addEventListener("input", () => {
|
||||
const snake = copySnake(snake0);
|
||||
const grid = copyGrid(grid0);
|
||||
const stack: any[] = [];
|
||||
const onChange = () => {
|
||||
debugger;
|
||||
|
||||
for (let i = 0; i < +inputK.value; i++)
|
||||
step(grid, snake, stack, bestRoute[i], gameOptions);
|
||||
const grid0 = copyGrid(grid);
|
||||
const stack0: Color[] = [];
|
||||
let snake0 = snake;
|
||||
chain.slice(0, k).forEach((s) => {
|
||||
snake0 = s;
|
||||
step(grid0, stack0, snake0);
|
||||
});
|
||||
|
||||
draw(grid, snake, stack);
|
||||
draw(grid0, snake0, stack0);
|
||||
};
|
||||
|
||||
onChange();
|
||||
|
||||
const input = document.createElement("input") as any;
|
||||
input.type = "range";
|
||||
input.value = 0;
|
||||
input.step = 1;
|
||||
input.min = 0;
|
||||
input.max = chain.length;
|
||||
input.style.width = "90%";
|
||||
input.addEventListener("input", () => {
|
||||
k = +input.value;
|
||||
onChange();
|
||||
});
|
||||
document.body.appendChild(inputK);
|
||||
draw(grid0, snake0, []);
|
||||
document.body.append(input);
|
||||
document.body.addEventListener("click", () => input.focus());
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
import { copyGrid } from "@snk/compute/grid";
|
||||
import { copySnake } from "@snk/compute/snake";
|
||||
import { createCanvas } from "./canvas";
|
||||
import { step } from "@snk/compute/step";
|
||||
import { getBestRoute } from "@snk/compute/getBestRoute";
|
||||
import { samples } from "./samples";
|
||||
|
||||
//
|
||||
// init
|
||||
|
||||
const label = "realistic";
|
||||
const { grid: grid0, snake: snake0, gameOptions } = samples.find(
|
||||
(s) => s.label === label
|
||||
);
|
||||
|
||||
//
|
||||
// compute
|
||||
|
||||
const s0 = performance.now();
|
||||
const bestRoute = getBestRoute(grid0, snake0, gameOptions, 120);
|
||||
console.log(performance.now() - s0);
|
||||
//
|
||||
// draw
|
||||
|
||||
const { draw } = createCanvas(grid0);
|
||||
|
||||
//
|
||||
// controls
|
||||
|
||||
const inputK: any = document.createElement("input");
|
||||
inputK.type = "range";
|
||||
inputK.style.width = "100%";
|
||||
inputK.min = 0;
|
||||
inputK.max = bestRoute.length;
|
||||
inputK.step = 1;
|
||||
inputK.value = 0;
|
||||
inputK.addEventListener("input", () => {
|
||||
const snake = copySnake(snake0);
|
||||
const grid = copyGrid(grid0);
|
||||
const stack: any[] = [];
|
||||
|
||||
for (let i = 0; i < +inputK.value; i++)
|
||||
step(grid, snake, stack, bestRoute[i], gameOptions);
|
||||
|
||||
draw(grid, snake, stack);
|
||||
});
|
||||
document.body.appendChild(inputK);
|
||||
draw(grid0, snake0, []);
|
||||
14
packages/demo/sample.ts
Normal file
14
packages/demo/sample.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { Grid } from "@snk/compute/grid";
|
||||
import { Snake } from "@snk/compute/snake";
|
||||
import * as grids from "@snk/compute/__fixtures__/grid";
|
||||
import * as snakes from "@snk/compute/__fixtures__/snake";
|
||||
|
||||
const sp = new URLSearchParams(window.location.search);
|
||||
|
||||
const gLabel = sp.get("grid") || "simple";
|
||||
const sLabel = sp.get("snake") || "snake3";
|
||||
|
||||
//@ts-ignore
|
||||
export const grid: Grid = grids[gLabel] || grids.simple;
|
||||
//@ts-ignore
|
||||
export const snake: Snake = snakes[sLabel] || snakes.snake3;
|
||||
@@ -1,85 +0,0 @@
|
||||
// @ts-ignore
|
||||
import * as ParkMiller from "park-miller";
|
||||
import { generateRandomGrid } from "@snk/compute/generateGrid";
|
||||
import { createEmptyGrid, setColor } from "@snk/compute/grid";
|
||||
|
||||
export const samples: any[] = [];
|
||||
|
||||
{
|
||||
const gameOptions = {
|
||||
colors: [1, 2, 3],
|
||||
maxSnakeLength: 1,
|
||||
};
|
||||
const snake = [{ x: 0, y: -1 }];
|
||||
const grid = createEmptyGrid(6, 6);
|
||||
samples.push({
|
||||
label: "empty",
|
||||
grid,
|
||||
snake,
|
||||
gameOptions,
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
const gameOptions = {
|
||||
colors: [1, 2, 3],
|
||||
maxSnakeLength: 1,
|
||||
};
|
||||
const snake = [{ x: 0, y: -1 }];
|
||||
const grid = createEmptyGrid(6, 6);
|
||||
setColor(grid, 2, 2, 2);
|
||||
samples.push({
|
||||
label: "small",
|
||||
grid,
|
||||
snake,
|
||||
gameOptions,
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
const gameOptions = {
|
||||
colors: [1, 2, 3],
|
||||
maxSnakeLength: 5,
|
||||
};
|
||||
const random = new ParkMiller(10);
|
||||
const rand = (a: number, b: number) => random.integerInRange(a, b - 1);
|
||||
const grid = generateRandomGrid(52, 7, { ...gameOptions, emptyP: 2 }, rand);
|
||||
const snake = [
|
||||
{ x: 4, y: -1 },
|
||||
{ x: 3, y: -1 },
|
||||
{ x: 2, y: -1 },
|
||||
{ x: 1, y: -1 },
|
||||
{ x: 0, y: -1 },
|
||||
];
|
||||
|
||||
samples.push({
|
||||
label: "realistic",
|
||||
grid,
|
||||
snake,
|
||||
gameOptions,
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
const gameOptions = {
|
||||
colors: [1, 2, 3],
|
||||
maxSnakeLength: 5,
|
||||
};
|
||||
const random = new ParkMiller(10);
|
||||
const rand = (a: number, b: number) => random.integerInRange(a, b - 1);
|
||||
const grid = generateRandomGrid(20, 7, { ...gameOptions, emptyP: 2 }, rand);
|
||||
const snake = [
|
||||
{ x: 4, y: -1 },
|
||||
{ x: 3, y: -1 },
|
||||
{ x: 2, y: -1 },
|
||||
{ x: 1, y: -1 },
|
||||
{ x: 0, y: -1 },
|
||||
];
|
||||
|
||||
samples.push({
|
||||
label: "realistic-small",
|
||||
grid,
|
||||
snake,
|
||||
gameOptions,
|
||||
});
|
||||
}
|
||||
@@ -13,7 +13,6 @@ const config: Configuration = {
|
||||
entry: {
|
||||
"demo.getAvailableRoutes": "./demo.getAvailableRoutes",
|
||||
"demo.getBestRoute": "./demo.getBestRoute",
|
||||
"demo.index": "./demo.index",
|
||||
},
|
||||
resolve: { extensions: [".ts", ".js"] },
|
||||
output: {
|
||||
@@ -27,14 +26,16 @@ const config: Configuration = {
|
||||
exclude: /node_modules/,
|
||||
test: /\.(js|ts)$/,
|
||||
loader: "ts-loader",
|
||||
options: {
|
||||
compilerOptions: {
|
||||
lib: ["dom", "ES2020"],
|
||||
target: "ES2020",
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
plugins: [
|
||||
new HtmlWebpackPlugin({
|
||||
filename: "index.html",
|
||||
chunks: ["demo.index"],
|
||||
}),
|
||||
new HtmlWebpackPlugin({
|
||||
filename: "demo-getAvailableRoutes.html",
|
||||
chunks: ["demo.getAvailableRoutes"],
|
||||
|
||||
@@ -69,6 +69,8 @@ export const drawCircleStack = (
|
||||
x * o.sizeCell + (o.sizeCell - o.sizeDot) / 2,
|
||||
y * o.sizeCell + (o.sizeCell - o.sizeDot) / 2
|
||||
);
|
||||
|
||||
//@ts-ignore
|
||||
ctx.fillStyle = o.colorDots[stack[i]];
|
||||
ctx.strokeStyle = o.colorBorder;
|
||||
ctx.lineWidth = 1;
|
||||
|
||||
@@ -33,6 +33,7 @@ export const drawWorld = (
|
||||
ctx.save();
|
||||
ctx.translate(o.sizeCell, (grid.height + 4) * o.sizeCell);
|
||||
for (let i = 0; i < stack.length; i++) {
|
||||
// @ts-ignore
|
||||
ctx.fillStyle = o.colorDots[stack[i]];
|
||||
ctx.fillRect(i * m, 0, m, 10);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
import { createGif } from "..";
|
||||
import { generateRandomGrid } from "@snk/compute/generateGrid";
|
||||
import { getBestRoute } from "@snk/compute/getBestRoute";
|
||||
import * as grids from "@snk/compute/__fixtures__/grid";
|
||||
import { snake3 as snake } from "@snk/compute/__fixtures__/snake";
|
||||
|
||||
const drawOptions = {
|
||||
sizeBorderRadius: 2,
|
||||
@@ -12,31 +15,29 @@ const drawOptions = {
|
||||
colorSnake: "purple",
|
||||
};
|
||||
|
||||
const gameOptions = { maxSnakeLength: 5, colors: [1, 2, 3, 4] };
|
||||
const gifOptions = { delay: 18 };
|
||||
|
||||
const gifOptions = { delay: 200 };
|
||||
const dir = path.resolve(__dirname, "__snapshots__");
|
||||
|
||||
it("should generate gif", async () => {
|
||||
const grid = generateRandomGrid(7, 7, { ...gameOptions, emptyP: 3 });
|
||||
try {
|
||||
fs.mkdirSync(dir);
|
||||
} catch (err) {}
|
||||
|
||||
const snake = [
|
||||
{ x: 4, y: -1 },
|
||||
{ x: 3, y: -1 },
|
||||
{ x: 2, y: -1 },
|
||||
{ x: 1, y: -1 },
|
||||
{ x: 0, y: -1 },
|
||||
];
|
||||
for (const key of [
|
||||
"empty",
|
||||
"simple",
|
||||
"corner",
|
||||
"small",
|
||||
"smallPacked",
|
||||
] as const)
|
||||
it(`should generate ${key} gif`, async () => {
|
||||
const grid = grids[key];
|
||||
|
||||
const commands = getBestRoute(grid, snake, gameOptions, 50).slice(0, 9);
|
||||
const chain = [snake, ...getBestRoute(grid, snake)!];
|
||||
|
||||
const gif = await createGif(
|
||||
grid,
|
||||
snake,
|
||||
commands,
|
||||
drawOptions,
|
||||
gameOptions,
|
||||
gifOptions
|
||||
);
|
||||
const gif = await createGif(grid, chain, drawOptions, gifOptions);
|
||||
|
||||
expect(gif).toBeDefined();
|
||||
});
|
||||
expect(gif).toBeDefined();
|
||||
|
||||
fs.writeFileSync(path.resolve(dir, key + ".gif"), gif);
|
||||
});
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
import { createGif } from "..";
|
||||
import { generateRandomGrid } from "@snk/compute/generateGrid";
|
||||
import { getBestRoute } from "@snk/compute/getBestRoute";
|
||||
|
||||
const drawOptions = {
|
||||
sizeBorderRadius: 2,
|
||||
sizeCell: 16,
|
||||
sizeDot: 12,
|
||||
colorBorder: "#1b1f230a",
|
||||
colorDots: { 1: "#9be9a8", 2: "#40c463", 3: "#30a14e", 4: "#216e39" },
|
||||
colorEmpty: "#ebedf0",
|
||||
colorSnake: "purple",
|
||||
};
|
||||
|
||||
const gameOptions = { maxSnakeLength: 5, colors: [1, 2, 3, 4] };
|
||||
|
||||
const gifOptions = { delay: 20 };
|
||||
|
||||
const grid = generateRandomGrid(14, 7, { ...gameOptions, emptyP: 3 });
|
||||
|
||||
const snake = [
|
||||
{ x: 4, y: -1 },
|
||||
{ x: 3, y: -1 },
|
||||
{ x: 2, y: -1 },
|
||||
{ x: 1, y: -1 },
|
||||
{ x: 0, y: -1 },
|
||||
];
|
||||
|
||||
const commands = getBestRoute(grid, snake, gameOptions, 50);
|
||||
|
||||
createGif(grid, snake, commands, drawOptions, gameOptions, gifOptions).then(
|
||||
(buffer) => {
|
||||
process.stdout.write(buffer);
|
||||
}
|
||||
);
|
||||
@@ -2,8 +2,7 @@ import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
import { createCanvas } from "canvas";
|
||||
import { Grid, copyGrid, Color } from "@snk/compute/grid";
|
||||
import { Point } from "@snk/compute/point";
|
||||
import { copySnake } from "@snk/compute/snake";
|
||||
import { Snake } from "@snk/compute/snake";
|
||||
import { drawWorld } from "@snk/draw/drawWorld";
|
||||
import { step } from "@snk/compute/step";
|
||||
import * as tmp from "tmp";
|
||||
@@ -11,17 +10,15 @@ import * as execa from "execa";
|
||||
|
||||
export const createGif = async (
|
||||
grid0: Grid,
|
||||
snake0: Point[],
|
||||
commands: Point[],
|
||||
chain: Snake[],
|
||||
drawOptions: Parameters<typeof drawWorld>[4],
|
||||
gameOptions: Parameters<typeof step>[4],
|
||||
gifOptions: { delay: number }
|
||||
) => {
|
||||
let snake = chain[0];
|
||||
const grid = copyGrid(grid0);
|
||||
const snake = copySnake(snake0);
|
||||
const stack: Color[] = [];
|
||||
|
||||
const width = drawOptions.sizeCell * (grid.width + 4);
|
||||
const width = drawOptions.sizeCell * (grid.width + 2);
|
||||
const height = drawOptions.sizeCell * (grid.height + 4) + 100;
|
||||
|
||||
const { name: dir, removeCallback: cleanUp } = tmp.dirSync({
|
||||
@@ -48,11 +45,11 @@ export const createGif = async (
|
||||
};
|
||||
|
||||
try {
|
||||
writeImage(0);
|
||||
for (let i = 0; i < chain.length; i++) {
|
||||
snake = chain[i];
|
||||
|
||||
for (let i = 0; i < commands.length; i++) {
|
||||
step(grid, snake, stack, commands[i], gameOptions);
|
||||
writeImage(i + 1);
|
||||
step(grid, stack, snake);
|
||||
writeImage(i);
|
||||
}
|
||||
|
||||
const outFileName = path.join(dir, "out.gif");
|
||||
|
||||
@@ -10,10 +10,6 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/execa": "2.0.0",
|
||||
"@types/tmp": "0.2.0",
|
||||
"@zeit/ncc": "0.22.3"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "ncc run __tests__/dev.ts --quiet | tail -n +2 > out.gif "
|
||||
"@types/tmp": "0.2.0"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user