🚀 refactor get available routes
This commit is contained in:
@@ -1,79 +0,0 @@
|
||||
// @ts-ignore
|
||||
import * as ParkMiller from "park-miller";
|
||||
import { generateRandomGrid } from "../generateGrid";
|
||||
import { Snake } from "../snake";
|
||||
import { Grid } from "../grid";
|
||||
import { getBestRoute } from "../getBestRoute";
|
||||
import { performance } from "perf_hooks";
|
||||
|
||||
const snake0 = [
|
||||
{ x: 4, y: -1 },
|
||||
{ x: 3, y: -1 },
|
||||
{ x: 2, y: -1 },
|
||||
{ x: 1, y: -1 },
|
||||
{ x: 0, y: -1 },
|
||||
];
|
||||
|
||||
const gameOptions = {
|
||||
maxSnakeLength: 5,
|
||||
colors: [1, 2, 3, 4],
|
||||
};
|
||||
|
||||
const MAX_DURATION = 60 * 1000;
|
||||
const MAX_ITERATION = 10;
|
||||
|
||||
const run = (grid: Grid, snake0: Snake, k: number) => {
|
||||
const stats: number[] = [];
|
||||
const s0 = performance.now();
|
||||
|
||||
let n = 0;
|
||||
|
||||
while (performance.now() - s0 < MAX_DURATION && n++ < MAX_ITERATION) {
|
||||
const s = performance.now();
|
||||
getBestRoute(grid, snake0, gameOptions, k);
|
||||
|
||||
stats.push(performance.now() - s);
|
||||
}
|
||||
|
||||
return stats;
|
||||
};
|
||||
|
||||
const report = (arr: number[]) => {
|
||||
const average = (arr: number[]) =>
|
||||
arr.reduce((s, x) => s + x, 0) / arr.length;
|
||||
|
||||
const spread = (arr: number[]) => {
|
||||
const m = average(arr);
|
||||
const v = average(arr.map((x) => Math.pow(x - m, 2)));
|
||||
return Math.sqrt(v) / m;
|
||||
};
|
||||
|
||||
const format = (x: number): string => {
|
||||
const u = Math.floor(x / 1000);
|
||||
const d = Math.floor(x % 1000).toString();
|
||||
return u === 0 ? d : format(u) + " " + d.padEnd(3, "0");
|
||||
};
|
||||
|
||||
return `${format(average(arr)).padStart(12)} ms ±${(spread(arr) * 100)
|
||||
.toFixed(2)
|
||||
.padStart(5)}% (x${arr.length.toString().padStart(3)})`;
|
||||
};
|
||||
|
||||
[
|
||||
//
|
||||
[10, 10, 100],
|
||||
[30, 7, 100],
|
||||
[52, 7, 100],
|
||||
|
||||
[10, 10, 800],
|
||||
[30, 7, 800],
|
||||
[52, 7, 800],
|
||||
].forEach(([w, h, k]) => {
|
||||
const random = new ParkMiller(10);
|
||||
const grid = generateRandomGrid(w, h, { ...gameOptions, emptyP: 3 }, (a, b) =>
|
||||
random.integerInRange(a, b - 1)
|
||||
);
|
||||
const stats = run(grid, snake0, k);
|
||||
|
||||
console.log(`${w}x${h} : ${k}\n ${report(stats)}\n`);
|
||||
});
|
||||
@@ -1,21 +0,0 @@
|
||||
import { createEmptyGrid, setColor } from "../grid";
|
||||
import { getAvailableRoutes } from "../getAvailableRoutes";
|
||||
|
||||
it("should find no routes in empty grid", () => {
|
||||
const grid = createEmptyGrid(10, 10);
|
||||
const snake = [{ x: 2, y: 2 }];
|
||||
const options = { maxSnakeLength: 1 };
|
||||
|
||||
expect(getAvailableRoutes(grid, snake, options)).toEqual([]);
|
||||
});
|
||||
|
||||
it("should find one route in single cell grid", () => {
|
||||
const grid = createEmptyGrid(10, 10);
|
||||
setColor(grid, 3, 2, 3);
|
||||
const snake = [{ x: 2, y: 2 }];
|
||||
const options = { maxSnakeLength: 1 };
|
||||
|
||||
expect(getAvailableRoutes(grid, snake, options)).toEqual([
|
||||
{ color: 3, snakeN: [{ x: 3, y: 2 }], directions: [{ x: 1, y: 0 }] },
|
||||
]);
|
||||
});
|
||||
@@ -3,7 +3,7 @@ import { createEmptyGrid, setColor, getColor, isInside } from "../grid";
|
||||
it("should set / get cell", () => {
|
||||
const grid = createEmptyGrid(2, 3);
|
||||
|
||||
expect(getColor(grid, 0, 1)).toBe(null);
|
||||
expect(getColor(grid, 0, 1)).toBe(0);
|
||||
|
||||
setColor(grid, 0, 1, 1);
|
||||
|
||||
|
||||
@@ -1,62 +1,43 @@
|
||||
import { snakeSelfCollide, snakeWillSelfCollide } from "../snake";
|
||||
import {
|
||||
createSnake,
|
||||
nextSnake,
|
||||
snakeToCells,
|
||||
snakeWillSelfCollide,
|
||||
} from "../snake";
|
||||
|
||||
test.each([
|
||||
[[{ x: 0, y: 0 }], false],
|
||||
[
|
||||
[
|
||||
{ x: 0, y: 0 },
|
||||
{ x: 0, y: 0 },
|
||||
],
|
||||
true,
|
||||
],
|
||||
[
|
||||
[
|
||||
{ x: 1, y: 7 },
|
||||
{ x: 0, y: 6 },
|
||||
{ x: 2, y: 8 },
|
||||
{ x: 1, y: 7 },
|
||||
{ x: 3, y: 9 },
|
||||
],
|
||||
true,
|
||||
],
|
||||
])("should report snake collision", (snake, collide) => {
|
||||
expect(snakeSelfCollide(snake)).toBe(collide);
|
||||
it("should convert to point", () => {
|
||||
const snk0 = [
|
||||
{ x: 1, y: -1 },
|
||||
{ x: 1, y: 0 },
|
||||
{ x: 0, y: 0 },
|
||||
];
|
||||
|
||||
expect(snakeToCells(createSnake(snk0))).toEqual(snk0);
|
||||
});
|
||||
|
||||
test.each([
|
||||
[
|
||||
[
|
||||
{ x: 1, y: 7 },
|
||||
{ x: 0, y: 7 },
|
||||
{ x: 0, y: 8 },
|
||||
{ x: 0, y: 9 },
|
||||
{ x: 1, y: 9 },
|
||||
],
|
||||
{ x: 1, y: 7 },
|
||||
true,
|
||||
],
|
||||
[
|
||||
[
|
||||
{ x: 1, y: 7 },
|
||||
{ x: 0, y: 7 },
|
||||
{ x: 0, y: 8 },
|
||||
{ x: 0, y: 9 },
|
||||
{ x: 1, y: 9 },
|
||||
],
|
||||
{ x: 1, y: 8 },
|
||||
false,
|
||||
],
|
||||
[
|
||||
[
|
||||
{ x: 1, y: 7 },
|
||||
{ x: 0, y: 7 },
|
||||
{ x: 0, y: 8 },
|
||||
{ x: 0, y: 9 },
|
||||
{ x: 1, y: 9 },
|
||||
],
|
||||
{ x: 1, y: 8 },
|
||||
false,
|
||||
],
|
||||
])("should report snake collision next", (snake, { x, y }, collide) => {
|
||||
expect(snakeWillSelfCollide(snake, x, y)).toBe(collide);
|
||||
it("should return next snake", () => {
|
||||
const snk0 = [
|
||||
{ x: 1, y: 1 },
|
||||
{ x: 1, y: 0 },
|
||||
{ x: 0, y: 0 },
|
||||
];
|
||||
|
||||
const snk1 = [
|
||||
{ x: 2, y: 1 },
|
||||
{ x: 1, y: 1 },
|
||||
{ x: 1, y: 0 },
|
||||
];
|
||||
|
||||
expect(snakeToCells(nextSnake(createSnake(snk0), 1, 0))).toEqual(snk1);
|
||||
});
|
||||
|
||||
it("should test snake collision", () => {
|
||||
const snk0 = [
|
||||
{ x: 1, y: 1 },
|
||||
{ x: 1, y: 0 },
|
||||
{ x: 0, y: 0 },
|
||||
];
|
||||
|
||||
expect(snakeWillSelfCollide(createSnake(snk0), 1, 0)).toBe(false);
|
||||
expect(snakeWillSelfCollide(createSnake(snk0), 0, -1)).toBe(true);
|
||||
});
|
||||
|
||||
@@ -1,93 +0,0 @@
|
||||
import { step } from "../step";
|
||||
import { around4 } from "../point";
|
||||
import { createEmptyGrid, setColor, getColor } from "../grid";
|
||||
|
||||
it("should move snake", () => {
|
||||
const grid = createEmptyGrid(4, 3);
|
||||
const snake = [{ x: 1, y: 1 }];
|
||||
const direction = around4[0];
|
||||
const stack: number[] = [];
|
||||
const options = { maxSnakeLength: 5 };
|
||||
|
||||
step(grid, snake, stack, direction, options);
|
||||
|
||||
expect(snake).toEqual([
|
||||
{ x: 2, y: 1 },
|
||||
{ x: 1, y: 1 },
|
||||
]);
|
||||
|
||||
step(grid, snake, stack, direction, options);
|
||||
|
||||
expect(snake).toEqual([
|
||||
{ x: 3, y: 1 },
|
||||
{ x: 2, y: 1 },
|
||||
{ x: 1, y: 1 },
|
||||
]);
|
||||
|
||||
step(grid, snake, stack, direction, options);
|
||||
|
||||
expect(snake).toEqual([
|
||||
{ x: 4, y: 1 },
|
||||
{ x: 3, y: 1 },
|
||||
{ x: 2, y: 1 },
|
||||
{ x: 1, y: 1 },
|
||||
]);
|
||||
});
|
||||
|
||||
it("should move short snake", () => {
|
||||
const grid = createEmptyGrid(8, 3);
|
||||
const snake = [{ x: 1, y: 1 }];
|
||||
const direction = around4[0];
|
||||
const stack: number[] = [];
|
||||
const options = { maxSnakeLength: 3 };
|
||||
|
||||
step(grid, snake, stack, direction, options);
|
||||
|
||||
expect(snake).toEqual([
|
||||
{ x: 2, y: 1 },
|
||||
{ x: 1, y: 1 },
|
||||
]);
|
||||
|
||||
step(grid, snake, stack, direction, options);
|
||||
|
||||
expect(snake).toEqual([
|
||||
{ x: 3, y: 1 },
|
||||
{ x: 2, y: 1 },
|
||||
{ x: 1, y: 1 },
|
||||
]);
|
||||
|
||||
step(grid, snake, stack, direction, options);
|
||||
|
||||
expect(snake).toEqual([
|
||||
{ x: 4, y: 1 },
|
||||
{ x: 3, y: 1 },
|
||||
{ x: 2, y: 1 },
|
||||
]);
|
||||
|
||||
step(grid, snake, stack, direction, options);
|
||||
|
||||
expect(snake).toEqual([
|
||||
{ x: 5, y: 1 },
|
||||
{ x: 4, y: 1 },
|
||||
{ x: 3, y: 1 },
|
||||
]);
|
||||
});
|
||||
|
||||
it("should pick up fruit", () => {
|
||||
const grid = createEmptyGrid(4, 3);
|
||||
const snake = [{ x: 1, y: 1 }];
|
||||
const direction = around4[0];
|
||||
const stack: number[] = [];
|
||||
const options = { maxSnakeLength: 2 };
|
||||
setColor(grid, 3, 1, 9);
|
||||
|
||||
step(grid, snake, stack, direction, options);
|
||||
|
||||
expect(getColor(grid, 3, 1)).toBe(9);
|
||||
expect(stack).toEqual([]);
|
||||
|
||||
step(grid, snake, stack, direction, options);
|
||||
|
||||
expect(getColor(grid, 3, 1)).toBe(null);
|
||||
expect(stack).toEqual([9]);
|
||||
});
|
||||
@@ -1,111 +1,58 @@
|
||||
import { Grid, isInsideLarge, getColor, isInside } from "./grid";
|
||||
import { around4, Point, pointEquals } from "./point";
|
||||
import { snakeWillSelfCollide, Snake } from "./snake";
|
||||
|
||||
const computeSnakeKey = (snake: Snake) => {
|
||||
let key = "";
|
||||
for (let i = 0; i < snake.length; i++) key += snake[i].x + "," + snake[i].y;
|
||||
return key;
|
||||
};
|
||||
|
||||
type I = {
|
||||
// h: number;
|
||||
// f: number;
|
||||
w: number;
|
||||
key: string;
|
||||
snake: Snake;
|
||||
parent: I | null;
|
||||
};
|
||||
|
||||
const unwrap = (o: I | null, headN: Point): Point[] => {
|
||||
if (!o) return [];
|
||||
|
||||
const head0 = o.snake[0];
|
||||
|
||||
return [
|
||||
...unwrap(o.parent, head0),
|
||||
{ x: headN.x - head0.x, y: headN.y - head0.y },
|
||||
];
|
||||
};
|
||||
|
||||
const snakeEquals = (a: Snake, b: Snake, n = 99999) => {
|
||||
for (let i = 0; i < Math.min(a.length, b.length, n); i++)
|
||||
if (!pointEquals(a[i], b[i])) return false;
|
||||
return true;
|
||||
};
|
||||
import { Grid, isInsideLarge, getColor, isInside, Color } from "./grid";
|
||||
import { around4 } from "./point";
|
||||
import {
|
||||
getHeadX,
|
||||
getHeadY,
|
||||
nextSnake,
|
||||
Snake,
|
||||
snakeEquals,
|
||||
snakeWillSelfCollide,
|
||||
} from "./snake";
|
||||
|
||||
export const getAvailableRoutes = (
|
||||
grid: Grid,
|
||||
snake0: Snake,
|
||||
options: { maxSnakeLength: number },
|
||||
maxSolutions = 10,
|
||||
maxLengthEquality = 1,
|
||||
maxWeight = 30,
|
||||
maxIterations = 500
|
||||
onSolution: (snakes: Snake[], color: Color) => boolean
|
||||
) => {
|
||||
const openList: I[] = [
|
||||
{
|
||||
key: computeSnakeKey(snake0),
|
||||
snake: snake0,
|
||||
w: 0,
|
||||
parent: null,
|
||||
},
|
||||
];
|
||||
const closeList: Record<string, I> = {};
|
||||
const openList: Snake[][] = [[snake0]];
|
||||
const closeList: Snake[] = [];
|
||||
|
||||
const solutions: { snakeN: Snake; directions: Point[] }[] = [];
|
||||
|
||||
while (
|
||||
openList.length &&
|
||||
maxIterations-- > 0 &&
|
||||
openList[0].w <= maxWeight &&
|
||||
solutions.length < maxSolutions
|
||||
) {
|
||||
openList.sort((a, b) => a.w - b.w);
|
||||
while (openList.length) {
|
||||
const c = openList.shift()!;
|
||||
const [snake] = c;
|
||||
|
||||
closeList[c.key] = c;
|
||||
closeList.push(snake);
|
||||
|
||||
const [head] = c.snake;
|
||||
const color =
|
||||
isInside(grid, head.x, head.y) && getColor(grid, head.x, head.y);
|
||||
const cx = getHeadX(snake);
|
||||
const cy = getHeadY(snake);
|
||||
|
||||
if (color) {
|
||||
const s0 = solutions.find((s) =>
|
||||
snakeEquals(s.snakeN, c.snake, maxLengthEquality + 1)
|
||||
);
|
||||
for (let i = 0; i < around4.length; i++) {
|
||||
const { x: dx, y: dy } = around4[i];
|
||||
|
||||
const directions = unwrap(c.parent, c.snake[0]);
|
||||
const nx = cx + dx;
|
||||
const ny = cy + dy;
|
||||
|
||||
if (!s0 || directions.length < s0.directions.length)
|
||||
solutions.push({ snakeN: c.snake, directions });
|
||||
} else {
|
||||
for (let i = 0; i < around4.length; i++) {
|
||||
const x = head.x + around4[i].x;
|
||||
const y = head.y + around4[i].y;
|
||||
if (
|
||||
isInsideLarge(grid, 1, nx, ny) &&
|
||||
!snakeWillSelfCollide(snake, dx, dy)
|
||||
) {
|
||||
const nsnake = nextSnake(snake, dx, dy);
|
||||
|
||||
if (
|
||||
isInsideLarge(grid, 1, x, y) &&
|
||||
!snakeWillSelfCollide(c.snake, x, y)
|
||||
) {
|
||||
const snake = c.snake.slice(0, options.maxSnakeLength - 1);
|
||||
snake.unshift({ x, y });
|
||||
if (!closeList.some((s) => snakeEquals(nsnake, s))) {
|
||||
const color = isInside(grid, nx, ny) && getColor(grid, nx, ny);
|
||||
|
||||
const key = computeSnakeKey(snake);
|
||||
const chain = [nsnake, ...c];
|
||||
|
||||
if (!closeList[key] && !openList.some((s) => s.key === key)) {
|
||||
const w = 1 + c.w;
|
||||
openList.push({ key, snake, w, parent: c });
|
||||
if (color) {
|
||||
if (onSolution(chain, color)) return;
|
||||
} else {
|
||||
// console.log(key, closeList);
|
||||
// debugger;
|
||||
if (!openList.some(([s]) => snakeEquals(nsnake, s))) {
|
||||
openList.push(chain);
|
||||
openList.sort((a, b) => a.length - b.length);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return solutions;
|
||||
};
|
||||
|
||||
export const snakeSteps: Snake[] = [];
|
||||
|
||||
@@ -3,7 +3,7 @@ export type Color = number;
|
||||
export type Grid = {
|
||||
width: number;
|
||||
height: number;
|
||||
data: (Color | null)[];
|
||||
data: Uint8Array;
|
||||
};
|
||||
|
||||
export const getIndex = (grid: Grid, x: number, y: number) =>
|
||||
@@ -18,7 +18,11 @@ export const isInsideLarge = (grid: Grid, m: number, x: number, y: number) =>
|
||||
export const getColor = (grid: Grid, x: number, y: number) =>
|
||||
grid.data[getIndex(grid, x, y)];
|
||||
|
||||
export const copyGrid = (grid: Grid) => ({ ...grid, data: grid.data.slice() });
|
||||
export const copyGrid = ({ width, height, data }: Grid) => ({
|
||||
width,
|
||||
height,
|
||||
data: Uint8Array.from(data),
|
||||
});
|
||||
|
||||
export const setColor = (
|
||||
grid: Grid,
|
||||
@@ -26,11 +30,11 @@ export const setColor = (
|
||||
y: number,
|
||||
color: Color | null
|
||||
) => {
|
||||
grid.data[getIndex(grid, x, y)] = color;
|
||||
grid.data[getIndex(grid, x, y)] = color || 0;
|
||||
};
|
||||
|
||||
export const createEmptyGrid = (width: number, height: number) => ({
|
||||
width,
|
||||
height,
|
||||
data: Array.from({ length: width * height }, () => null),
|
||||
data: new Uint8Array(width * height),
|
||||
});
|
||||
|
||||
@@ -5,6 +5,6 @@ export const around4 = [
|
||||
{ x: 0, y: -1 },
|
||||
{ x: -1, y: 0 },
|
||||
{ x: 0, y: 1 },
|
||||
];
|
||||
] as const;
|
||||
|
||||
export const pointEquals = (a: Point, b: Point) => a.x === b.x && a.y === b.y;
|
||||
|
||||
@@ -1,37 +1,44 @@
|
||||
import { Point } from "./point";
|
||||
|
||||
export type Snake = Point[];
|
||||
export type Snake = Uint8Array & { _tag: "__Snake__" };
|
||||
|
||||
export const snakeSelfCollideNext = (
|
||||
snake: Snake,
|
||||
direction: Point,
|
||||
options: { maxSnakeLength: number }
|
||||
) => {
|
||||
const hx = snake[0].x + direction.x;
|
||||
const hy = snake[0].y + direction.y;
|
||||
export const getHeadX = (snake: Snake) => snake[0] - 2;
|
||||
export const getHeadY = (snake: Snake) => snake[1] - 2;
|
||||
|
||||
for (let i = 0; i < Math.min(options.maxSnakeLength, snake.length); i++)
|
||||
if (snake[i].x === hx && snake[i].y === hy) return true;
|
||||
export const snakeEquals = (a: Snake, b: Snake) => {
|
||||
for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;
|
||||
return true;
|
||||
};
|
||||
|
||||
export const nextSnake = (snake: Snake, dx: number, dy: number) => {
|
||||
const copy = new Uint8Array(snake.length);
|
||||
for (let i = 2; i < snake.length; i++) copy[i] = snake[i - 2];
|
||||
copy[0] = snake[0] + dx;
|
||||
copy[1] = snake[1] + dy;
|
||||
return copy as Snake;
|
||||
};
|
||||
|
||||
export const snakeWillSelfCollide = (snake: Snake, dx: number, dy: number) => {
|
||||
const nx = snake[0] + dx;
|
||||
const ny = snake[1] + dy;
|
||||
|
||||
for (let i = 2; i < snake.length - 2; i += 2)
|
||||
if (snake[i + 0] === nx && snake[i + 1] === ny) return true;
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
export const snakeWillSelfCollide = (
|
||||
snake: Snake,
|
||||
headx: number,
|
||||
heady: number
|
||||
) => {
|
||||
for (let i = 0; i < snake.length - 1; i++)
|
||||
if (snake[i].x === headx && snake[i].y === heady) return true;
|
||||
export const snakeToCells = (snake: Snake) =>
|
||||
Array.from({ length: snake.length / 2 }, (_, i) => ({
|
||||
x: snake[i * 2 + 0] - 2,
|
||||
y: snake[i * 2 + 1] - 2,
|
||||
}));
|
||||
|
||||
return false;
|
||||
export const createSnake = (points: Point[]) => {
|
||||
const snake = new Uint8Array(points.length * 2);
|
||||
for (let i = points.length; i--; ) {
|
||||
snake[i * 2 + 0] = points[i].x + 2;
|
||||
snake[i * 2 + 1] = points[i].y + 2;
|
||||
}
|
||||
return snake as Snake;
|
||||
};
|
||||
|
||||
export const snakeSelfCollide = (snake: Snake) => {
|
||||
for (let i = 1; i < snake.length; i++)
|
||||
if (snake[i].x === snake[0].x && snake[i].y === snake[0].y) return true;
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
export const copySnake = (x: Snake) => x.map((p) => ({ ...p }));
|
||||
|
||||
@@ -44,5 +44,5 @@ export const createCanvas = ({
|
||||
drawWorld(ctx, grid, snake, stack, drawOptions);
|
||||
};
|
||||
|
||||
return { draw, canvas };
|
||||
return { draw, canvas, ctx };
|
||||
};
|
||||
|
||||
@@ -1,105 +1,75 @@
|
||||
import { copyGrid } from "@snk/compute/grid";
|
||||
import { copySnake } from "@snk/compute/snake";
|
||||
import { createCanvas } from "./canvas";
|
||||
import { stepSnake } from "@snk/compute/step";
|
||||
import { samples } from "./samples";
|
||||
import { getAvailableRoutes } from "@snk/compute/getAvailableRoutes";
|
||||
import { createSnake, Snake, snakeToCells } from "@snk/compute/snake";
|
||||
import { GUI } from "dat.gui";
|
||||
import { Point } from "@snk/compute/point";
|
||||
|
||||
//
|
||||
// init
|
||||
|
||||
const label = new URLSearchParams(window.location.search).get("sample");
|
||||
const { grid: grid0, snake: snake0, gameOptions } =
|
||||
samples.find((s) => s.label === label) || samples[0];
|
||||
const { grid: grid0 } = samples.find((s) => s.label === label) || samples[0];
|
||||
|
||||
//
|
||||
// compute
|
||||
|
||||
const routes = getAvailableRoutes(grid0, snake0, gameOptions, 20);
|
||||
const snake0 = createSnake([
|
||||
//
|
||||
{ x: -1, y: -1 },
|
||||
{ x: -1, y: 0 },
|
||||
{ x: -1, y: 1 },
|
||||
{ x: -1, y: 2 },
|
||||
{ x: -1, y: 3 },
|
||||
]);
|
||||
const routes: Snake[][] = [];
|
||||
getAvailableRoutes(grid0, snake0, (snakes) => {
|
||||
routes.push(snakes);
|
||||
return routes.length > 10;
|
||||
});
|
||||
|
||||
const config = { routeN: 0, routeK: 0 };
|
||||
|
||||
//
|
||||
// draw
|
||||
|
||||
const { canvas, draw } = createCanvas(grid0);
|
||||
const { canvas, ctx, draw } = createCanvas(grid0);
|
||||
document.body.appendChild(canvas);
|
||||
|
||||
const update = (n: number, k: number) => {
|
||||
const snake = copySnake(snake0);
|
||||
const grid = copyGrid(grid0);
|
||||
const route = routes[n];
|
||||
draw(grid0, snake0, []);
|
||||
|
||||
const trace = [{ ...snake[0] }];
|
||||
let cancel: number;
|
||||
|
||||
for (let i = 0; i < k; i++) {
|
||||
stepSnake(snake, route.directions[i], gameOptions);
|
||||
trace.push({ ...snake[0] });
|
||||
}
|
||||
const mod = (x: number, m: number) => ((x % m) + m) % m;
|
||||
|
||||
draw(grid, snake, []);
|
||||
const onChange = () => {
|
||||
const t = Math.floor(Date.now() / 300);
|
||||
|
||||
const [cell] = route.snakeN;
|
||||
cancelAnimationFrame(cancel);
|
||||
cancel = requestAnimationFrame(onChange);
|
||||
|
||||
const ctx = canvas.getContext("2d")!;
|
||||
const chain = routes[config.routeN] || [snake0];
|
||||
|
||||
for (let i = 0; i < routes.length; i++) {
|
||||
ctx.fillStyle = "orange";
|
||||
ctx.fillRect(
|
||||
16 * (routes[i].snakeN[0].x + 1) + 6,
|
||||
16 * (routes[i].snakeN[0].y + 2) + 6,
|
||||
4,
|
||||
4
|
||||
);
|
||||
}
|
||||
draw(grid0, chain[mod(-t, chain.length)], []);
|
||||
|
||||
const cells: Point[] = [];
|
||||
chain.forEach((s) => cells.push(...snakeToCells(s)));
|
||||
|
||||
ctx.fillStyle = "orange";
|
||||
ctx.fillRect(16 * (cell.x + 1) + 4, 16 * (cell.y + 2) + 4, 8, 8);
|
||||
ctx.fillRect(0, 0, 1, 1);
|
||||
|
||||
for (let i = 0; i < trace.length; i++) {
|
||||
ctx.fillStyle = "purple";
|
||||
ctx.fillRect(16 * (trace[i].x + 1) + 6, 16 * (trace[i].y + 2) + 6, 4, 4);
|
||||
}
|
||||
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);
|
||||
});
|
||||
};
|
||||
|
||||
//
|
||||
// controls
|
||||
// ui
|
||||
|
||||
// const input0: any = document.createElement("input");
|
||||
// input0.type = "range";
|
||||
// input0.style.width = "100%";
|
||||
// input0.min = 0;
|
||||
// input0.max = snakeSteps.length - 1;
|
||||
// input0.step = 1;
|
||||
// input0.value = 0;
|
||||
// input0.addEventListener("input", () => {
|
||||
// const grid = copyGrid(grid0);
|
||||
// draw(grid, snakeSteps[+input0.value], []);
|
||||
// });
|
||||
// document.body.appendChild(input0);
|
||||
const gui = new GUI();
|
||||
gui.add(config, "routeN", 0, routes.length - 1, 1).onChange(onChange);
|
||||
|
||||
const inputA: any = document.createElement("input");
|
||||
inputA.type = "range";
|
||||
inputA.style.width = "100%";
|
||||
inputA.min = 0;
|
||||
inputA.max = routes.length - 1;
|
||||
inputA.step = 1;
|
||||
inputA.value = 0;
|
||||
inputA.addEventListener("input", () => {
|
||||
inputB.value = inputB.max = routes[+inputA.value].directions.length;
|
||||
update(+inputA.value, +inputB.value);
|
||||
});
|
||||
document.body.appendChild(inputA);
|
||||
|
||||
const inputB: any = document.createElement("input");
|
||||
inputB.type = "range";
|
||||
inputB.style.width = "100%";
|
||||
inputB.min = 0;
|
||||
inputB.step = 1;
|
||||
inputB.value = 0;
|
||||
inputB.addEventListener("input", () => {
|
||||
update(+inputA.value, +inputB.value);
|
||||
});
|
||||
document.body.appendChild(inputB);
|
||||
|
||||
if (routes[+inputA.value]) {
|
||||
inputB.value = inputB.max = routes[+inputA.value].directions.length;
|
||||
update(+inputA.value, +inputB.value);
|
||||
}
|
||||
onChange();
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
"@snk/draw": "1.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/dat.gui": "0.7.5",
|
||||
"dat.gui": "0.7.7",
|
||||
"html-webpack-plugin": "4.3.0",
|
||||
"ts-loader": "8.0.1",
|
||||
"webpack": "4.43.0",
|
||||
|
||||
@@ -19,7 +19,7 @@ export const drawGrid = (
|
||||
for (let y = grid.height; y--; ) {
|
||||
const c = getColor(grid, x, y);
|
||||
// @ts-ignore
|
||||
const color = c === null ? o.colorEmpty : o.colorDots[c];
|
||||
const color = !c ? o.colorEmpty : o.colorDots[c];
|
||||
ctx.save();
|
||||
ctx.translate(
|
||||
x * o.sizeCell + (o.sizeCell - o.sizeDot) / 2,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Grid, Color } from "@snk/compute/grid";
|
||||
import { pathRoundedRect } from "./pathRoundedRect";
|
||||
import { Point } from "@snk/compute/point";
|
||||
import { drawGrid } from "./drawGrid";
|
||||
import { Snake, snakeToCells } from "@snk/compute/snake";
|
||||
|
||||
type Options = {
|
||||
colorDots: Record<Color, string>;
|
||||
@@ -15,15 +15,17 @@ type Options = {
|
||||
|
||||
export const drawSnake = (
|
||||
ctx: CanvasRenderingContext2D,
|
||||
snake: Point[],
|
||||
snake: Snake,
|
||||
o: Options
|
||||
) => {
|
||||
for (let i = 0; i < snake.length; i++) {
|
||||
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(snake[i].x * o.sizeCell + u, snake[i].y * o.sizeCell + u);
|
||||
ctx.translate(cells[i].x * o.sizeCell + u, cells[i].y * o.sizeCell + u);
|
||||
ctx.beginPath();
|
||||
pathRoundedRect(
|
||||
ctx,
|
||||
@@ -39,7 +41,7 @@ export const drawSnake = (
|
||||
export const drawWorld = (
|
||||
ctx: CanvasRenderingContext2D,
|
||||
grid: Grid,
|
||||
snake: Point[],
|
||||
snake: Snake,
|
||||
stack: Color[],
|
||||
o: Options
|
||||
) => {
|
||||
@@ -60,4 +62,10 @@ export const drawWorld = (
|
||||
ctx.fillRect(i * m, 0, m, 10);
|
||||
}
|
||||
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();
|
||||
};
|
||||
|
||||
10
yarn.lock
10
yarn.lock
@@ -526,6 +526,11 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/color-name/-/color-name-1.1.1.tgz#1c1261bbeaa10a8055bbc5d8ab84b7b2afc846a0"
|
||||
integrity sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==
|
||||
|
||||
"@types/dat.gui@0.7.5":
|
||||
version "0.7.5"
|
||||
resolved "https://registry.yarnpkg.com/@types/dat.gui/-/dat.gui-0.7.5.tgz#8f4f87300ae553a57b246a7bf1b9fe5e13ca8b79"
|
||||
integrity sha512-5AqLThlTiuDSOZA7XZFogRj/UdGKn/iIfdFPuh37kY4s7TjTt+YUOlUmcCrY6wAYFFyThtt2z8qZlYcdkhJZ5w==
|
||||
|
||||
"@types/execa@2.0.0":
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/execa/-/execa-2.0.0.tgz#4dd5523520c51834bf8d0c280f460814e5238281"
|
||||
@@ -1918,6 +1923,11 @@ dashdash@^1.12.0:
|
||||
dependencies:
|
||||
assert-plus "^1.0.0"
|
||||
|
||||
dat.gui@0.7.7:
|
||||
version "0.7.7"
|
||||
resolved "https://registry.yarnpkg.com/dat.gui/-/dat.gui-0.7.7.tgz#7f96dbd21621a76385203659aebfa264ee6ae89b"
|
||||
integrity sha512-sRl/28gF/XRC5ywC9I4zriATTsQcpSsRG7seXCPnTkK8/EQMIbCu5NPMpICLGxX9ZEUvcXR3ArLYCtgreFoMDw==
|
||||
|
||||
data-urls@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/data-urls/-/data-urls-2.0.0.tgz#156485a72963a970f5d5821aaf642bef2bf2db9b"
|
||||
|
||||
Reference in New Issue
Block a user