Compare commits
31 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 | ||
|
|
1898ec16e4 | ||
|
|
fd9d7dadf6 | ||
|
|
73bfce908e | ||
|
|
dd23c1630e | ||
|
|
7377068a9a | ||
|
|
8a06b668cd |
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 }}
|
|
||||||
86
.github/workflows/main.yml
vendored
86
.github/workflows/main.yml
vendored
@@ -1,30 +1,88 @@
|
|||||||
name: main
|
name: main
|
||||||
|
|
||||||
on: [push, pull_request]
|
on: [push]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
test:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
# - run: sudo apt-get install gifsicle graphicsmagick
|
- run: sudo apt-get install gifsicle graphicsmagick
|
||||||
- uses: actions/checkout@v1
|
- uses: actions/checkout@v2.3.3
|
||||||
# - uses: actions/setup-node@v1.4.2
|
- uses: actions/setup-node@v1.4.4
|
||||||
# with:
|
with:
|
||||||
# node-version: 14
|
node-version: 14
|
||||||
|
|
||||||
# - uses: bahmutov/npm-install@v1.4.1
|
- uses: bahmutov/npm-install@v1.4.3
|
||||||
|
|
||||||
# - run: yarn type
|
- run: yarn type
|
||||||
# - run: yarn lint
|
- run: yarn lint
|
||||||
# - run: yarn test --ci
|
- run: yarn test --ci
|
||||||
# - run: yarn build:lib
|
- 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
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- run: mkdir dist
|
||||||
|
|
||||||
- name: generate-snake-game-from-github-contribution-grid
|
- name: generate-snake-game-from-github-contribution-grid
|
||||||
|
id: snake-gif
|
||||||
uses: Platane/snk@master
|
uses: Platane/snk@master
|
||||||
with:
|
with:
|
||||||
github_user_name: platane
|
github_user_name: platane
|
||||||
|
gif_out_path: dist/github-contribution-grid-snake.gif
|
||||||
|
|
||||||
- run: ls
|
- name: ensure the generated file exists
|
||||||
- run: ls
|
run: |
|
||||||
- run: ls
|
ls -l ${{ steps.snake-gif.outputs.gif_out_path }}
|
||||||
|
test -f ${{ steps.snake-gif.outputs.gif_out_path }}
|
||||||
|
|
||||||
|
- 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 }}
|
||||||
|
|||||||
19
Dockerfile
19
Dockerfile
@@ -4,13 +4,16 @@ RUN apt-get update \
|
|||||||
&& apt-get install -y --no-install-recommends gifsicle graphicsmagick \
|
&& apt-get install -y --no-install-recommends gifsicle graphicsmagick \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
COPY tsconfig.json package.json yarn.lock /github/platane.aa/
|
COPY tsconfig.json package.json yarn.lock /github/snk/
|
||||||
COPY packages /github/platane.aa/
|
COPY packages /github/snk/packages
|
||||||
|
|
||||||
RUN ( cd /github/platane.aa/ ; yarn install --frozen-lockfile )
|
RUN ( \
|
||||||
|
cd /github/snk \
|
||||||
RUN ( cd /github/platane.aa/ ; yarn build:action )
|
&& find . \
|
||||||
|
&& yarn install --frozen-lockfile \
|
||||||
CMD ["find", "/github"]
|
&& yarn build:action \
|
||||||
# CMD ["node", "./generate-snake-game-from-github-contribution-grid/packages/action/dist/index.js"]
|
&& mv packages/action/dist/* . \
|
||||||
|
&& rm -rf packages tsconfig.json package.json yarn.lock node_modules \
|
||||||
|
)
|
||||||
|
|
||||||
|
CMD ["node", "/github/snk/index.js"]
|
||||||
|
|||||||
@@ -1,3 +1,12 @@
|
|||||||
# snk
|
# snk
|
||||||
|
|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
Generates a snake game from a github user contributions grid and output a screen capture as gif
|
Generates a snake game from a github user contributions grid and output a screen capture as gif
|
||||||
|
|
||||||
|
- [demo](https://platane.github.io/snk/index.html)
|
||||||
|
|
||||||
|
- [github action](https://github.com/marketplace/actions/generate-snake-game-from-github-contribution-grid)
|
||||||
|
|||||||
@@ -9,9 +9,6 @@ outputs:
|
|||||||
runs:
|
runs:
|
||||||
using: "docker"
|
using: "docker"
|
||||||
image: "Dockerfile"
|
image: "Dockerfile"
|
||||||
# args:
|
|
||||||
# - ${{ inputs.github_user_name }}
|
|
||||||
# - ${{ inputs.gif_out_path }}
|
|
||||||
|
|
||||||
inputs:
|
inputs:
|
||||||
github_user_name:
|
github_user_name:
|
||||||
|
|||||||
12
package.json
12
package.json
@@ -5,11 +5,12 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"repository": "github:platane/snk",
|
"repository": "github:platane/snk",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/jest": "26.0.4",
|
"@types/jest": "26.0.14",
|
||||||
"jest": "26.1.0",
|
"@types/node": "14.11.8",
|
||||||
"prettier": "2.0.5",
|
"jest": "26.5.2",
|
||||||
"ts-jest": "26.1.2",
|
"prettier": "2.1.2",
|
||||||
"typescript": "3.9.6"
|
"ts-jest": "26.4.1",
|
||||||
|
"typescript": "4.0.3"
|
||||||
},
|
},
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"packages/**"
|
"packages/**"
|
||||||
@@ -18,6 +19,7 @@
|
|||||||
"type": "tsc --noEmit",
|
"type": "tsc --noEmit",
|
||||||
"lint": "yarn prettier -c '**/*.{ts,js,json,md,yml,yaml}' '!packages/action/dist/**' '!packages/demo/dist/**' '!packages/demo/webpack.config.js'",
|
"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",
|
"test": "jest --verbose --passWithNoTests --no-cache",
|
||||||
|
"dev:demo": "( cd packages/demo ; yarn dev )",
|
||||||
"build:demo": "( cd packages/demo ; yarn build )",
|
"build:demo": "( cd packages/demo ; yarn build )",
|
||||||
"build:action": "( cd packages/action ; yarn build )"
|
"build:action": "( cd packages/action ; yarn build )"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,31 +1,21 @@
|
|||||||
import { getGithubUserContribution, Cell } from "@snk/github-user-contribution";
|
import { getGithubUserContribution } from "@snk/github-user-contribution";
|
||||||
import { generateEmptyGrid } from "@snk/compute/generateGrid";
|
import { createGif } from "@snk/gif-creator";
|
||||||
import { setColor } from "@snk/compute/grid";
|
import { createSnake } from "@snk/compute/snake";
|
||||||
import { computeBestRun } from "@snk/compute";
|
import { getBestRoute } from "@snk/compute/getBestRoute";
|
||||||
import { createGif } from "../gif-creator";
|
import { userContributionToGrid } from "./userContributionToGrid";
|
||||||
|
|
||||||
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;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const generateContributionSnake = async (userName: string) => {
|
export const generateContributionSnake = async (userName: string) => {
|
||||||
const { cells, colorScheme } = await getGithubUserContribution(userName);
|
const { cells, colorScheme } = await getGithubUserContribution(userName);
|
||||||
|
|
||||||
const grid0 = userContributionToGrid(cells);
|
const grid0 = userContributionToGrid(cells);
|
||||||
|
|
||||||
const snake0 = [
|
const snake0 = createSnake([
|
||||||
{ x: 4, y: -1 },
|
{ x: 4, y: -1 },
|
||||||
{ x: 3, y: -1 },
|
{ x: 3, y: -1 },
|
||||||
{ x: 2, y: -1 },
|
{ x: 2, y: -1 },
|
||||||
{ x: 1, y: -1 },
|
{ x: 1, y: -1 },
|
||||||
{ x: 0, y: -1 },
|
{ x: 0, y: -1 },
|
||||||
];
|
]);
|
||||||
|
|
||||||
const drawOptions = {
|
const drawOptions = {
|
||||||
sizeBorderRadius: 2,
|
sizeBorderRadius: 2,
|
||||||
@@ -37,20 +27,11 @@ export const generateContributionSnake = async (userName: string) => {
|
|||||||
colorSnake: "purple",
|
colorSnake: "purple",
|
||||||
};
|
};
|
||||||
|
|
||||||
const gameOptions = { maxSnakeLength: 5 };
|
const gifOptions = { frameDuration: 100, step: 2 };
|
||||||
|
|
||||||
const gifOptions = { delay: 10 };
|
const chain = getBestRoute(grid0, snake0)!;
|
||||||
|
|
||||||
const commands = computeBestRun(grid0, snake0, gameOptions);
|
const buffer = await createGif(grid0, chain, drawOptions, gifOptions);
|
||||||
|
|
||||||
const buffer = await createGif(
|
|
||||||
grid0,
|
|
||||||
snake0,
|
|
||||||
commands,
|
|
||||||
drawOptions,
|
|
||||||
gameOptions,
|
|
||||||
gifOptions
|
|
||||||
);
|
|
||||||
|
|
||||||
return buffer;
|
return buffer;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,23 +4,14 @@ import { generateContributionSnake } from "./generateContributionSnake";
|
|||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
try {
|
try {
|
||||||
console.log("argv", process.argv);
|
const userName = core.getInput("github_user_name");
|
||||||
|
const gifOutPath = core.getInput("gif_out_path");
|
||||||
|
|
||||||
console.log(core.getInput("user_name"));
|
const buffer = await generateContributionSnake(userName);
|
||||||
console.log(core.getInput("gif_out_path"));
|
|
||||||
console.log("--");
|
|
||||||
console.log("--");
|
|
||||||
console.log(process.cwd());
|
|
||||||
console.log("--");
|
|
||||||
console.log(fs.readdirSync(process.cwd()));
|
|
||||||
console.log("--");
|
|
||||||
console.log("--");
|
|
||||||
console.log(process.env.GITHUB_WORKSPACE);
|
|
||||||
console.log("--");
|
|
||||||
console.log(fs.readdirSync(process.cwd()));
|
|
||||||
|
|
||||||
const buffer = await generateContributionSnake(core.getInput("user_name"));
|
fs.writeFileSync(gifOutPath, buffer);
|
||||||
fs.writeFileSync(core.getInput("gif_out_path"), buffer);
|
|
||||||
|
console.log(`::set-output name=gif_out_path::${gifOutPath}`);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
core.setFailed(`Action failed with "${e.message}"`);
|
core.setFailed(`Action failed with "${e.message}"`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"name": "@snk/action",
|
"name": "@snk/action",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@actions/core": "1.2.4",
|
"@actions/core": "1.2.6",
|
||||||
"@snk/gif-creator": "1.0.0",
|
"@snk/gif-creator": "1.0.0",
|
||||||
"@snk/github-user-contribution": "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 { createEmptyGrid, setColor, getColor, isInside, Color } from "../grid";
|
||||||
import { setColor, getColor, isInside } from "../grid";
|
|
||||||
|
|
||||||
it("should set / get cell", () => {
|
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);
|
expect(getColor(grid, 0, 1)).toBe(1);
|
||||||
});
|
});
|
||||||
@@ -20,7 +19,7 @@ test.each([
|
|||||||
[2, 1, false],
|
[2, 1, false],
|
||||||
[0, 3, false],
|
[0, 3, false],
|
||||||
])("isInside", (x, y, output) => {
|
])("isInside", (x, y, output) => {
|
||||||
const grid = generateEmptyGrid(2, 3);
|
const grid = createEmptyGrid(2, 3);
|
||||||
|
|
||||||
expect(isInside(grid, x, y)).toBe(output);
|
expect(isInside(grid, x, y)).toBe(output);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,24 +1,43 @@
|
|||||||
import { snakeSelfCollide } from "../snake";
|
import {
|
||||||
|
createSnake,
|
||||||
|
nextSnake,
|
||||||
|
snakeToCells,
|
||||||
|
snakeWillSelfCollide,
|
||||||
|
} from "../snake";
|
||||||
|
|
||||||
test.each([
|
it("should convert to point", () => {
|
||||||
[[{ x: 0, y: 0 }], false],
|
const snk0 = [
|
||||||
[
|
{ x: 1, y: -1 },
|
||||||
[
|
{ x: 1, y: 0 },
|
||||||
{ x: 0, y: 0 },
|
{ x: 0, y: 0 },
|
||||||
{ x: 0, y: 0 },
|
];
|
||||||
],
|
|
||||||
true,
|
expect(snakeToCells(createSnake(snk0))).toEqual(snk0);
|
||||||
],
|
});
|
||||||
[
|
|
||||||
[
|
it("should return next snake", () => {
|
||||||
{ x: 1, y: 7 },
|
const snk0 = [
|
||||||
{ x: 0, y: 6 },
|
{ x: 1, y: 1 },
|
||||||
{ x: 2, y: 8 },
|
{ x: 1, y: 0 },
|
||||||
{ x: 1, y: 7 },
|
{ x: 0, y: 0 },
|
||||||
{ x: 3, y: 9 },
|
];
|
||||||
],
|
|
||||||
true,
|
const snk1 = [
|
||||||
],
|
{ x: 2, y: 1 },
|
||||||
])("should report snake collision", (snake, collide) => {
|
{ x: 1, y: 1 },
|
||||||
expect(snakeSelfCollide(snake)).toBe(collide);
|
{ 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) =>
|
export const fillRandomGrid = (
|
||||||
generateGrid(width, height, { colors: [], emptyP: 1 });
|
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 generateGrid = (
|
if (k >= 0) setColor(grid, x, y, colors[k]);
|
||||||
width: number,
|
else setColorEmpty(grid, x, y);
|
||||||
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;
|
|
||||||
};
|
};
|
||||||
|
|||||||
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 = {
|
export type Grid = {
|
||||||
width: number;
|
width: number;
|
||||||
height: number;
|
height: number;
|
||||||
data: (Color | null)[];
|
data: Uint8Array;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getIndex = (grid: Grid, x: number, y: number) =>
|
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) =>
|
export const isInsideLarge = (grid: Grid, m: number, x: number, y: number) =>
|
||||||
x >= -m && y >= -m && x < grid.width + m && y < grid.height + m;
|
x >= -m && y >= -m && x < grid.width + m && y < grid.height + m;
|
||||||
|
|
||||||
export const getColor = (grid: Grid, x: number, y: number) =>
|
export const copyGrid = ({ width, height, data }: Grid) => ({
|
||||||
grid.data[getIndex(grid, x, y)];
|
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 = (
|
export const setColor = (
|
||||||
grid: Grid,
|
grid: Grid,
|
||||||
x: number,
|
x: number,
|
||||||
y: 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,44 +0,0 @@
|
|||||||
import { Grid, Color, copyGrid, isInsideLarge } from "./grid";
|
|
||||||
import { Point, around4 } from "./point";
|
|
||||||
import { stepSnake, step } from "./step";
|
|
||||||
import { copySnake, snakeSelfCollide } from "./snake";
|
|
||||||
|
|
||||||
const isGridEmpty = (grid: Grid) => grid.data.every((x) => x === null);
|
|
||||||
|
|
||||||
export const computeBestRun = (
|
|
||||||
grid: Grid,
|
|
||||||
snake: Point[],
|
|
||||||
options: { maxSnakeLength: number }
|
|
||||||
) => {
|
|
||||||
const g = copyGrid(grid);
|
|
||||||
const s = copySnake(snake);
|
|
||||||
const q: Color[] = [];
|
|
||||||
|
|
||||||
const commands: Point[] = [];
|
|
||||||
|
|
||||||
let u = 500;
|
|
||||||
|
|
||||||
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",
|
"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: 0, y: -1 },
|
||||||
{ x: -1, y: 0 },
|
{ x: -1, y: 0 },
|
||||||
{ x: 0, y: 1 },
|
{ 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,24 +1,46 @@
|
|||||||
import { Point } from "./point";
|
import { Point } from "./point";
|
||||||
|
|
||||||
export const snakeSelfCollideNext = (
|
export type Snake = Uint8Array & { _tag: "__Snake__" };
|
||||||
snake: Point[],
|
|
||||||
direction: Point,
|
|
||||||
options: { maxSnakeLength: number }
|
|
||||||
) => {
|
|
||||||
const hx = snake[0].x + direction.x;
|
|
||||||
const hy = snake[0].y + direction.y;
|
|
||||||
|
|
||||||
for (let i = 0; i < Math.min(options.maxSnakeLength, snake.length); i++)
|
export const getHeadX = (snake: Snake) => snake[0] - 2;
|
||||||
if (snake[i].x === hx && snake[i].y === hy) return true;
|
export const getHeadY = (snake: Snake) => snake[1] - 2;
|
||||||
|
|
||||||
|
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;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const snakeSelfCollide = (snake: Point[]) => {
|
export const snakeToCells = (snake: Snake) =>
|
||||||
for (let i = 1; i < snake.length; i++)
|
Array.from({ length: snake.length / 2 }, (_, i) => ({
|
||||||
if (snake[i].x === snake[0].x && snake[i].y === snake[0].y) return true;
|
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: Point[]) => 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 {
|
||||||
import { Point } from "./point";
|
Color,
|
||||||
|
getColor,
|
||||||
|
Grid,
|
||||||
|
isEmpty,
|
||||||
|
isInside,
|
||||||
|
setColorEmpty,
|
||||||
|
} from "./grid";
|
||||||
|
import { getHeadX, getHeadY, Snake } from "./snake";
|
||||||
|
|
||||||
const moveSnake = (snake: Point[], headx: number, heady: number) => {
|
export const step = (grid: Grid, stack: Color[], snake: Snake) => {
|
||||||
for (let k = snake.length - 1; k > 0; k--) {
|
const x = getHeadX(snake);
|
||||||
snake[k].x = snake[k - 1].x;
|
const y = getHeadY(snake);
|
||||||
snake[k].y = snake[k - 1].y;
|
const color = getColor(grid, x, y);
|
||||||
}
|
|
||||||
snake[0].x = headx;
|
|
||||||
snake[0].y = heady;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const stepSnake = (
|
if (isInside(grid, x, y) && !isEmpty(color)) {
|
||||||
snake: Point[],
|
stack.push(color);
|
||||||
direction: Point,
|
setColorEmpty(grid, x, y);
|
||||||
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 });
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
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,81 +0,0 @@
|
|||||||
// import { generateGrid } from "@snk/compute/generateGrid";
|
|
||||||
|
|
||||||
import { generateGrid } 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 { Point } from "@snk/compute/point";
|
|
||||||
|
|
||||||
const copySnake = (x: Point[]) => x.map((p) => ({ ...p }));
|
|
||||||
|
|
||||||
export const run = async () => {
|
|
||||||
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 };
|
|
||||||
|
|
||||||
const grid0 = generateGrid(42, 7, { colors: [1, 2, 3, 4], emptyP: 3 });
|
|
||||||
|
|
||||||
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);
|
|
||||||
|
|
||||||
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);
|
|
||||||
};
|
|
||||||
|
|
||||||
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", () => update(+input.value));
|
|
||||||
document.addEventListener("click", () => input.focus());
|
|
||||||
|
|
||||||
document.body.appendChild(input);
|
|
||||||
|
|
||||||
update(+input.value);
|
|
||||||
|
|
||||||
// while (chain.length) {
|
|
||||||
// await wait(100);
|
|
||||||
|
|
||||||
// step(grid, snake, stack, chain.shift()!, gameOptions);
|
|
||||||
|
|
||||||
// ctx.clearRect(0, 0, 9999, 9999);
|
|
||||||
// drawWorld(ctx, grid, snake, stack, options);
|
|
||||||
// }
|
|
||||||
|
|
||||||
// const wait = (delay = 0) => new Promise((r) => setTimeout(r, delay));
|
|
||||||
};
|
|
||||||
|
|
||||||
run();
|
|
||||||
@@ -2,18 +2,22 @@
|
|||||||
"name": "@snk/demo",
|
"name": "@snk/demo",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@snk/compute": "1.0.0"
|
"@snk/compute": "1.0.0",
|
||||||
|
"@snk/draw": "1.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"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-cli": "3.3.12",
|
||||||
"webpack-dev-server": "3.11.0",
|
"webpack-dev-server": "3.11.0"
|
||||||
"ts-loader": "8.0.1",
|
|
||||||
"html-webpack-plugin": "4.3.0"
|
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"prepare": "tsc webpack.config.ts",
|
"build": "webpack",
|
||||||
"build": "yarn prepare ; webpack",
|
"dev": "webpack-dev-server --port ${PORT-3000}"
|
||||||
"dev": "yarn prepare ; 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";
|
import * as path from "path";
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import * as HtmlWebpackPlugin from "html-webpack-plugin";
|
import * as HtmlWebpackPlugin from "html-webpack-plugin";
|
||||||
|
|
||||||
import type { Configuration } from "webpack";
|
import type { Configuration } from "webpack";
|
||||||
|
|
||||||
const basePathname = (process.env.BASE_PATHNAME || "")
|
const basePathname = (process.env.BASE_PATHNAME || "")
|
||||||
.split("/")
|
.split("/")
|
||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
|
|
||||||
|
const demos: string[] = require("./demo.json");
|
||||||
|
|
||||||
const config: Configuration = {
|
const config: Configuration = {
|
||||||
mode: "development",
|
mode: "development",
|
||||||
entry: "./index",
|
entry: Object.fromEntries(
|
||||||
|
demos.map((demo: string) => [demo, `./demo.${demo}`])
|
||||||
|
),
|
||||||
resolve: { extensions: [".ts", ".js"] },
|
resolve: { extensions: [".ts", ".js"] },
|
||||||
output: {
|
output: {
|
||||||
path: path.join(__dirname, "dist"),
|
path: path.join(__dirname, "dist"),
|
||||||
@@ -21,27 +25,31 @@ const config: Configuration = {
|
|||||||
rules: [
|
rules: [
|
||||||
{
|
{
|
||||||
exclude: /node_modules/,
|
exclude: /node_modules/,
|
||||||
test: /\.(js|ts)$/,
|
test: /\.ts$/,
|
||||||
loader: "ts-loader",
|
loader: "ts-loader",
|
||||||
|
options: {
|
||||||
|
compilerOptions: {
|
||||||
|
lib: ["dom", "es2020"],
|
||||||
|
target: "es2020",
|
||||||
|
module: "es2020",
|
||||||
|
moduleResolution: "node",
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
// game
|
...demos.map(
|
||||||
new HtmlWebpackPlugin({
|
(demo) =>
|
||||||
title: "demo",
|
new HtmlWebpackPlugin({
|
||||||
filename: "index.html",
|
filename: `${demo}.html`,
|
||||||
meta: {
|
chunks: [demo],
|
||||||
viewport: "width=device-width, initial-scale=1, shrink-to-fit=no",
|
})
|
||||||
},
|
),
|
||||||
}),
|
|
||||||
],
|
],
|
||||||
|
|
||||||
devtool: false,
|
devtool: false,
|
||||||
stats: "errors-only",
|
stats: "errors-only",
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
devServer: {},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default config;
|
export default config;
|
||||||
|
|||||||
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--; ) {
|
for (let y = grid.height; y--; ) {
|
||||||
const c = getColor(grid, x, y);
|
const c = getColor(grid, x, y);
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const color = c === null ? o.colorEmpty : o.colorDots[c];
|
const color = !c ? o.colorEmpty : o.colorDots[c];
|
||||||
ctx.save();
|
ctx.save();
|
||||||
ctx.translate(
|
ctx.translate(
|
||||||
x * o.sizeCell + (o.sizeCell - o.sizeDot) / 2,
|
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 { Grid, Color } from "@snk/compute/grid";
|
||||||
import { pathRoundedRect } from "./pathRoundedRect";
|
|
||||||
import { Point } from "@snk/compute/point";
|
|
||||||
import { drawGrid } from "./drawGrid";
|
import { drawGrid } from "./drawGrid";
|
||||||
|
import { Snake } from "@snk/compute/snake";
|
||||||
|
import { drawSnake, drawSnakeLerp } from "./drawSnake";
|
||||||
|
|
||||||
type Options = {
|
export type Options = {
|
||||||
colorDots: Record<Color, string>;
|
colorDots: Record<Color, string>;
|
||||||
colorEmpty: string;
|
colorEmpty: string;
|
||||||
colorBorder: string;
|
colorBorder: string;
|
||||||
@@ -13,51 +13,75 @@ type Options = {
|
|||||||
sizeBorderRadius: number;
|
sizeBorderRadius: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const drawSnake = (
|
export const drawStack = (
|
||||||
ctx: CanvasRenderingContext2D,
|
ctx: CanvasRenderingContext2D,
|
||||||
snake: Point[],
|
stack: Color[],
|
||||||
o: Options
|
max: number,
|
||||||
|
width: number,
|
||||||
|
o: { colorDots: Record<Color, string> }
|
||||||
) => {
|
) => {
|
||||||
for (let i = 0; i < snake.length; i++) {
|
ctx.save();
|
||||||
const u = (i + 1) * 0.6;
|
|
||||||
|
|
||||||
ctx.save();
|
const m = width / max;
|
||||||
ctx.fillStyle = o.colorSnake;
|
|
||||||
ctx.translate(snake[i].x * o.sizeCell + u, snake[i].y * o.sizeCell + u);
|
for (let i = 0; i < stack.length; i++) {
|
||||||
ctx.beginPath();
|
// @ts-ignore
|
||||||
pathRoundedRect(
|
ctx.fillStyle = o.colorDots[stack[i]];
|
||||||
ctx,
|
ctx.fillRect(i * m, 0, m + width * 0.005, 10);
|
||||||
o.sizeCell - u * 2,
|
|
||||||
o.sizeCell - u * 2,
|
|
||||||
(o.sizeCell - u * 2) * 0.25
|
|
||||||
);
|
|
||||||
ctx.fill();
|
|
||||||
ctx.restore();
|
|
||||||
}
|
}
|
||||||
|
ctx.restore();
|
||||||
};
|
};
|
||||||
|
|
||||||
export const drawWorld = (
|
export const drawWorld = (
|
||||||
ctx: CanvasRenderingContext2D,
|
ctx: CanvasRenderingContext2D,
|
||||||
grid: Grid,
|
grid: Grid,
|
||||||
snake: Point[],
|
snake: Snake,
|
||||||
stack: Color[],
|
stack: Color[],
|
||||||
o: Options
|
o: Options
|
||||||
) => {
|
) => {
|
||||||
ctx.save();
|
ctx.save();
|
||||||
|
|
||||||
ctx.translate(2 * o.sizeCell, 2 * o.sizeCell);
|
ctx.translate(1 * o.sizeCell, 2 * o.sizeCell);
|
||||||
drawGrid(ctx, grid, o);
|
drawGrid(ctx, grid, o);
|
||||||
drawSnake(ctx, snake, o);
|
drawSnake(ctx, snake, o);
|
||||||
|
|
||||||
ctx.restore();
|
ctx.restore();
|
||||||
|
|
||||||
const m = 5;
|
|
||||||
|
|
||||||
ctx.save();
|
ctx.save();
|
||||||
|
|
||||||
ctx.translate(o.sizeCell, (grid.height + 4) * o.sizeCell);
|
ctx.translate(o.sizeCell, (grid.height + 4) * o.sizeCell);
|
||||||
for (let i = 0; i < stack.length; i++) {
|
|
||||||
ctx.fillStyle = o.colorDots[stack[i]];
|
const max = grid.data.reduce((sum, x) => sum + +!!x, stack.length);
|
||||||
ctx.fillRect(i * m, 0, m, 10);
|
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();
|
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 { createGif } from "..";
|
||||||
import { generateGrid } from "@snk/compute/generateGrid";
|
import * as grids from "@snk/compute/__fixtures__/grid";
|
||||||
import { computeBestRun } from "@snk/compute";
|
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 = {
|
const drawOptions = {
|
||||||
sizeBorderRadius: 2,
|
sizeBorderRadius: 2,
|
||||||
@@ -12,31 +18,52 @@ const drawOptions = {
|
|||||||
colorSnake: "purple",
|
colorSnake: "purple",
|
||||||
};
|
};
|
||||||
|
|
||||||
const gameOptions = { maxSnakeLength: 5 };
|
const gifOptions = { frameDuration: 200, step: 1 };
|
||||||
|
|
||||||
const gifOptions = { delay: 200 };
|
const dir = path.resolve(__dirname, "__snapshots__");
|
||||||
|
|
||||||
it("should generate gif", async () => {
|
try {
|
||||||
const grid = generateGrid(14, 7, { colors: [1, 2, 3, 4], emptyP: 3 });
|
fs.mkdirSync(dir);
|
||||||
|
} catch (err) {}
|
||||||
|
|
||||||
const snake = [
|
for (const key of [
|
||||||
{ x: 4, y: -1 },
|
"empty",
|
||||||
{ x: 3, y: -1 },
|
"simple",
|
||||||
{ x: 2, y: -1 },
|
"corner",
|
||||||
{ x: 1, y: -1 },
|
"small",
|
||||||
{ x: 0, y: -1 },
|
"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(
|
const gif = await createGif(grid, chain, drawOptions, gifOptions);
|
||||||
grid,
|
|
||||||
snake,
|
expect(gif).toBeDefined();
|
||||||
commands,
|
|
||||||
drawOptions,
|
fs.writeFileSync(path.resolve(dir, key + ".gif"), gif);
|
||||||
gameOptions,
|
});
|
||||||
gifOptions
|
|
||||||
);
|
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();
|
expect(gif).toBeDefined();
|
||||||
|
|
||||||
|
fs.writeFileSync(path.resolve(dir, "swipper.gif"), gif);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,35 +0,0 @@
|
|||||||
import { createGif } from "..";
|
|
||||||
import { generateGrid } 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 };
|
|
||||||
|
|
||||||
const gifOptions = { delay: 20 };
|
|
||||||
|
|
||||||
const grid = generateGrid(42, 7, { colors: [1, 2, 3, 4], 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,58 +2,73 @@ import * as fs from "fs";
|
|||||||
import * as path from "path";
|
import * as path from "path";
|
||||||
import { createCanvas } from "canvas";
|
import { createCanvas } from "canvas";
|
||||||
import { Grid, copyGrid, Color } from "@snk/compute/grid";
|
import { Grid, copyGrid, Color } from "@snk/compute/grid";
|
||||||
import { Point } from "@snk/compute/point";
|
import { Snake } from "@snk/compute/snake";
|
||||||
import { copySnake } from "@snk/compute/snake";
|
import { Options, drawLerpWorld } from "@snk/draw/drawWorld";
|
||||||
import { drawWorld } from "@snk/draw/drawWorld";
|
|
||||||
import { step } from "@snk/compute/step";
|
import { step } from "@snk/compute/step";
|
||||||
import * as tmp from "tmp";
|
import * as tmp from "tmp";
|
||||||
// @ts-ignore
|
|
||||||
import * as execa from "execa";
|
import * as execa from "execa";
|
||||||
|
|
||||||
export const createGif = async (
|
const withTmpDir = async <T>(
|
||||||
grid0: Grid,
|
handler: (dir: string) => Promise<T>
|
||||||
snake0: Point[],
|
): Promise<T> => {
|
||||||
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 { name: dir, removeCallback: cleanUp } = tmp.dirSync({
|
const { name: dir, removeCallback: cleanUp } = tmp.dirSync({
|
||||||
unsafeCleanup: true,
|
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 {
|
try {
|
||||||
writeImage(0);
|
return await handler(dir);
|
||||||
|
} finally {
|
||||||
|
cleanUp();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
for (let i = 0; i < commands.length; i++) {
|
export const createGif = async (
|
||||||
step(grid, snake, stack, commands[i], gameOptions);
|
grid0: Grid,
|
||||||
writeImage(i + 1);
|
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");
|
const outFileName = path.join(dir, "out.gif");
|
||||||
@@ -64,7 +79,7 @@ export const createGif = async (
|
|||||||
[
|
[
|
||||||
"convert",
|
"convert",
|
||||||
["-loop", "0"],
|
["-loop", "0"],
|
||||||
["-delay", gifOptions.delay.toString()],
|
["-delay", (gifOptions.frameDuration / 10).toString()],
|
||||||
["-dispose", "2"],
|
["-dispose", "2"],
|
||||||
// ["-layers", "OptimizeFrame"],
|
// ["-layers", "OptimizeFrame"],
|
||||||
["-compress", "LZW"],
|
["-compress", "LZW"],
|
||||||
@@ -86,7 +101,4 @@ export const createGif = async (
|
|||||||
);
|
);
|
||||||
|
|
||||||
return fs.readFileSync(optimizedFileName);
|
return fs.readFileSync(optimizedFileName);
|
||||||
} finally {
|
});
|
||||||
cleanUp();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -14,6 +14,6 @@
|
|||||||
"@zeit/ncc": "0.22.3"
|
"@zeit/ncc": "0.22.3"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "ncc run __tests__/dev.ts --quiet | tail -n +2 > out.gif "
|
"benchmark": "ncc run __tests__/benchmark.ts --quiet"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { getGithubUserContribution } from "..";
|
|||||||
it("should get user contribution", async () => {
|
it("should get user contribution", async () => {
|
||||||
const { cells, colorScheme } = await getGithubUserContribution("platane");
|
const { cells, colorScheme } = await getGithubUserContribution("platane");
|
||||||
|
|
||||||
expect(cells).toBeDefined();
|
expect(cells.length).toBeGreaterThan(300);
|
||||||
expect(colorScheme).toEqual([
|
expect(colorScheme).toEqual([
|
||||||
"#ebedf0",
|
"#ebedf0",
|
||||||
"#9be9a8",
|
"#9be9a8",
|
||||||
|
|||||||
@@ -1,26 +1,11 @@
|
|||||||
// import * as https from "https";
|
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
// import * as cheerio from "cheerio";
|
|
||||||
|
|
||||||
import { JSDOM } from "jsdom";
|
import { JSDOM } from "jsdom";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* get the contribution grid from a github user page
|
||||||
|
*
|
||||||
|
* @param userName
|
||||||
|
*/
|
||||||
export const getGithubUserContribution = async (userName: string) => {
|
export const getGithubUserContribution = async (userName: string) => {
|
||||||
// const content: string = await new Promise((resolve, reject) => {
|
|
||||||
// const req = https.request(`https://github.com/${userName}`, (res) => {
|
|
||||||
// let data = "";
|
|
||||||
|
|
||||||
// res.on("error", reject);
|
|
||||||
// res.on("data", (chunk) => (data += chunk));
|
|
||||||
// res.on("end", () => resolve(data));
|
|
||||||
// });
|
|
||||||
|
|
||||||
// req.on("error", reject);
|
|
||||||
// req.end();
|
|
||||||
// });
|
|
||||||
|
|
||||||
// const dom = new JSDOM(content);
|
|
||||||
|
|
||||||
const dom = await JSDOM.fromURL(`https://github.com/${userName}`);
|
const dom = await JSDOM.fromURL(`https://github.com/${userName}`);
|
||||||
|
|
||||||
const colorScheme = Array.from(
|
const colorScheme = Array.from(
|
||||||
@@ -34,14 +19,14 @@ export const getGithubUserContribution = async (userName: string) => {
|
|||||||
dom.window.document.querySelectorAll(".js-calendar-graph-svg > g > g")
|
dom.window.document.querySelectorAll(".js-calendar-graph-svg > g > g")
|
||||||
)
|
)
|
||||||
.map((column, x) =>
|
.map((column, x) =>
|
||||||
Array.from(column.querySelectorAll("rect")).map((element, y) => ({
|
Array.from(column.querySelectorAll("rect")).map((element, y) => {
|
||||||
x,
|
const count = +element.getAttribute("data-count")!;
|
||||||
y,
|
const date = element.getAttribute("data-date")!;
|
||||||
count: element.getAttribute("data-count"),
|
const color = element.getAttribute("fill")!;
|
||||||
date: element.getAttribute("data-date"),
|
const k = colorScheme.indexOf(color);
|
||||||
color: element.getAttribute("fill"),
|
|
||||||
k: colorScheme.indexOf(element.getAttribute("fill")!),
|
return { x, y, count, date, color, k };
|
||||||
}))
|
})
|
||||||
)
|
)
|
||||||
.flat();
|
.flat();
|
||||||
|
|
||||||
@@ -53,9 +38,3 @@ type ThenArg<T> = T extends PromiseLike<infer U> ? U : T;
|
|||||||
export type Cell = ThenArg<
|
export type Cell = ThenArg<
|
||||||
ReturnType<typeof getGithubUserContribution>
|
ReturnType<typeof getGithubUserContribution>
|
||||||
>["cells"][number];
|
>["cells"][number];
|
||||||
|
|
||||||
// "#ebedf0";
|
|
||||||
// "#9be9a8";
|
|
||||||
// "#40c463";
|
|
||||||
// "#30a14e";
|
|
||||||
// "#216e39";
|
|
||||||
|
|||||||
@@ -2,9 +2,9 @@
|
|||||||
"name": "@snk/github-user-contribution",
|
"name": "@snk/github-user-contribution",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"jsdom": "16.3.0"
|
"jsdom": "16.4.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/jsdom": "16.2.3"
|
"@types/jsdom": "16.2.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user