🚀 prune layer algorithm
This commit is contained in:
@@ -29,6 +29,13 @@ setColor(enclave, 4, 3, 2 as Color);
|
|||||||
setColor(enclave, 3, 3, 1 as Color);
|
setColor(enclave, 3, 3, 1 as Color);
|
||||||
setColor(enclave, 5, 5, 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 create = (width: number, height: number, emptyP: number) => {
|
||||||
const grid = createEmptyGrid(width, height);
|
const grid = createEmptyGrid(width, height);
|
||||||
const random = new ParkMiller(10);
|
const random = new ParkMiller(10);
|
||||||
|
|||||||
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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
257
packages/compute/getBestRoute-layer.ts
Normal file
257
packages/compute/getBestRoute-layer.ts
Normal file
@@ -0,0 +1,257 @@
|
|||||||
|
import {
|
||||||
|
Color,
|
||||||
|
copyGrid,
|
||||||
|
getColor,
|
||||||
|
Grid,
|
||||||
|
isEmpty,
|
||||||
|
isInside,
|
||||||
|
isInsideLarge,
|
||||||
|
setColorEmpty,
|
||||||
|
} from "./grid";
|
||||||
|
import { around4 } from "./point";
|
||||||
|
import {
|
||||||
|
getHeadX,
|
||||||
|
getHeadY,
|
||||||
|
nextSnake,
|
||||||
|
Snake,
|
||||||
|
snakeEquals,
|
||||||
|
snakeWillSelfCollide,
|
||||||
|
} from "./snake";
|
||||||
|
import { sortPush } from "./utils/sortPush";
|
||||||
|
|
||||||
|
type M = { x: number; y: number; parent: M | null; h: number };
|
||||||
|
|
||||||
|
const unwrap = (grid: Grid, m: M | null): { x: number; y: number }[] =>
|
||||||
|
m ? [...unwrap(grid, m.parent), m] : [];
|
||||||
|
|
||||||
|
const getEscapePath = (
|
||||||
|
grid: Grid,
|
||||||
|
x: number,
|
||||||
|
y: number,
|
||||||
|
color: Color,
|
||||||
|
forbidden: { x: number; y: number }[] = []
|
||||||
|
) => {
|
||||||
|
const openList: M[] = [{ x, y, h: 0, parent: null }];
|
||||||
|
const closeList: { x: number; y: number }[] = [];
|
||||||
|
|
||||||
|
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 };
|
||||||
|
|
||||||
|
closeList.push(o);
|
||||||
|
openList.push(o);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
openList.sort((a, b) => a.h - b.h);
|
||||||
|
}
|
||||||
|
|
||||||
|
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: { x: number; y: number }[] = [];
|
||||||
|
|
||||||
|
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;
|
||||||
|
};
|
||||||
|
|
||||||
|
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();
|
||||||
|
};
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getAvailableWhiteListedRoutes = (
|
||||||
|
grid: Grid,
|
||||||
|
snake: Snake,
|
||||||
|
whiteList0: { x: number; y: number }[],
|
||||||
|
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;
|
||||||
|
};
|
||||||
|
|
||||||
|
const arrayEquals = <T>(a: T[], b: T[]) =>
|
||||||
|
a.length === b.length && a.every((_, i) => a[i] === b[i]);
|
||||||
|
|
||||||
|
type O = {
|
||||||
|
snake: Snake;
|
||||||
|
chain: Snake[];
|
||||||
|
chunk: { x: number; y: number }[];
|
||||||
|
grid: Grid;
|
||||||
|
parent: O | null;
|
||||||
|
};
|
||||||
|
const uunwrap = (o: O | null): Snake[] =>
|
||||||
|
!o ? [] : [...uunwrap(o.parent), ...o.chain.reverse()];
|
||||||
|
|
||||||
|
export const cleanLayer = (
|
||||||
|
grid0: Grid,
|
||||||
|
snake0: Snake,
|
||||||
|
chunk0: { x: number; y: number }[]
|
||||||
|
) => {
|
||||||
|
const next = {
|
||||||
|
grid: grid0,
|
||||||
|
snake: snake0,
|
||||||
|
chain: [snake0],
|
||||||
|
chunk: chunk0,
|
||||||
|
parent: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
const openList: O[] = [next];
|
||||||
|
const closeList: O[] = [next];
|
||||||
|
|
||||||
|
while (openList.length) {
|
||||||
|
const o = openList.shift()!;
|
||||||
|
|
||||||
|
if (o.chunk.length === 0) return uunwrap(o);
|
||||||
|
|
||||||
|
for (const chain of getAvailableWhiteListedRoutes(
|
||||||
|
o.grid,
|
||||||
|
o.snake,
|
||||||
|
o.chunk,
|
||||||
|
1
|
||||||
|
)) {
|
||||||
|
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 next = { snake, chain, chunk, grid, parent: o };
|
||||||
|
openList.push(next);
|
||||||
|
closeList.push(next);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getBestRoute = (grid0: Grid, snake0: Snake) => {
|
||||||
|
// for (const color of colors) {
|
||||||
|
// const chunk = pruneLayer(grid0, color, snakeN);
|
||||||
|
// layers.push({ chunk, grid: copyGrid(grid0) });
|
||||||
|
// }
|
||||||
|
|
||||||
|
const grid = copyGrid(grid0);
|
||||||
|
const colors = extractColors(grid0);
|
||||||
|
|
||||||
|
const chunk = pruneLayer(grid, colors[0], snake0.length / 2);
|
||||||
|
|
||||||
|
console.log(extractColors(grid0));
|
||||||
|
|
||||||
|
return cleanLayer(grid0, snake0, chunk);
|
||||||
|
};
|
||||||
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);
|
||||||
|
};
|
||||||
@@ -4,7 +4,7 @@ const container = document.createElement("div");
|
|||||||
container.style.fontFamily = "helvetica";
|
container.style.fontFamily = "helvetica";
|
||||||
document.body.appendChild(container);
|
document.body.appendChild(container);
|
||||||
|
|
||||||
for (const demo of ["getAvailableRoutes", "getBestRoute"]) {
|
for (const demo of require("./demo.json").filter((x: any) => x !== "index")) {
|
||||||
const title = document.createElement("h1");
|
const title = document.createElement("h1");
|
||||||
title.innerText = demo;
|
title.innerText = demo;
|
||||||
|
|
||||||
@@ -14,7 +14,7 @@ for (const demo of ["getAvailableRoutes", "getBestRoute"]) {
|
|||||||
const a = document.createElement("a");
|
const a = document.createElement("a");
|
||||||
a.style.display = "block";
|
a.style.display = "block";
|
||||||
a.innerText = `${demo} - ${g}`;
|
a.innerText = `${demo} - ${g}`;
|
||||||
a.href = `./demo-${demo}.html?grid=${g}`;
|
a.href = `./${demo}.html?grid=${g}`;
|
||||||
|
|
||||||
container.appendChild(a);
|
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/getBestRoute-layer";
|
||||||
|
|
||||||
|
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());
|
||||||
@@ -8,13 +8,13 @@ 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: {
|
entry: Object.fromEntries(
|
||||||
"demo.getAvailableRoutes": "./demo.getAvailableRoutes",
|
demos.map((demo: string) => [demo, `./demo.${demo}`])
|
||||||
"demo.getBestRoute": "./demo.getBestRoute",
|
),
|
||||||
"demo.index": "./demo.index",
|
|
||||||
},
|
|
||||||
resolve: { extensions: [".ts", ".js"] },
|
resolve: { extensions: [".ts", ".js"] },
|
||||||
output: {
|
output: {
|
||||||
path: path.join(__dirname, "dist"),
|
path: path.join(__dirname, "dist"),
|
||||||
@@ -39,18 +39,13 @@ const config: Configuration = {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
new HtmlWebpackPlugin({
|
...demos.map(
|
||||||
filename: "index.html",
|
(demo) =>
|
||||||
chunks: ["demo.index"],
|
new HtmlWebpackPlugin({
|
||||||
}),
|
filename: `${demo}.html`,
|
||||||
new HtmlWebpackPlugin({
|
chunks: [demo],
|
||||||
filename: "demo-getAvailableRoutes.html",
|
})
|
||||||
chunks: ["demo.getAvailableRoutes"],
|
),
|
||||||
}),
|
|
||||||
new HtmlWebpackPlugin({
|
|
||||||
filename: "demo-getBestRoute.html",
|
|
||||||
chunks: ["demo.getBestRoute"],
|
|
||||||
}),
|
|
||||||
],
|
],
|
||||||
|
|
||||||
devtool: false,
|
devtool: false,
|
||||||
|
|||||||
Reference in New Issue
Block a user