Compare commits
25 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1e1967ef61 | ||
|
|
3d16c675bd | ||
|
|
8f5c1969a6 | ||
|
|
fe821f6251 | ||
|
|
d7423423f8 | ||
|
|
03396bae31 | ||
|
|
b63a1191b4 | ||
|
|
a9555b092a | ||
|
|
1f9dda0ca6 | ||
|
|
202bd7cacb | ||
|
|
bc18120a98 | ||
|
|
bb0750e8ba | ||
|
|
16a47349be | ||
|
|
b0784fbaca | ||
|
|
d5bdc84680 | ||
|
|
9c758febe7 | ||
|
|
2125640716 | ||
|
|
64f0b872aa | ||
|
|
9b92697ef9 | ||
|
|
8d8956229c | ||
|
|
2499529b1d | ||
|
|
3625bdb819 | ||
|
|
9ab55aaad6 | ||
|
|
48d89528d5 | ||
|
|
e637604df1 |
29
.github/workflows/deploy.yml
vendored
29
.github/workflows/deploy.yml
vendored
@@ -1,29 +0,0 @@
|
||||
name: deploy
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
deploy-demo:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- uses: actions/setup-node@v1.4.2
|
||||
with:
|
||||
node-version: 14
|
||||
|
||||
- uses: bahmutov/npm-install@v1.4.1
|
||||
|
||||
- run: yarn build:demo
|
||||
env:
|
||||
BASE_PATHNAME: "snk"
|
||||
|
||||
- uses: crazy-max/ghaction-github-pages@v2.1.1
|
||||
with:
|
||||
target_branch: gh-pages
|
||||
build_dir: packages/demo/dist
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.MY_GITHUB_TOKEN_GH_PAGES }}
|
||||
52
.github/workflows/main.yml
vendored
52
.github/workflows/main.yml
vendored
@@ -1,6 +1,6 @@
|
||||
name: main
|
||||
|
||||
on: [push, pull_request]
|
||||
on: [push]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
@@ -8,18 +8,33 @@ jobs:
|
||||
|
||||
steps:
|
||||
- run: sudo apt-get install gifsicle graphicsmagick
|
||||
- uses: actions/checkout@v1
|
||||
- uses: actions/setup-node@v1.4.2
|
||||
- uses: actions/checkout@v2.3.3
|
||||
- uses: actions/setup-node@v1.4.4
|
||||
with:
|
||||
node-version: 14
|
||||
|
||||
- uses: bahmutov/npm-install@v1.4.1
|
||||
- uses: bahmutov/npm-install@v1.4.3
|
||||
|
||||
- run: yarn type
|
||||
- run: yarn lint
|
||||
- run: yarn test --ci
|
||||
- run: yarn build:action
|
||||
|
||||
test-benchmark:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- run: sudo apt-get install gifsicle graphicsmagick
|
||||
- uses: actions/checkout@v2.3.3
|
||||
- uses: actions/setup-node@v1.4.4
|
||||
with:
|
||||
node-version: 14
|
||||
|
||||
- uses: bahmutov/npm-install@v1.4.3
|
||||
|
||||
- run: ( cd packages/compute ; yarn benchmark )
|
||||
- run: ( cd packages/gif-creator ; yarn benchmark )
|
||||
|
||||
test-action:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
@@ -38,9 +53,36 @@ jobs:
|
||||
ls -l ${{ steps.snake-gif.outputs.gif_out_path }}
|
||||
test -f ${{ steps.snake-gif.outputs.gif_out_path }}
|
||||
|
||||
- uses: crazy-max/ghaction-github-pages@v2.1.1
|
||||
- uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: output
|
||||
path: ${{ steps.snake-gif.outputs.gif_out_path }}
|
||||
|
||||
- uses: crazy-max/ghaction-github-pages@v2.1.3
|
||||
with:
|
||||
target_branch: output
|
||||
build_dir: dist
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.MY_GITHUB_TOKEN_GH_PAGES }}
|
||||
|
||||
deploy-ghpages:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2.3.3
|
||||
- uses: actions/setup-node@v1.4.4
|
||||
with:
|
||||
node-version: 14
|
||||
|
||||
- uses: bahmutov/npm-install@v1.4.3
|
||||
|
||||
- run: yarn build:demo
|
||||
env:
|
||||
BASE_PATHNAME: "snk"
|
||||
|
||||
- uses: crazy-max/ghaction-github-pages@v2.1.3
|
||||
if: success() && github.ref == 'refs/heads/master'
|
||||
with:
|
||||
target_branch: gh-pages
|
||||
build_dir: packages/demo/dist
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.MY_GITHUB_TOKEN_GH_PAGES }}
|
||||
|
||||
12
package.json
12
package.json
@@ -5,11 +5,12 @@
|
||||
"private": true,
|
||||
"repository": "github:platane/snk",
|
||||
"devDependencies": {
|
||||
"@types/jest": "26.0.4",
|
||||
"jest": "26.1.0",
|
||||
"prettier": "2.0.5",
|
||||
"ts-jest": "26.1.2",
|
||||
"typescript": "3.9.6"
|
||||
"@types/jest": "26.0.14",
|
||||
"@types/node": "14.11.8",
|
||||
"jest": "26.5.2",
|
||||
"prettier": "2.1.2",
|
||||
"ts-jest": "26.4.1",
|
||||
"typescript": "4.0.3"
|
||||
},
|
||||
"workspaces": [
|
||||
"packages/**"
|
||||
@@ -18,6 +19,7 @@
|
||||
"type": "tsc --noEmit",
|
||||
"lint": "yarn prettier -c '**/*.{ts,js,json,md,yml,yaml}' '!packages/action/dist/**' '!packages/demo/dist/**' '!packages/demo/webpack.config.js'",
|
||||
"test": "jest --verbose --passWithNoTests --no-cache",
|
||||
"dev:demo": "( cd packages/demo ; yarn dev )",
|
||||
"build:demo": "( cd packages/demo ; yarn build )",
|
||||
"build:action": "( cd packages/action ; yarn build )"
|
||||
}
|
||||
|
||||
@@ -1,31 +1,21 @@
|
||||
import { getGithubUserContribution, Cell } from "@snk/github-user-contribution";
|
||||
import { generateEmptyGrid } from "@snk/compute/generateGrid";
|
||||
import { setColor } from "@snk/compute/grid";
|
||||
import { computeBestRun } from "@snk/compute";
|
||||
import { createGif } from "../gif-creator";
|
||||
|
||||
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 = generateEmptyGrid(width, height);
|
||||
for (const c of cells) setColor(grid, c.x, c.y, c.k === 0 ? null : c.k);
|
||||
|
||||
return grid;
|
||||
};
|
||||
import { getGithubUserContribution } from "@snk/github-user-contribution";
|
||||
import { createGif } from "@snk/gif-creator";
|
||||
import { createSnake } from "@snk/compute/snake";
|
||||
import { getBestRoute } from "@snk/compute/getBestRoute";
|
||||
import { userContributionToGrid } from "./userContributionToGrid";
|
||||
|
||||
export const generateContributionSnake = async (userName: string) => {
|
||||
const { cells, colorScheme } = await getGithubUserContribution(userName);
|
||||
|
||||
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,
|
||||
@@ -37,23 +27,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 = { frameDuration: 100, step: 2 };
|
||||
|
||||
const gifOptions = { delay: 3 };
|
||||
const chain = getBestRoute(grid0, snake0)!;
|
||||
|
||||
const commands = computeBestRun(grid0, snake0, gameOptions);
|
||||
|
||||
const buffer = await createGif(
|
||||
grid0,
|
||||
snake0,
|
||||
commands,
|
||||
drawOptions,
|
||||
gameOptions,
|
||||
gifOptions
|
||||
);
|
||||
const buffer = await createGif(grid0, chain, drawOptions, gifOptions);
|
||||
|
||||
return buffer;
|
||||
};
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "@snk/action",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"@actions/core": "1.2.4",
|
||||
"@actions/core": "1.2.6",
|
||||
"@snk/gif-creator": "1.0.0",
|
||||
"@snk/github-user-contribution": "1.0.0"
|
||||
},
|
||||
|
||||
13
packages/action/userContributionToGrid.ts
Normal file
13
packages/action/userContributionToGrid.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { setColor, createEmptyGrid } from "@snk/compute/grid";
|
||||
import type { Cell } from "@snk/github-user-contribution";
|
||||
import type { Color } from "@snk/compute/grid";
|
||||
|
||||
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) if (c.k) setColor(grid, c.x, c.y, c.k as Color);
|
||||
|
||||
return grid;
|
||||
};
|
||||
54
packages/compute/__fixtures__/grid.ts
Normal file
54
packages/compute/__fixtures__/grid.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
// @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);
|
||||
|
||||
// enclaved color
|
||||
export const enclave = createEmptyGrid(7, 7);
|
||||
setColor(enclave, 3, 4, 2 as Color);
|
||||
setColor(enclave, 2, 3, 2 as Color);
|
||||
setColor(enclave, 2, 4, 2 as Color);
|
||||
setColor(enclave, 4, 4, 2 as Color);
|
||||
setColor(enclave, 4, 3, 2 as Color);
|
||||
setColor(enclave, 3, 3, 1 as Color);
|
||||
setColor(enclave, 5, 5, 1 as Color);
|
||||
|
||||
// enclaved color
|
||||
export const enclaveBorder = createEmptyGrid(7, 7);
|
||||
setColor(enclaveBorder, 1, 0, 3 as Color);
|
||||
setColor(enclaveBorder, 2, 1, 3 as Color);
|
||||
setColor(enclaveBorder, 3, 0, 3 as Color);
|
||||
setColor(enclaveBorder, 2, 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);
|
||||
|
||||
// small realistic
|
||||
export const realistic = create(52, 7, 3);
|
||||
export const realisticFull = create(52, 7, 0);
|
||||
10
packages/compute/__fixtures__/snake.ts
Normal file
10
packages/compute/__fixtures__/snake.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
// @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 snake5 = create(5);
|
||||
export const snake9 = create(9);
|
||||
29
packages/compute/__tests__/benchmark.ts
Normal file
29
packages/compute/__tests__/benchmark.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { realistic as grid } from "../__fixtures__/grid";
|
||||
import { snake3 } from "../__fixtures__/snake";
|
||||
import { performance } from "perf_hooks";
|
||||
import { getAvailableRoutes } from "../getAvailableRoutes";
|
||||
import { getBestRoute } from "../getBestRoute";
|
||||
|
||||
{
|
||||
const m = 100;
|
||||
const s = performance.now();
|
||||
for (let k = m; k--; ) {
|
||||
const solutions = [];
|
||||
|
||||
getAvailableRoutes(grid, snake3, (snakes) => {
|
||||
solutions.push(snakes);
|
||||
return false;
|
||||
});
|
||||
}
|
||||
console.log("getAvailableRoutes", (performance.now() - s) / m, "ms");
|
||||
}
|
||||
|
||||
{
|
||||
const m = 10;
|
||||
const s = performance.now();
|
||||
for (let k = m; k--; ) {
|
||||
getBestRoute(grid, snake3);
|
||||
}
|
||||
|
||||
console.log("getBestRoute", (performance.now() - s) / m, "ms");
|
||||
}
|
||||
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,12 +1,11 @@
|
||||
import { generateEmptyGrid } from "../generateGrid";
|
||||
import { setColor, getColor, isInside } from "../grid";
|
||||
import { createEmptyGrid, setColor, getColor, isInside, Color } from "../grid";
|
||||
|
||||
it("should set / get cell", () => {
|
||||
const grid = generateEmptyGrid(2, 3);
|
||||
const grid = createEmptyGrid(2, 3);
|
||||
|
||||
expect(getColor(grid, 0, 1)).toBe(null);
|
||||
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);
|
||||
});
|
||||
@@ -20,7 +19,7 @@ test.each([
|
||||
[2, 1, false],
|
||||
[0, 3, false],
|
||||
])("isInside", (x, y, output) => {
|
||||
const grid = generateEmptyGrid(2, 3);
|
||||
const grid = createEmptyGrid(2, 3);
|
||||
|
||||
expect(isInside(grid, x, y)).toBe(output);
|
||||
});
|
||||
|
||||
@@ -1,24 +1,43 @@
|
||||
import { snakeSelfCollide } 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);
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
|
||||
86
packages/compute/__tests__/sortPush.spec.ts
Normal file
86
packages/compute/__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);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1,94 +0,0 @@
|
||||
import { step } from "../step";
|
||||
import { generateEmptyGrid } from "../generateGrid";
|
||||
import { around4 } from "../point";
|
||||
import { setColor, getColor } from "../grid";
|
||||
|
||||
it("should move snake", () => {
|
||||
const grid = generateEmptyGrid(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 = generateEmptyGrid(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 = generateEmptyGrid(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]);
|
||||
});
|
||||
132
packages/compute/cleanLayer.ts
Normal file
132
packages/compute/cleanLayer.ts
Normal file
@@ -0,0 +1,132 @@
|
||||
import { copyGrid, isEmpty, setColorEmpty } from "./grid";
|
||||
import { getHeadX, getHeadY, snakeEquals } from "./snake";
|
||||
import { sortPush } from "./utils/sortPush";
|
||||
import { arrayEquals } from "./utils/array";
|
||||
import { getAvailableRoutes } from "./getAvailableRoutes";
|
||||
import type { Snake } from "./snake";
|
||||
import type { Grid } from "./grid";
|
||||
import type { Point } from "./point";
|
||||
|
||||
type M = {
|
||||
snake: Snake;
|
||||
chain: Snake[];
|
||||
chunk: Point[];
|
||||
grid: Grid;
|
||||
parent: M | null;
|
||||
w: number;
|
||||
h: number;
|
||||
f: number;
|
||||
};
|
||||
const unwrap = (o: M | null): Snake[] =>
|
||||
!o ? [] : [...o.chain, ...unwrap(o.parent)];
|
||||
|
||||
const createGetHeuristic = (grid: Grid, chunk0: Point[]) => {
|
||||
const n = grid.data.reduce((sum, x: any) => sum + +!isEmpty(x), 0);
|
||||
const area = grid.width * grid.height;
|
||||
|
||||
const k =
|
||||
Math.sqrt((2 * area) / chunk0.length) * 1 + (n - chunk0.length) / area;
|
||||
|
||||
return (chunk: any[]) => chunk.length * k;
|
||||
};
|
||||
|
||||
export const getAvailableWhiteListedRoutes = (
|
||||
grid: Grid,
|
||||
snake: Snake,
|
||||
whiteList: Point[]
|
||||
) => {
|
||||
let solution: Snake[] | null;
|
||||
|
||||
getAvailableRoutes(grid, snake, (chain) => {
|
||||
const hx = getHeadX(chain[0]);
|
||||
const hy = getHeadY(chain[0]);
|
||||
|
||||
if (!whiteList.some(({ x, y }) => hx === x && hy === y)) return false;
|
||||
|
||||
solution = chain;
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
// @ts-ignore
|
||||
return solution;
|
||||
};
|
||||
|
||||
export const cleanLayer = (grid0: Grid, snake0: Snake, chunk0: Point[]) => {
|
||||
const getH = createGetHeuristic(grid0, chunk0);
|
||||
|
||||
const next = {
|
||||
grid: grid0,
|
||||
snake: snake0,
|
||||
chain: [snake0],
|
||||
chunk: chunk0,
|
||||
parent: null,
|
||||
h: getH(chunk0),
|
||||
f: getH(chunk0),
|
||||
w: 0,
|
||||
};
|
||||
|
||||
const openList: M[] = [next];
|
||||
const closeList: M[] = [next];
|
||||
|
||||
while (openList.length) {
|
||||
const o = openList.shift()!;
|
||||
|
||||
if (o.chunk.length === 0) return unwrap(o).slice(0, -1);
|
||||
|
||||
const chain = getAvailableWhiteListedRoutes(o.grid, o.snake, o.chunk);
|
||||
|
||||
if (chain) {
|
||||
const snake = chain[0];
|
||||
const x = getHeadX(snake);
|
||||
const y = getHeadY(snake);
|
||||
|
||||
const chunk = o.chunk.filter((u) => u.x !== x || u.y !== y);
|
||||
|
||||
if (
|
||||
!closeList.some(
|
||||
(u) => snakeEquals(u.snake, snake) && arrayEquals(u.chunk, chunk)
|
||||
)
|
||||
) {
|
||||
const grid = copyGrid(o.grid);
|
||||
setColorEmpty(grid, x, y);
|
||||
|
||||
const h = getH(chunk);
|
||||
const w = o.w + chain.length;
|
||||
const f = h + w;
|
||||
|
||||
const next = { snake, chain, chunk, grid, parent: o, h, w, f };
|
||||
sortPush(openList, next, (a, b) => a.f - b.f);
|
||||
closeList.push(next);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// export const getAvailableWhiteListedRoutes = (
|
||||
// grid: Grid,
|
||||
// snake: Snake,
|
||||
// whiteList0: Point[],
|
||||
// n = 3
|
||||
// ) => {
|
||||
// const whiteList = whiteList0.slice();
|
||||
// const solutions: Snake[][] = [];
|
||||
|
||||
// getAvailableRoutes(grid, snake, (chain) => {
|
||||
// const hx = getHeadX(chain[0]);
|
||||
// const hy = getHeadY(chain[0]);
|
||||
|
||||
// const i = whiteList.findIndex(({ x, y }) => hx === x && hy === y);
|
||||
|
||||
// if (i >= 0) {
|
||||
// whiteList.splice(i, 1);
|
||||
// solutions.push(chain);
|
||||
|
||||
// if (solutions.length >= n || whiteList.length === 0) return true;
|
||||
// }
|
||||
|
||||
// return false;
|
||||
// });
|
||||
|
||||
// return solutions;
|
||||
// };
|
||||
@@ -1,27 +1,21 @@
|
||||
import { Grid, Color } from "./grid";
|
||||
import { Grid, Color, setColor, setColorEmpty } from "./grid";
|
||||
|
||||
const rand = (a: number, b: number) => Math.floor(Math.random() * (b - a)) + a;
|
||||
const defaultRand = (a: number, b: number) =>
|
||||
Math.floor(Math.random() * (b - a)) + a;
|
||||
|
||||
export const generateEmptyGrid = (width: number, height: number) =>
|
||||
generateRandomGrid(width, height, { colors: [], emptyP: 1 });
|
||||
export const fillRandomGrid = (
|
||||
grid: Grid,
|
||||
{
|
||||
colors = [1, 2, 3] as Color[],
|
||||
emptyP = 2,
|
||||
}: { colors?: Color[]; emptyP?: number } = {},
|
||||
rand = defaultRand
|
||||
) => {
|
||||
for (let x = grid.width; x--; )
|
||||
for (let y = grid.height; y--; ) {
|
||||
const k = rand(-emptyP, colors.length);
|
||||
|
||||
export const generateRandomGrid = (
|
||||
width: number,
|
||||
height: number,
|
||||
options: { colors: Color[]; emptyP: number } = {
|
||||
colors: [1, 2, 3],
|
||||
emptyP: 2,
|
||||
}
|
||||
): Grid => {
|
||||
const g = {
|
||||
width,
|
||||
height,
|
||||
data: Array.from({ length: width * height }, () => {
|
||||
const x = rand(-options.emptyP, options.colors.length);
|
||||
|
||||
return x < 0 ? null : options.colors[x];
|
||||
}),
|
||||
};
|
||||
|
||||
return g;
|
||||
if (k >= 0) setColor(grid, x, y, colors[k]);
|
||||
else setColorEmpty(grid, x, y);
|
||||
}
|
||||
};
|
||||
|
||||
57
packages/compute/getAvailableRoutes.ts
Normal file
57
packages/compute/getAvailableRoutes.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import { isInsideLarge, getColor, isInside, isEmpty } from "./grid";
|
||||
import { around4 } from "./point";
|
||||
import {
|
||||
getHeadX,
|
||||
getHeadY,
|
||||
nextSnake,
|
||||
snakeEquals,
|
||||
snakeWillSelfCollide,
|
||||
} from "./snake";
|
||||
import { sortPush } from "./utils/sortPush";
|
||||
import type { Snake } from "./snake";
|
||||
import type { Grid, Color } from "./grid";
|
||||
|
||||
/**
|
||||
* get routes leading to non-empty cells until onSolution returns true
|
||||
*/
|
||||
export const getAvailableRoutes = (
|
||||
grid: Grid,
|
||||
snake0: Snake,
|
||||
onSolution: (snakes: Snake[], color: Color) => boolean
|
||||
) => {
|
||||
const openList: Snake[][] = [[snake0]];
|
||||
const closeList: Snake[] = [];
|
||||
|
||||
while (openList.length) {
|
||||
const c = openList.shift()!;
|
||||
const [snake] = c;
|
||||
|
||||
const cx = getHeadX(snake);
|
||||
const cy = getHeadY(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 (
|
||||
isInsideLarge(grid, 1, nx, ny) &&
|
||||
!snakeWillSelfCollide(snake, dx, dy)
|
||||
) {
|
||||
const nsnake = nextSnake(snake, dx, dy);
|
||||
|
||||
if (!closeList.some((s) => snakeEquals(nsnake, s))) {
|
||||
const color = isInside(grid, nx, ny) && getColor(grid, nx, ny);
|
||||
|
||||
if (!color || isEmpty(color)) {
|
||||
sortPush(openList, [nsnake, ...c], (a, b) => a.length - b.length);
|
||||
closeList.push(nsnake);
|
||||
} else {
|
||||
if (onSolution([nsnake, ...c.slice(0, -1)], color)) return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
22
packages/compute/getBestRoute.ts
Normal file
22
packages/compute/getBestRoute.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { copyGrid, extractColors } from "./grid";
|
||||
import type { Snake } from "./snake";
|
||||
import type { Grid } from "./grid";
|
||||
import { pruneLayer } from "./pruneLayer";
|
||||
import { cleanLayer } from "./cleanLayer";
|
||||
|
||||
export const getBestRoute = (grid0: Grid, snake0: Snake) => {
|
||||
const grid = copyGrid(grid0);
|
||||
const colors = extractColors(grid0);
|
||||
const snakeN = snake0.length / 2;
|
||||
|
||||
const chain: Snake[] = [snake0];
|
||||
|
||||
for (const color of colors) {
|
||||
const gridN = copyGrid(grid);
|
||||
const chunk = pruneLayer(grid, color, snakeN);
|
||||
const c = cleanLayer(gridN, chain[0], chunk);
|
||||
if (c) chain.unshift(...c);
|
||||
}
|
||||
|
||||
return chain.reverse().slice(1);
|
||||
};
|
||||
@@ -1,9 +1,10 @@
|
||||
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;
|
||||
height: number;
|
||||
data: (Color | null)[];
|
||||
data: Uint8Array;
|
||||
};
|
||||
|
||||
export const getIndex = (grid: Grid, x: number, y: number) =>
|
||||
@@ -15,16 +16,86 @@ 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 copyGrid = (grid: Grid) => ({ ...grid, data: grid.data.slice() });
|
||||
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;
|
||||
grid.data[getIndex(grid, x, y)] = color || 0;
|
||||
};
|
||||
|
||||
export const setColorEmpty = (grid: Grid, x: number, y: number) => {
|
||||
setColor(grid, x, y, 0 as Empty);
|
||||
};
|
||||
|
||||
/**
|
||||
* return true if the grid is empty
|
||||
*/
|
||||
export const isGridEmpty = (grid: Grid) => grid.data.every((x) => x === 0);
|
||||
|
||||
/**
|
||||
* extract colors
|
||||
* return a list of the colors found in the grid
|
||||
*/
|
||||
export const extractColors = (grid: Grid): Color[] => {
|
||||
const colors = new Set<Color>();
|
||||
grid.data.forEach((c: any) => {
|
||||
if (!isEmpty(c)) colors.add(c);
|
||||
});
|
||||
return Array.from(colors.keys()).sort();
|
||||
};
|
||||
|
||||
/**
|
||||
* extract colors count
|
||||
* return a list of the colors and their occurrences found in the grid
|
||||
*/
|
||||
export const extractColorCount = (grid: Grid) => {
|
||||
const colors = new Map<Color, number>();
|
||||
grid.data.forEach((c: any) => {
|
||||
if (!isEmpty(c)) colors.set(c, 1 + (colors.get(c) || 0));
|
||||
});
|
||||
return Array.from(colors.entries()).map(([color, count]) => ({
|
||||
color,
|
||||
count,
|
||||
}));
|
||||
};
|
||||
|
||||
/**
|
||||
* return true if the both are equals
|
||||
*/
|
||||
export const gridEquals = (a: Grid, b: Grid) =>
|
||||
a.data.every((_, i) => a.data[i] === b.data[i]);
|
||||
|
||||
/**
|
||||
* return a unique string for the grid
|
||||
*/
|
||||
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,
|
||||
data: new Uint8Array(width * height),
|
||||
});
|
||||
|
||||
@@ -1,174 +0,0 @@
|
||||
import { Grid, Color, copyGrid, isInsideLarge, getColor } from "./grid";
|
||||
import { Point, around4 } from "./point";
|
||||
import { step } from "./step";
|
||||
import { copySnake, snakeSelfCollide, Snake } 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> = {};
|
||||
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 values = colors
|
||||
.map((k) => Array.from({ length: colorCount[k] }, () => k))
|
||||
.flat();
|
||||
const weights = colors
|
||||
.map((k) =>
|
||||
Array.from({ length: colorCount[k] }).map(
|
||||
(_, i, arr) => i / (arr.length - 1)
|
||||
)
|
||||
)
|
||||
.flat();
|
||||
|
||||
return (_grid: Grid, _snake: Snake, stack: Color[]) => {
|
||||
let score = 0;
|
||||
|
||||
for (let i = 0; i < stack.length; i++) {
|
||||
const u = stack[i] - values[i];
|
||||
|
||||
if (u !== 0) debugger;
|
||||
|
||||
if (u > 0) score -= 100 * u * (1 + 1 - weights[i]);
|
||||
else if (u < 0) score -= 100 * -u * (1 + weights[i]);
|
||||
else score += 100;
|
||||
}
|
||||
|
||||
return score;
|
||||
};
|
||||
};
|
||||
|
||||
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("");
|
||||
|
||||
const createCell = (
|
||||
key: string,
|
||||
grid: Grid,
|
||||
snake: Snake,
|
||||
stack: Color[],
|
||||
parent: any | null,
|
||||
heuristic: number
|
||||
) => ({
|
||||
key,
|
||||
parent,
|
||||
grid,
|
||||
snake,
|
||||
stack,
|
||||
weight: 1 + (parent?.weight || 0),
|
||||
f: heuristic - 0 * (1 + (parent?.weight || 0)),
|
||||
});
|
||||
|
||||
const unwrap = (c: ReturnType<typeof createCell> | null): Point[] =>
|
||||
c && c.parent
|
||||
? [
|
||||
...unwrap(c.parent),
|
||||
{ x: c.snake[0].x - c.snake[1].x, y: c.snake[0].y - c.snake[1].y },
|
||||
]
|
||||
: [];
|
||||
|
||||
export const computeBestRun = (
|
||||
grid0: Grid,
|
||||
snake0: Snake,
|
||||
options: { maxSnakeLength: number; colors: Color[] }
|
||||
) => {
|
||||
// const grid = copyGrid(grid0);
|
||||
// const snake = copySnake(snake0);
|
||||
// const stack: Color[] = [];
|
||||
|
||||
const computeHeuristic = createComputeHeuristic(
|
||||
grid0,
|
||||
snake0,
|
||||
options.colors
|
||||
);
|
||||
|
||||
const closeList: any = {};
|
||||
const openList = [
|
||||
createCell(
|
||||
computeKey(grid0, snake0, []),
|
||||
grid0,
|
||||
snake0,
|
||||
[],
|
||||
null,
|
||||
computeHeuristic(grid0, snake0, [])
|
||||
),
|
||||
];
|
||||
|
||||
let u = 8000;
|
||||
|
||||
let best = openList[0];
|
||||
|
||||
while (openList.length && u-- > 0) {
|
||||
openList.sort((a, b) => b.f - a.f);
|
||||
const c = openList.shift()!;
|
||||
|
||||
closeList[c.key] = true;
|
||||
|
||||
if (isGridEmpty(c.grid)) return unwrap(c);
|
||||
|
||||
if (c.f > best.f) best = c;
|
||||
|
||||
for (const direction of around4) {
|
||||
const snake = copySnake(c.snake);
|
||||
const stack = c.stack.slice();
|
||||
const grid = copyGrid(c.grid);
|
||||
|
||||
step(grid, snake, stack, direction, options);
|
||||
|
||||
const key = computeKey(grid, snake, stack);
|
||||
|
||||
if (
|
||||
!closeList[key] &&
|
||||
isInsideLarge(grid, 1, snake[0].x, snake[0].y) &&
|
||||
!snakeSelfCollide(snake)
|
||||
) {
|
||||
openList.push(
|
||||
createCell(
|
||||
key,
|
||||
grid,
|
||||
snake,
|
||||
stack,
|
||||
c,
|
||||
computeHeuristic(grid, snake, stack)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return unwrap(best);
|
||||
|
||||
// while (!isGridEmpty(g) && u-- > 0) {
|
||||
// let direction;
|
||||
|
||||
// for (let k = 10; k--; ) {
|
||||
// direction = around4[Math.floor(Math.random() * around4.length)];
|
||||
|
||||
// const sn = copySnake(s);
|
||||
// stepSnake(sn, direction, options);
|
||||
|
||||
// if (isInsideLarge(g, 1, sn[0].x, sn[0].y) && !snakeSelfCollide(sn)) {
|
||||
// break;
|
||||
// } else {
|
||||
// direction = undefined;
|
||||
// }
|
||||
// }
|
||||
|
||||
// if (direction !== undefined) {
|
||||
// step(g, s, q, direction, options);
|
||||
// commands.push(direction);
|
||||
// }
|
||||
// }
|
||||
|
||||
// return commands;
|
||||
};
|
||||
@@ -1,4 +1,11 @@
|
||||
{
|
||||
"name": "@snk/compute",
|
||||
"version": "1.0.0"
|
||||
"version": "1.0.0",
|
||||
"devDependencies": {
|
||||
"@zeit/ncc": "0.22.3",
|
||||
"park-miller": "1.1.0"
|
||||
},
|
||||
"scripts": {
|
||||
"benchmark": "ncc run __tests__/benchmark.ts --quiet"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,4 +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;
|
||||
|
||||
85
packages/compute/pruneLayer.ts
Normal file
85
packages/compute/pruneLayer.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
import { getColor, isEmpty, isInside, setColorEmpty } from "./grid";
|
||||
import { around4 } from "./point";
|
||||
import { sortPush } from "./utils/sortPush";
|
||||
import type { Color, Grid } from "./grid";
|
||||
import type { Point } from "./point";
|
||||
|
||||
type M = Point & { parent: M | null; h: number };
|
||||
|
||||
const unwrap = (grid: Grid, m: M | null): Point[] =>
|
||||
m ? [...unwrap(grid, m.parent), m] : [];
|
||||
|
||||
const getEscapePath = (
|
||||
grid: Grid,
|
||||
x: number,
|
||||
y: number,
|
||||
color: Color,
|
||||
forbidden: Point[] = []
|
||||
) => {
|
||||
const openList: M[] = [{ x, y, h: 0, parent: null }];
|
||||
const closeList: Point[] = [];
|
||||
|
||||
while (openList.length) {
|
||||
const c = openList.shift()!;
|
||||
|
||||
if (c.y === -1 || c.y === grid.height) return unwrap(grid, c);
|
||||
|
||||
for (const a of around4) {
|
||||
const x = c.x + a.x;
|
||||
const y = c.y + a.y;
|
||||
|
||||
if (!forbidden.some((cl) => cl.x === x && cl.y === y)) {
|
||||
if (!isInside(grid, x, y))
|
||||
return unwrap(grid, { x, y, parent: c } as any);
|
||||
|
||||
const u = getColor(grid, x, y);
|
||||
|
||||
if (
|
||||
(isEmpty(u) || u <= color) &&
|
||||
!closeList.some((cl) => cl.x === x && cl.y === y)
|
||||
) {
|
||||
const h = Math.abs(grid.height / 2 - y);
|
||||
const o = { x, y, parent: c, h };
|
||||
|
||||
sortPush(openList, o, (a, b) => a.h - b.h);
|
||||
closeList.push(o);
|
||||
openList.push(o);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const isFree = (
|
||||
grid: Grid,
|
||||
x: number,
|
||||
y: number,
|
||||
color: Color,
|
||||
snakeN: number
|
||||
) => {
|
||||
const one = getEscapePath(grid, x, y, color);
|
||||
|
||||
if (!one) return false;
|
||||
|
||||
const two = getEscapePath(grid, x, y, color, one.slice(0, snakeN));
|
||||
|
||||
return !!two;
|
||||
};
|
||||
|
||||
export const pruneLayer = (grid: Grid, color: Color, snakeN: number) => {
|
||||
const chunk: Point[] = [];
|
||||
|
||||
for (let x = grid.width; x--; )
|
||||
for (let y = grid.height; y--; ) {
|
||||
const c = getColor(grid, x, y);
|
||||
|
||||
if (!isEmpty(c) && c <= color && isFree(grid, x, y, color, snakeN)) {
|
||||
setColorEmpty(grid, x, y);
|
||||
chunk.push({ x, y });
|
||||
}
|
||||
}
|
||||
|
||||
return chunk;
|
||||
};
|
||||
@@ -1,26 +1,46 @@
|
||||
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 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;
|
||||
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 copySnake = (x: Snake) => x.map((p) => ({ ...p }));
|
||||
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";
|
||||
|
||||
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);
|
||||
};
|
||||
|
||||
2
packages/compute/utils/array.ts
Normal file
2
packages/compute/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/compute/utils/sortPush.ts
Normal file
22
packages/compute/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);
|
||||
};
|
||||
59
packages/demo/canvas.ts
Normal file
59
packages/demo/canvas.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import { Color, Grid } from "@snk/compute/grid";
|
||||
import { drawLerpWorld, drawWorld } from "@snk/draw/drawWorld";
|
||||
import { Snake } from "@snk/compute/snake";
|
||||
|
||||
export const drawOptions = {
|
||||
sizeBorderRadius: 2,
|
||||
sizeCell: 16,
|
||||
sizeDot: 12,
|
||||
colorBorder: "#1b1f230a",
|
||||
colorDots: {
|
||||
1: "#9be9a8",
|
||||
2: "#40c463",
|
||||
3: "#30a14e",
|
||||
4: "#216e39",
|
||||
5: "orange",
|
||||
},
|
||||
colorEmpty: "#ebedf0",
|
||||
colorSnake: "purple",
|
||||
};
|
||||
|
||||
export const createCanvas = ({
|
||||
width,
|
||||
height,
|
||||
}: {
|
||||
width: number;
|
||||
height: number;
|
||||
}) => {
|
||||
const canvas = document.createElement("canvas");
|
||||
const upscale = 2;
|
||||
const w = drawOptions.sizeCell * (width + 4);
|
||||
const h = drawOptions.sizeCell * (height + 4) + 200;
|
||||
canvas.width = w * upscale;
|
||||
canvas.height = h * upscale;
|
||||
canvas.style.width = w + "px";
|
||||
canvas.style.height = h + "px";
|
||||
|
||||
document.body.appendChild(canvas);
|
||||
|
||||
const ctx = canvas.getContext("2d")!;
|
||||
ctx.scale(upscale, upscale);
|
||||
|
||||
const draw = (grid: Grid, snake: Snake, stack: Color[]) => {
|
||||
ctx.clearRect(0, 0, 9999, 9999);
|
||||
drawWorld(ctx, grid, snake, stack, drawOptions);
|
||||
};
|
||||
|
||||
const drawLerp = (
|
||||
grid: Grid,
|
||||
snake0: Snake,
|
||||
snake1: Snake,
|
||||
stack: Color[],
|
||||
k: number
|
||||
) => {
|
||||
ctx.clearRect(0, 0, 9999, 9999);
|
||||
drawLerpWorld(ctx, grid, snake0, snake1, stack, k, drawOptions);
|
||||
};
|
||||
|
||||
return { draw, drawLerp, canvas, ctx };
|
||||
};
|
||||
62
packages/demo/demo.getAvailableRoutes.ts
Normal file
62
packages/demo/demo.getAvailableRoutes.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import { createCanvas } from "./canvas";
|
||||
import { snakeToCells } from "@snk/compute/snake";
|
||||
import { GUI } from "dat.gui";
|
||||
import { grid, snake } from "./sample";
|
||||
import { getAvailableRoutes } from "@snk/compute/getAvailableRoutes";
|
||||
import type { Point } from "@snk/compute/point";
|
||||
import type { Snake } from "@snk/compute/snake";
|
||||
|
||||
//
|
||||
// compute
|
||||
|
||||
const routes: Snake[][] = [];
|
||||
getAvailableRoutes(grid, snake, (chain) => {
|
||||
routes.push(chain);
|
||||
return routes.length > 10;
|
||||
});
|
||||
|
||||
const config = { routeN: 0, routeK: 0 };
|
||||
|
||||
//
|
||||
// draw
|
||||
|
||||
const { canvas, ctx, draw } = createCanvas(grid);
|
||||
document.body.appendChild(canvas);
|
||||
|
||||
draw(grid, snake, []);
|
||||
|
||||
let cancel: number;
|
||||
|
||||
const mod = (x: number, m: number) => ((x % m) + m) % m;
|
||||
|
||||
const onChange = () => {
|
||||
const t = Math.floor(Date.now() / 300);
|
||||
|
||||
cancelAnimationFrame(cancel);
|
||||
cancel = requestAnimationFrame(onChange);
|
||||
|
||||
const chain = routes[config.routeN] || [snake];
|
||||
|
||||
draw(grid, chain[mod(-t, chain.length)], []);
|
||||
|
||||
const cells: Point[] = [];
|
||||
chain.forEach((s) => cells.push(...snakeToCells(s)));
|
||||
|
||||
ctx.fillStyle = "orange";
|
||||
ctx.fillRect(0, 0, 1, 1);
|
||||
|
||||
cells
|
||||
.filter((x, i, arr) => i === arr.indexOf(x))
|
||||
.forEach((c) => {
|
||||
ctx.beginPath();
|
||||
ctx.fillRect((1 + c.x + 0.5) * 16 - 2, (2 + c.y + 0.5) * 16 - 2, 4, 4);
|
||||
});
|
||||
};
|
||||
|
||||
//
|
||||
// ui
|
||||
|
||||
const gui = new GUI();
|
||||
gui.add(config, "routeN", 0, routes.length - 1, 1).onChange(onChange);
|
||||
|
||||
onChange();
|
||||
57
packages/demo/demo.getBestRoute.ts
Normal file
57
packages/demo/demo.getBestRoute.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import { createCanvas } from "./canvas";
|
||||
import { getBestRoute } from "@snk/compute/getBestRoute";
|
||||
import { Color, copyGrid } from "../compute/grid";
|
||||
import { grid, snake } from "./sample";
|
||||
import { step } from "@snk/compute/step";
|
||||
import { isStableAndBound, stepSpring } from "./springUtils";
|
||||
|
||||
const chain = [snake, ...getBestRoute(grid, snake)!];
|
||||
|
||||
//
|
||||
// draw
|
||||
|
||||
const spring = { x: 0, v: 0, target: 0 };
|
||||
const springParams = { tension: 120, friction: 20, maxVelocity: 50 };
|
||||
let animationFrame: number;
|
||||
|
||||
const { canvas, drawLerp } = createCanvas(grid);
|
||||
document.body.appendChild(canvas);
|
||||
|
||||
const clamp = (x: number, a: number, b: number) => Math.max(a, Math.min(b, x));
|
||||
|
||||
const loop = () => {
|
||||
cancelAnimationFrame(animationFrame);
|
||||
|
||||
stepSpring(spring, springParams, spring.target);
|
||||
const stable = isStableAndBound(spring, spring.target);
|
||||
|
||||
const grid0 = copyGrid(grid);
|
||||
const stack0: Color[] = [];
|
||||
for (let i = 0; i < Math.min(chain.length, spring.x); i++)
|
||||
step(grid0, stack0, chain[i]);
|
||||
|
||||
const snake0 = chain[clamp(Math.floor(spring.x), 0, chain.length - 1)];
|
||||
const snake1 = chain[clamp(Math.ceil(spring.x), 0, chain.length - 1)];
|
||||
const k = spring.x % 1;
|
||||
|
||||
drawLerp(grid0, snake0, snake1, stack0, k);
|
||||
|
||||
if (!stable) animationFrame = requestAnimationFrame(loop);
|
||||
};
|
||||
|
||||
loop();
|
||||
|
||||
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", () => {
|
||||
spring.target = +input.value;
|
||||
cancelAnimationFrame(animationFrame);
|
||||
animationFrame = requestAnimationFrame(loop);
|
||||
});
|
||||
document.body.append(input);
|
||||
document.body.addEventListener("click", () => input.focus());
|
||||
21
packages/demo/demo.index.ts
Normal file
21
packages/demo/demo.index.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import * as grid from "@snk/compute/__fixtures__/grid";
|
||||
|
||||
const container = document.createElement("div");
|
||||
container.style.fontFamily = "helvetica";
|
||||
document.body.appendChild(container);
|
||||
|
||||
for (const demo of require("./demo.json").filter((x: any) => x !== "index")) {
|
||||
const title = document.createElement("h1");
|
||||
title.innerText = demo;
|
||||
|
||||
container.appendChild(title);
|
||||
|
||||
for (const g of Object.keys(grid)) {
|
||||
const a = document.createElement("a");
|
||||
a.style.display = "block";
|
||||
a.innerText = `${demo} - ${g}`;
|
||||
a.href = `./${demo}.html?grid=${g}`;
|
||||
|
||||
container.appendChild(a);
|
||||
}
|
||||
}
|
||||
1
packages/demo/demo.json
Normal file
1
packages/demo/demo.json
Normal file
@@ -0,0 +1 @@
|
||||
["index", "getAvailableRoutes", "getBestRoute", "pruneLayer"]
|
||||
48
packages/demo/demo.pruneLayer.ts
Normal file
48
packages/demo/demo.pruneLayer.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { createCanvas } from "./canvas";
|
||||
import { Color, copyGrid } from "../compute/grid";
|
||||
import { grid, snake } from "./sample";
|
||||
import { pruneLayer } from "@snk/compute/pruneLayer";
|
||||
|
||||
const colors = [1, 2, 3] as Color[];
|
||||
|
||||
const snakeN = snake.length / 2;
|
||||
|
||||
const layers = [{ grid, chunk: [] as { x: number; y: number }[] }];
|
||||
let grid0 = copyGrid(grid);
|
||||
for (const color of colors) {
|
||||
const chunk = pruneLayer(grid0, color, snakeN);
|
||||
layers.push({ chunk, grid: copyGrid(grid0) });
|
||||
}
|
||||
|
||||
const { canvas, ctx, draw } = createCanvas(grid);
|
||||
document.body.appendChild(canvas);
|
||||
|
||||
let k = 0;
|
||||
|
||||
const loop = () => {
|
||||
const { grid, chunk } = layers[k];
|
||||
|
||||
draw(grid, snake, []);
|
||||
|
||||
ctx.fillStyle = "orange";
|
||||
chunk.forEach(({ x, y }) => {
|
||||
ctx.beginPath();
|
||||
ctx.fillRect((1 + x + 0.5) * 16 - 2, (2 + y + 0.5) * 16 - 2, 4, 4);
|
||||
});
|
||||
};
|
||||
|
||||
loop();
|
||||
|
||||
const input = document.createElement("input") as any;
|
||||
input.type = "range";
|
||||
input.value = 0;
|
||||
input.step = 1;
|
||||
input.min = 0;
|
||||
input.max = layers.length - 1;
|
||||
input.style.width = "90%";
|
||||
input.addEventListener("input", () => {
|
||||
k = +input.value;
|
||||
loop();
|
||||
});
|
||||
document.body.append(input);
|
||||
document.body.addEventListener("click", () => input.focus());
|
||||
@@ -1,91 +0,0 @@
|
||||
import { generateRandomGrid } from "@snk/compute/generateGrid";
|
||||
import { Color, copyGrid } from "@snk/compute/grid";
|
||||
import { computeBestRun } from "@snk/compute";
|
||||
import { step } from "@snk/compute/step";
|
||||
import { drawWorld } from "@snk/draw/drawWorld";
|
||||
import { copySnake } from "@snk/compute/snake";
|
||||
|
||||
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 = { colors: [1, 2, 3], maxSnakeLength: 5 };
|
||||
|
||||
const grid0 = generateRandomGrid(18, 7, { ...gameOptions, emptyP: 2 });
|
||||
|
||||
const snake0 = [
|
||||
{ x: 4, y: -1 },
|
||||
{ x: 3, y: -1 },
|
||||
{ x: 2, y: -1 },
|
||||
{ x: 1, y: -1 },
|
||||
{ x: 0, y: -1 },
|
||||
];
|
||||
const stack0: Color[] = [];
|
||||
|
||||
const chain = computeBestRun(grid0, snake0, gameOptions);
|
||||
|
||||
//
|
||||
// draw
|
||||
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.width = drawOptions.sizeCell * (grid0.width + 4);
|
||||
canvas.height = drawOptions.sizeCell * (grid0.height + 4) + 100;
|
||||
document.body.appendChild(canvas);
|
||||
const ctx = canvas.getContext("2d")!;
|
||||
|
||||
const update = (n: number) => {
|
||||
const snake = copySnake(snake0);
|
||||
const stack = stack0.slice();
|
||||
const grid = copyGrid(grid0);
|
||||
|
||||
for (let i = 0; i < n; i++) step(grid, snake, stack, chain[i], gameOptions);
|
||||
|
||||
ctx.clearRect(0, 0, 9999, 9999);
|
||||
drawWorld(ctx, grid, snake, stack, drawOptions);
|
||||
};
|
||||
|
||||
//
|
||||
// controls
|
||||
|
||||
const input: any = document.createElement("input");
|
||||
input.type = "range";
|
||||
input.style.width = "100%";
|
||||
input.min = 0;
|
||||
input.max = chain.length;
|
||||
input.step = 1;
|
||||
input.value = 0;
|
||||
input.addEventListener("input", () => {
|
||||
setAutoPlay(false);
|
||||
update(+input.value);
|
||||
});
|
||||
document.addEventListener("click", () => input.focus());
|
||||
|
||||
document.body.appendChild(input);
|
||||
|
||||
const autoplayButton = document.createElement("button");
|
||||
let cancel: any;
|
||||
const loop = () => {
|
||||
input.value = (+input.value + 1) % +input.max;
|
||||
update(+input.value);
|
||||
cancelAnimationFrame(cancel);
|
||||
cancel = requestAnimationFrame(loop);
|
||||
};
|
||||
const setAutoPlay = (a: boolean) => {
|
||||
autoplayButton.innerHTML = a ? "pause" : "play";
|
||||
if (a) loop();
|
||||
else cancelAnimationFrame(cancel);
|
||||
};
|
||||
autoplayButton.addEventListener("click", () => {
|
||||
debugger;
|
||||
setAutoPlay(autoplayButton.innerHTML === "play");
|
||||
});
|
||||
document.body.appendChild(autoplayButton);
|
||||
|
||||
setAutoPlay(true);
|
||||
update(+input.value);
|
||||
@@ -6,15 +6,18 @@
|
||||
"@snk/draw": "1.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"webpack": "4.43.0",
|
||||
"@types/dat.gui": "0.7.5",
|
||||
"@types/webpack": "4.41.22",
|
||||
"dat.gui": "0.7.7",
|
||||
"html-webpack-plugin": "4.5.0",
|
||||
"ts-loader": "8.0.4",
|
||||
"ts-node": "9.0.0",
|
||||
"webpack": "4.44.2",
|
||||
"webpack-cli": "3.3.12",
|
||||
"webpack-dev-server": "3.11.0",
|
||||
"ts-loader": "8.0.1",
|
||||
"html-webpack-plugin": "4.3.0"
|
||||
"webpack-dev-server": "3.11.0"
|
||||
},
|
||||
"scripts": {
|
||||
"prepare": "tsc webpack.config.ts",
|
||||
"build": "yarn prepare ; webpack",
|
||||
"dev": "yarn prepare ; webpack-dev-server --port ${PORT-3000}"
|
||||
"build": "webpack",
|
||||
"dev": "webpack-dev-server --port ${PORT-3000}"
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
63
packages/demo/springUtils.ts
Normal file
63
packages/demo/springUtils.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
const epsilon = 0.01;
|
||||
|
||||
export const clamp = (a: number, b: number) => (x: number) =>
|
||||
Math.max(a, Math.min(b, x));
|
||||
|
||||
/**
|
||||
* step the spring, mutate the state to reflect the state at t+dt
|
||||
*
|
||||
*/
|
||||
const stepSpringOne = (
|
||||
s: { x: number; v: number },
|
||||
{
|
||||
tension,
|
||||
friction,
|
||||
maxVelocity = Infinity,
|
||||
}: { tension: number; friction: number; maxVelocity?: number },
|
||||
target: number,
|
||||
dt = 1 / 60
|
||||
) => {
|
||||
const a = -tension * (s.x - target) - friction * s.v;
|
||||
|
||||
s.v += a * dt;
|
||||
s.v = clamp(-maxVelocity / dt, maxVelocity / dt)(s.v);
|
||||
s.x += s.v * dt;
|
||||
};
|
||||
|
||||
/**
|
||||
* return true if the spring is to be considered in a stable state
|
||||
* ( close enough to the target and with a small enough velocity )
|
||||
*/
|
||||
export const isStable = (
|
||||
s: { x: number; v: number },
|
||||
target: number,
|
||||
dt = 1 / 60
|
||||
) => Math.abs(s.x - target) < epsilon && Math.abs(s.v * dt) < epsilon;
|
||||
|
||||
export const isStableAndBound = (
|
||||
s: { x: number; v: number },
|
||||
target: number,
|
||||
dt?: number
|
||||
) => {
|
||||
const stable = isStable(s, target, dt);
|
||||
if (stable) {
|
||||
s.x = target;
|
||||
s.v = 0;
|
||||
}
|
||||
return stable;
|
||||
};
|
||||
|
||||
export const stepSpring = (
|
||||
s: { x: number; v: number },
|
||||
params: { tension: number; friction: number; maxVelocity?: number },
|
||||
target: number,
|
||||
dt = 1 / 60
|
||||
) => {
|
||||
const interval = 1 / 60;
|
||||
|
||||
while (dt > 0) {
|
||||
stepSpringOne(s, params, target, Math.min(interval, dt));
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
dt -= interval;
|
||||
}
|
||||
};
|
||||
@@ -1,16 +1,20 @@
|
||||
import * as path from "path";
|
||||
|
||||
// @ts-ignore
|
||||
import * as HtmlWebpackPlugin from "html-webpack-plugin";
|
||||
|
||||
import type { Configuration } from "webpack";
|
||||
|
||||
const basePathname = (process.env.BASE_PATHNAME || "")
|
||||
.split("/")
|
||||
.filter(Boolean);
|
||||
|
||||
const demos: string[] = require("./demo.json");
|
||||
|
||||
const config: Configuration = {
|
||||
mode: "development",
|
||||
entry: "./index",
|
||||
entry: Object.fromEntries(
|
||||
demos.map((demo: string) => [demo, `./demo.${demo}`])
|
||||
),
|
||||
resolve: { extensions: [".ts", ".js"] },
|
||||
output: {
|
||||
path: path.join(__dirname, "dist"),
|
||||
@@ -21,19 +25,27 @@ const config: Configuration = {
|
||||
rules: [
|
||||
{
|
||||
exclude: /node_modules/,
|
||||
test: /\.(js|ts)$/,
|
||||
test: /\.ts$/,
|
||||
loader: "ts-loader",
|
||||
options: {
|
||||
compilerOptions: {
|
||||
lib: ["dom", "es2020"],
|
||||
target: "es2020",
|
||||
module: "es2020",
|
||||
moduleResolution: "node",
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
plugins: [
|
||||
new HtmlWebpackPlugin({
|
||||
title: "demo",
|
||||
filename: "index.html",
|
||||
meta: {
|
||||
viewport: "width=device-width, initial-scale=1, shrink-to-fit=no",
|
||||
},
|
||||
}),
|
||||
...demos.map(
|
||||
(demo) =>
|
||||
new HtmlWebpackPlugin({
|
||||
filename: `${demo}.html`,
|
||||
chunks: [demo],
|
||||
})
|
||||
),
|
||||
],
|
||||
|
||||
devtool: false,
|
||||
|
||||
86
packages/draw/drawCircleStack.ts
Normal file
86
packages/draw/drawCircleStack.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import { Color } from "@snk/compute/grid";
|
||||
import { pathRoundedRect } from "./pathRoundedRect";
|
||||
import { Point } from "@snk/compute/point";
|
||||
|
||||
type Options = {
|
||||
colorDots: Record<Color, string>;
|
||||
colorBorder: string;
|
||||
sizeCell: number;
|
||||
sizeDot: number;
|
||||
sizeBorderRadius: number;
|
||||
};
|
||||
|
||||
const isInsideCircle = (x: number, y: number, r: number) => {
|
||||
const l = 6;
|
||||
let k = 0;
|
||||
for (let dx = 0; dx < l; dx++)
|
||||
for (let dy = 0; dy < l; dy++) {
|
||||
const ux = x + (dx + 0.5) / l;
|
||||
const uy = y + (dy + 0.5) / l;
|
||||
|
||||
if (ux * ux + uy * uy < r * r) k++;
|
||||
}
|
||||
|
||||
return k > l * l * 0.6;
|
||||
};
|
||||
|
||||
export const getCellPath = (n: number): Point[] => {
|
||||
const l = Math.ceil(Math.sqrt(n));
|
||||
|
||||
const cells = [];
|
||||
|
||||
for (let x = -l; x <= l; x++)
|
||||
for (let y = -l; y <= l; y++) {
|
||||
const a = (Math.atan2(y, x) + (5 * Math.PI) / 2) % (Math.PI * 2);
|
||||
|
||||
let r = 0;
|
||||
|
||||
while (!isInsideCircle(x, y, r + 0.5)) r++;
|
||||
|
||||
cells.push({ x, y, f: r * 100 + a });
|
||||
}
|
||||
|
||||
return cells.sort((a, b) => a.f - b.f).slice(0, n);
|
||||
};
|
||||
|
||||
export const cellPath = getCellPath(52 * 7 + 5);
|
||||
|
||||
export const getCircleSize = (n: number) => {
|
||||
const c = cellPath.slice(0, n);
|
||||
const xs = c.map((p) => p.x);
|
||||
const ys = c.map((p) => p.y);
|
||||
|
||||
return {
|
||||
max: { x: Math.max(0, ...xs), y: Math.max(0, ...ys) },
|
||||
min: { x: Math.min(0, ...xs), y: Math.min(0, ...ys) },
|
||||
};
|
||||
};
|
||||
|
||||
export const drawCircleStack = (
|
||||
ctx: CanvasRenderingContext2D,
|
||||
stack: Color[],
|
||||
o: Options
|
||||
) => {
|
||||
for (let i = stack.length; i--; ) {
|
||||
const { x, y } = cellPath[i];
|
||||
|
||||
ctx.save();
|
||||
ctx.translate(
|
||||
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;
|
||||
ctx.beginPath();
|
||||
|
||||
pathRoundedRect(ctx, o.sizeDot, o.sizeDot, o.sizeBorderRadius);
|
||||
|
||||
ctx.fill();
|
||||
ctx.stroke();
|
||||
ctx.closePath();
|
||||
ctx.restore();
|
||||
}
|
||||
};
|
||||
@@ -19,7 +19,7 @@ export const drawGrid = (
|
||||
for (let y = grid.height; y--; ) {
|
||||
const c = getColor(grid, x, y);
|
||||
// @ts-ignore
|
||||
const color = c === null ? o.colorEmpty : o.colorDots[c];
|
||||
const color = !c ? o.colorEmpty : o.colorDots[c];
|
||||
ctx.save();
|
||||
ctx.translate(
|
||||
x * o.sizeCell + (o.sizeCell - o.sizeDot) / 2,
|
||||
|
||||
68
packages/draw/drawSnake.ts
Normal file
68
packages/draw/drawSnake.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import { pathRoundedRect } from "./pathRoundedRect";
|
||||
import { Snake, snakeToCells } from "@snk/compute/snake";
|
||||
|
||||
type Options = {
|
||||
colorSnake: string;
|
||||
sizeCell: number;
|
||||
};
|
||||
|
||||
export const drawSnake = (
|
||||
ctx: CanvasRenderingContext2D,
|
||||
snake: Snake,
|
||||
o: Options
|
||||
) => {
|
||||
const cells = snakeToCells(snake);
|
||||
|
||||
for (let i = 0; i < cells.length; i++) {
|
||||
const u = (i + 1) * 0.6;
|
||||
|
||||
ctx.save();
|
||||
ctx.fillStyle = o.colorSnake;
|
||||
ctx.translate(cells[i].x * o.sizeCell + u, cells[i].y * o.sizeCell + u);
|
||||
ctx.beginPath();
|
||||
pathRoundedRect(
|
||||
ctx,
|
||||
o.sizeCell - u * 2,
|
||||
o.sizeCell - u * 2,
|
||||
(o.sizeCell - u * 2) * 0.25
|
||||
);
|
||||
ctx.fill();
|
||||
ctx.restore();
|
||||
}
|
||||
};
|
||||
|
||||
const lerp = (k: number, a: number, b: number) => (1 - k) * a + k * b;
|
||||
const clamp = (x: number, a: number, b: number) => Math.max(a, Math.min(b, x));
|
||||
|
||||
export const drawSnakeLerp = (
|
||||
ctx: CanvasRenderingContext2D,
|
||||
snake0: Snake,
|
||||
snake1: Snake,
|
||||
k: number,
|
||||
o: Options
|
||||
) => {
|
||||
const m = 0.8;
|
||||
const n = snake0.length / 2;
|
||||
for (let i = 0; i < n; i++) {
|
||||
const u = (i + 1) * 0.6;
|
||||
|
||||
const a = (1 - m) * (i / Math.max(n - 1, 1));
|
||||
const ki = clamp((k - a) / m, 0, 1);
|
||||
|
||||
const x = lerp(ki, snake0[i * 2 + 0], snake1[i * 2 + 0]) - 2;
|
||||
const y = lerp(ki, snake0[i * 2 + 1], snake1[i * 2 + 1]) - 2;
|
||||
|
||||
ctx.save();
|
||||
ctx.fillStyle = o.colorSnake;
|
||||
ctx.translate(x * o.sizeCell + u, y * o.sizeCell + u);
|
||||
ctx.beginPath();
|
||||
pathRoundedRect(
|
||||
ctx,
|
||||
o.sizeCell - u * 2,
|
||||
o.sizeCell - u * 2,
|
||||
(o.sizeCell - u * 2) * 0.25
|
||||
);
|
||||
ctx.fill();
|
||||
ctx.restore();
|
||||
}
|
||||
};
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Grid, Color } from "@snk/compute/grid";
|
||||
import { pathRoundedRect } from "./pathRoundedRect";
|
||||
import { Point } from "@snk/compute/point";
|
||||
import { drawGrid } from "./drawGrid";
|
||||
import { Snake } from "@snk/compute/snake";
|
||||
import { drawSnake, drawSnakeLerp } from "./drawSnake";
|
||||
|
||||
type Options = {
|
||||
export type Options = {
|
||||
colorDots: Record<Color, string>;
|
||||
colorEmpty: string;
|
||||
colorBorder: string;
|
||||
@@ -13,33 +13,29 @@ type Options = {
|
||||
sizeBorderRadius: number;
|
||||
};
|
||||
|
||||
export const drawSnake = (
|
||||
export const drawStack = (
|
||||
ctx: CanvasRenderingContext2D,
|
||||
snake: Point[],
|
||||
o: Options
|
||||
stack: Color[],
|
||||
max: number,
|
||||
width: number,
|
||||
o: { colorDots: Record<Color, string> }
|
||||
) => {
|
||||
for (let i = 0; i < snake.length; i++) {
|
||||
const u = (i + 1) * 0.6;
|
||||
ctx.save();
|
||||
|
||||
ctx.save();
|
||||
ctx.fillStyle = o.colorSnake;
|
||||
ctx.translate(snake[i].x * o.sizeCell + u, snake[i].y * o.sizeCell + u);
|
||||
ctx.beginPath();
|
||||
pathRoundedRect(
|
||||
ctx,
|
||||
o.sizeCell - u * 2,
|
||||
o.sizeCell - u * 2,
|
||||
(o.sizeCell - u * 2) * 0.25
|
||||
);
|
||||
ctx.fill();
|
||||
ctx.restore();
|
||||
const m = width / max;
|
||||
|
||||
for (let i = 0; i < stack.length; i++) {
|
||||
// @ts-ignore
|
||||
ctx.fillStyle = o.colorDots[stack[i]];
|
||||
ctx.fillRect(i * m, 0, m + width * 0.005, 10);
|
||||
}
|
||||
ctx.restore();
|
||||
};
|
||||
|
||||
export const drawWorld = (
|
||||
ctx: CanvasRenderingContext2D,
|
||||
grid: Grid,
|
||||
snake: Point[],
|
||||
snake: Snake,
|
||||
stack: Color[],
|
||||
o: Options
|
||||
) => {
|
||||
@@ -51,13 +47,41 @@ export const drawWorld = (
|
||||
|
||||
ctx.restore();
|
||||
|
||||
const m = 5;
|
||||
|
||||
ctx.save();
|
||||
|
||||
ctx.translate(o.sizeCell, (grid.height + 4) * o.sizeCell);
|
||||
for (let i = 0; i < stack.length; i++) {
|
||||
ctx.fillStyle = o.colorDots[stack[i]];
|
||||
ctx.fillRect(i * m, 0, m, 10);
|
||||
}
|
||||
|
||||
const max = grid.data.reduce((sum, x) => sum + +!!x, stack.length);
|
||||
drawStack(ctx, stack, max, grid.width * o.sizeCell, o);
|
||||
|
||||
ctx.restore();
|
||||
|
||||
// ctx.save();
|
||||
// ctx.translate(o.sizeCell + 100, (grid.height + 4) * o.sizeCell + 100);
|
||||
// ctx.scale(0.6, 0.6);
|
||||
// drawCircleStack(ctx, stack, o);
|
||||
// ctx.restore();
|
||||
};
|
||||
|
||||
export const drawLerpWorld = (
|
||||
ctx: CanvasRenderingContext2D,
|
||||
grid: Grid,
|
||||
snake0: Snake,
|
||||
snake1: Snake,
|
||||
stack: Color[],
|
||||
k: number,
|
||||
o: Options
|
||||
) => {
|
||||
ctx.save();
|
||||
|
||||
ctx.translate(1 * o.sizeCell, 2 * o.sizeCell);
|
||||
drawGrid(ctx, grid, o);
|
||||
drawSnakeLerp(ctx, snake0, snake1, k, o);
|
||||
|
||||
ctx.translate(0, (grid.height + 2) * o.sizeCell);
|
||||
|
||||
const max = grid.data.reduce((sum, x) => sum + +!!x, stack.length);
|
||||
drawStack(ctx, stack, max, grid.width * o.sizeCell, o);
|
||||
|
||||
ctx.restore();
|
||||
};
|
||||
|
||||
38
packages/gif-creator/__tests__/benchmark.ts
Normal file
38
packages/gif-creator/__tests__/benchmark.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { performance } from "perf_hooks";
|
||||
import { createSnake, nextSnake } from "@snk/compute/snake";
|
||||
import { realistic as grid } from "@snk/compute/__fixtures__/grid";
|
||||
import { createGif } from "..";
|
||||
|
||||
let snake = createSnake(Array.from({ length: 6 }, (_, i) => ({ x: i, y: -1 })));
|
||||
|
||||
const chain = [snake];
|
||||
for (let y = -1; y < grid.height; y++) {
|
||||
snake = nextSnake(snake, 0, 1);
|
||||
chain.push(snake);
|
||||
|
||||
for (let x = grid.width - 1; x--; ) {
|
||||
snake = nextSnake(snake, (y + 100) % 2 ? 1 : -1, 0);
|
||||
chain.push(snake);
|
||||
}
|
||||
}
|
||||
|
||||
const drawOptions = {
|
||||
sizeBorderRadius: 2,
|
||||
sizeCell: 16,
|
||||
sizeDot: 12,
|
||||
colorBorder: "#1b1f230a",
|
||||
colorDots: { 1: "#9be9a8", 2: "#40c463", 3: "#30a14e", 4: "#216e39" },
|
||||
colorEmpty: "#ebedf0",
|
||||
colorSnake: "purple",
|
||||
};
|
||||
|
||||
const gifOptions = { frameDuration: 200, step: 1 };
|
||||
|
||||
(async () => {
|
||||
const m = 3;
|
||||
const s = performance.now();
|
||||
for (let k = m; k--; )
|
||||
await createGif(grid, chain.slice(0, 50), drawOptions, gifOptions);
|
||||
|
||||
console.log((performance.now() - s) / m, "ms");
|
||||
})();
|
||||
@@ -1,6 +1,12 @@
|
||||
import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
import { createGif } from "..";
|
||||
import { generateRandomGrid } from "@snk/compute/generateGrid";
|
||||
import { computeBestRun } from "@snk/compute";
|
||||
import * as grids from "@snk/compute/__fixtures__/grid";
|
||||
import { snake3 as snake } from "@snk/compute/__fixtures__/snake";
|
||||
import { createSnake, nextSnake } from "@snk/compute/snake";
|
||||
import { getBestRoute } from "@snk/compute/getBestRoute";
|
||||
|
||||
jest.setTimeout(20 * 1000);
|
||||
|
||||
const drawOptions = {
|
||||
sizeBorderRadius: 2,
|
||||
@@ -12,31 +18,52 @@ const drawOptions = {
|
||||
colorSnake: "purple",
|
||||
};
|
||||
|
||||
const gameOptions = { maxSnakeLength: 5, colors: [1, 2, 3, 4] };
|
||||
const gifOptions = { frameDuration: 200, step: 1 };
|
||||
|
||||
const gifOptions = { delay: 200 };
|
||||
const dir = path.resolve(__dirname, "__snapshots__");
|
||||
|
||||
it("should generate gif", async () => {
|
||||
const grid = generateRandomGrid(14, 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",
|
||||
"enclave",
|
||||
] as const)
|
||||
it(`should generate ${key} gif`, async () => {
|
||||
const grid = grids[key];
|
||||
|
||||
const commands = computeBestRun(grid, snake, gameOptions).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();
|
||||
|
||||
fs.writeFileSync(path.resolve(dir, key + ".gif"), gif);
|
||||
});
|
||||
|
||||
it(`should generate swipper`, async () => {
|
||||
const grid = grids.smallFull;
|
||||
let snk = createSnake(Array.from({ length: 6 }, (_, i) => ({ x: i, y: -1 })));
|
||||
|
||||
const chain = [snk];
|
||||
for (let y = -1; y < grid.height; y++) {
|
||||
snk = nextSnake(snk, 0, 1);
|
||||
chain.push(snk);
|
||||
|
||||
for (let x = grid.width - 1; x--; ) {
|
||||
snk = nextSnake(snk, (y + 100) % 2 ? 1 : -1, 0);
|
||||
chain.push(snk);
|
||||
}
|
||||
}
|
||||
|
||||
const gif = await createGif(grid, chain, drawOptions, gifOptions);
|
||||
|
||||
expect(gif).toBeDefined();
|
||||
|
||||
fs.writeFileSync(path.resolve(dir, "swipper.gif"), gif);
|
||||
});
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
import { createGif } from "..";
|
||||
import { generateRandomGrid } from "@snk/compute/generateGrid";
|
||||
import { computeBestRun } from "@snk/compute";
|
||||
|
||||
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(42, 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 = computeBestRun(grid, snake, gameOptions);
|
||||
|
||||
createGif(grid, snake, commands, drawOptions, gameOptions, gifOptions).then(
|
||||
(buffer) => {
|
||||
process.stdout.write(buffer);
|
||||
}
|
||||
);
|
||||
@@ -2,57 +2,73 @@ 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 { drawWorld } from "@snk/draw/drawWorld";
|
||||
import { Snake } from "@snk/compute/snake";
|
||||
import { Options, drawLerpWorld } from "@snk/draw/drawWorld";
|
||||
import { step } from "@snk/compute/step";
|
||||
import * as tmp from "tmp";
|
||||
import * as execa from "execa";
|
||||
|
||||
export const createGif = async (
|
||||
grid0: Grid,
|
||||
snake0: Point[],
|
||||
commands: Point[],
|
||||
drawOptions: Parameters<typeof drawWorld>[4],
|
||||
gameOptions: Parameters<typeof step>[4],
|
||||
gifOptions: { delay: number }
|
||||
) => {
|
||||
const grid = copyGrid(grid0);
|
||||
const snake = copySnake(snake0);
|
||||
const stack: Color[] = [];
|
||||
|
||||
const width = drawOptions.sizeCell * (grid.width + 4);
|
||||
const height = drawOptions.sizeCell * (grid.height + 4) + 100;
|
||||
|
||||
const withTmpDir = async <T>(
|
||||
handler: (dir: string) => Promise<T>
|
||||
): Promise<T> => {
|
||||
const { name: dir, removeCallback: cleanUp } = tmp.dirSync({
|
||||
unsafeCleanup: true,
|
||||
});
|
||||
|
||||
const canvas = createCanvas(width, height);
|
||||
const ctx = canvas.getContext("2d")!;
|
||||
|
||||
const writeImage = (i: number) => {
|
||||
ctx.clearRect(0, 0, 99999, 99999);
|
||||
ctx.fillStyle = "#fff";
|
||||
ctx.fillRect(0, 0, 99999, 99999);
|
||||
drawWorld(ctx, grid, snake, stack, drawOptions);
|
||||
|
||||
const buffer = canvas.toBuffer("image/png", {
|
||||
compressionLevel: 0,
|
||||
filters: canvas.PNG_FILTER_NONE,
|
||||
});
|
||||
|
||||
const fileName = path.join(dir, `${i.toString().padStart(4, "0")}.png`);
|
||||
|
||||
fs.writeFileSync(fileName, buffer);
|
||||
};
|
||||
|
||||
try {
|
||||
writeImage(0);
|
||||
return await handler(dir);
|
||||
} finally {
|
||||
cleanUp();
|
||||
}
|
||||
};
|
||||
|
||||
for (let i = 0; i < commands.length; i++) {
|
||||
step(grid, snake, stack, commands[i], gameOptions);
|
||||
writeImage(i + 1);
|
||||
export const createGif = async (
|
||||
grid0: Grid,
|
||||
chain: Snake[],
|
||||
drawOptions: Options,
|
||||
gifOptions: { frameDuration: number; step: number }
|
||||
) =>
|
||||
withTmpDir(async (dir) => {
|
||||
const width = drawOptions.sizeCell * (grid0.width + 2);
|
||||
const height = drawOptions.sizeCell * (grid0.height + 4) + 100;
|
||||
|
||||
const canvas = createCanvas(width, height);
|
||||
const ctx = canvas.getContext("2d")!;
|
||||
|
||||
const grid = copyGrid(grid0);
|
||||
const stack: Color[] = [];
|
||||
|
||||
for (let i = 0; i < chain.length; i += 1) {
|
||||
const snake0 = chain[i];
|
||||
const snake1 = chain[Math.min(chain.length - 1, i + 1)];
|
||||
step(grid, stack, snake0);
|
||||
|
||||
for (let k = 0; k < gifOptions.step; k++) {
|
||||
ctx.clearRect(0, 0, 99999, 99999);
|
||||
ctx.fillStyle = "#fff";
|
||||
ctx.fillRect(0, 0, 99999, 99999);
|
||||
drawLerpWorld(
|
||||
ctx,
|
||||
grid,
|
||||
snake0,
|
||||
snake1,
|
||||
stack,
|
||||
k / gifOptions.step,
|
||||
drawOptions
|
||||
);
|
||||
|
||||
const buffer = canvas.toBuffer("image/png", {
|
||||
compressionLevel: 0,
|
||||
filters: canvas.PNG_FILTER_NONE,
|
||||
});
|
||||
|
||||
const fileName = path.join(
|
||||
dir,
|
||||
`${(i * gifOptions.step + k).toString().padStart(4, "0")}.png`
|
||||
);
|
||||
|
||||
fs.writeFileSync(fileName, buffer);
|
||||
}
|
||||
}
|
||||
|
||||
const outFileName = path.join(dir, "out.gif");
|
||||
@@ -63,7 +79,7 @@ export const createGif = async (
|
||||
[
|
||||
"convert",
|
||||
["-loop", "0"],
|
||||
["-delay", gifOptions.delay.toString()],
|
||||
["-delay", (gifOptions.frameDuration / 10).toString()],
|
||||
["-dispose", "2"],
|
||||
// ["-layers", "OptimizeFrame"],
|
||||
["-compress", "LZW"],
|
||||
@@ -85,7 +101,4 @@ export const createGif = async (
|
||||
);
|
||||
|
||||
return fs.readFileSync(optimizedFileName);
|
||||
} finally {
|
||||
cleanUp();
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
@@ -14,6 +14,6 @@
|
||||
"@zeit/ncc": "0.22.3"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "ncc run __tests__/dev.ts --quiet | tail -n +2 > out.gif "
|
||||
"benchmark": "ncc run __tests__/benchmark.ts --quiet"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
"name": "@snk/github-user-contribution",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"jsdom": "16.3.0"
|
||||
"jsdom": "16.4.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jsdom": "16.2.3"
|
||||
"@types/jsdom": "16.2.4"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user