🚀 prune layer algorithm

This commit is contained in:
platane
2020-10-13 21:05:33 +02:00
committed by Platane
parent d7423423f8
commit fe821f6251
8 changed files with 435 additions and 19 deletions

View File

@@ -29,6 +29,13 @@ 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);

View 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);
}
});
});

View 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);
};

View 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);
};

View File

@@ -4,7 +4,7 @@ const container = document.createElement("div");
container.style.fontFamily = "helvetica";
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");
title.innerText = demo;
@@ -14,7 +14,7 @@ for (const demo of ["getAvailableRoutes", "getBestRoute"]) {
const a = document.createElement("a");
a.style.display = "block";
a.innerText = `${demo} - ${g}`;
a.href = `./demo-${demo}.html?grid=${g}`;
a.href = `./${demo}.html?grid=${g}`;
container.appendChild(a);
}

1
packages/demo/demo.json Normal file
View File

@@ -0,0 +1 @@
["index", "getAvailableRoutes", "getBestRoute", "pruneLayer"]

View 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());

View File

@@ -8,13 +8,13 @@ const basePathname = (process.env.BASE_PATHNAME || "")
.split("/")
.filter(Boolean);
const demos: string[] = require("./demo.json");
const config: Configuration = {
mode: "development",
entry: {
"demo.getAvailableRoutes": "./demo.getAvailableRoutes",
"demo.getBestRoute": "./demo.getBestRoute",
"demo.index": "./demo.index",
},
entry: Object.fromEntries(
demos.map((demo: string) => [demo, `./demo.${demo}`])
),
resolve: { extensions: [".ts", ".js"] },
output: {
path: path.join(__dirname, "dist"),
@@ -39,18 +39,13 @@ const config: Configuration = {
],
},
plugins: [
new HtmlWebpackPlugin({
filename: "index.html",
chunks: ["demo.index"],
}),
new HtmlWebpackPlugin({
filename: "demo-getAvailableRoutes.html",
chunks: ["demo.getAvailableRoutes"],
}),
new HtmlWebpackPlugin({
filename: "demo-getBestRoute.html",
chunks: ["demo.getBestRoute"],
}),
...demos.map(
(demo) =>
new HtmlWebpackPlugin({
filename: `${demo}.html`,
chunks: [demo],
})
),
],
devtool: false,