🚀 faster solution

This commit is contained in:
platane
2020-10-14 22:03:50 +02:00
committed by Platane
parent fe821f6251
commit 8f5c1969a6
9 changed files with 279 additions and 557 deletions

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

View File

@@ -1,20 +1,15 @@
import {
Grid,
isInsideLarge,
getColor,
isInside,
Color,
isEmpty,
} from "./grid";
import { isInsideLarge, getColor, isInside, isEmpty } from "./grid";
import { around4 } from "./point";
import {
getHeadX,
getHeadY,
nextSnake,
Snake,
snakeEquals,
snakeWillSelfCollide,
} from "./snake";
import { sortPush } from "./utils/sortPush";
import type { Snake } from "./snake";
import type { Grid, Color } from "./grid";
export const getAvailableRoutes = (
grid: Grid,
@@ -28,8 +23,6 @@ export const getAvailableRoutes = (
const c = openList.shift()!;
const [snake] = c;
closeList.push(snake);
const cx = getHeadX(snake);
const cy = getHeadY(snake);
@@ -48,42 +41,14 @@ export const getAvailableRoutes = (
if (!closeList.some((s) => snakeEquals(nsnake, s))) {
const color = isInside(grid, nx, ny) && getColor(grid, nx, ny);
if (color && !isEmpty(color)) {
if (onSolution([nsnake, ...c.slice(0, -1)], color)) return;
if (!color || isEmpty(color)) {
sortPush(openList, [nsnake, ...c], (a, b) => a.length - b.length);
closeList.push(nsnake);
} else {
if (!openList.some(([s]) => snakeEquals(nsnake, s))) {
const chain = [nsnake, ...c];
openList.push(chain);
openList.sort((a, b) => a.length - b.length);
}
if (onSolution([nsnake, ...c.slice(0, -1)], color)) return;
}
}
}
}
}
};
export const getAvailableInterestingRoutes = (
grid: Grid,
snake0: Snake,
onSolution: (snakes: Snake[], color: Color) => boolean,
n = snake0.length
) => {
const solutions: Snake[] = [];
getAvailableRoutes(grid, snake0, (snakes, color) => {
const [snake] = snakes;
for (let j = solutions.length; j--; ) {
let same = true;
for (let i = 0; i < n * 2; i++)
same = same && solutions[j][i] === snake[i];
if (same) return false;
}
solutions.push(snake);
return onSolution(snakes, color);
});
};

View File

@@ -1,65 +0,0 @@
import { Grid, isInsideLarge, getColor, isInside, isEmpty } from "./grid";
import { around4 } from "./point";
import {
getHeadX,
getHeadY,
nextSnake,
Snake,
snakeEquals,
snakeWillSelfCollide,
} from "./snake";
const snakeEqualsN = (a: Snake, b: Snake, n = a.length / 2) => {
for (let i = 0; i < n * 2; i++) if (a[i] !== b[i]) return false;
return true;
};
export const getAvailableRoutes = (grid: Grid, snake0: Snake, n?: number) => {
const openList: Snake[][] = [[snake0]];
const closeList: Snake[] = [];
const solutions: Snake[][] = [];
while (openList.length) {
const c = openList.shift()!;
const [snake] = c;
closeList.push(snake);
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)) {
if (solutions.every(([s]) => !snakeEqualsN(s, nsnake, n))) {
const solution = [nsnake, ...c.slice(0, -1)];
solutions.push(solution);
}
} else {
if (!openList.some(([s]) => snakeEquals(nsnake, s))) {
const chain = [nsnake, ...c];
openList.push(chain);
openList.sort((a, b) => a.length - b.length);
}
}
}
}
}
}
return solutions;
};

View File

@@ -1,257 +0,0 @@
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

@@ -1,198 +1,22 @@
import { getAvailableInterestingRoutes } from "./getAvailableRoutes";
import {
Color,
copyGrid,
getColor,
Grid,
gridEquals,
isEmpty,
setColorEmpty,
} from "./grid";
import { copySnake, getHeadX, getHeadY, Snake, snakeEquals } from "./snake";
const createHeuristic = (grid0: Grid) => {
const colorCount: Record<Color, number> = [];
for (let x = grid0.width; x--; )
for (let y = grid0.height; y--; ) {
const color = getColor(grid0, x, y);
if (!isEmpty(color))
// @ts-ignore
colorCount[color] = (0 | colorCount[color]) + 1;
}
// const colors = Object.keys(colorCount).map((x) => +x);
const target = Object.entries(colorCount)
.sort(([a], [b]) => +a - +b)
.map(([color, length]: any) => Array.from({ length }, () => +color))
.flat();
const getHeuristic = (_grid: Grid, _snake: Snake, stack: Color[]) =>
stack.reduce((s, x, i) => s + (target[i] === x ? 1 : 0), 0);
const getNextColorHeuristic = (
_grid: Grid,
_snake: Snake,
stack: Color[]
) => {
const colorTarget = target[stack.length];
// const cc = { ...colorCount };
// for (const color of stack) cc[color]--;
// let colorTarget;
// for (let i = colors.length; i--; )
// if (cc[colors[i]] > 0) colorTarget = colors[i];
return (c: Color) => {
if (colorTarget === c) return 1;
return 0;
// return 1 - Math.abs(colorTarget - c) / 10;
};
};
const isEnd = (_grid: Grid, _snake: Snake, stack: Color[]) =>
stack.length === target.length;
return { isEnd, getHeuristic, getNextColorHeuristic };
};
type OpenListItem = {
grid: Grid;
snake: Snake;
chain: Snake[];
stack: Color[];
weight: number;
heuristic: number;
parent: OpenListItem | null;
};
const unroll = (o: OpenListItem | null): Snake[] =>
!o ? [] : [...unroll(o.parent), ...o.chain.slice().reverse()];
const itemEquals = (
a: { grid: Grid; snake: Snake },
b: { grid: Grid; snake: Snake }
) => snakeEquals(a.snake, b.snake) && gridEquals(a.grid, b.grid);
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 { isEnd, getNextColorHeuristic } = createHeuristic(grid0);
const grid = copyGrid(grid0);
const colors = extractColors(grid0);
const snakeN = snake0.length / 2;
let grid = copyGrid(grid0);
let snake = copySnake(snake0);
let stack: Color[] = [];
const chain: Snake[] = [snake0];
const fullChain: Snake[] = [];
while (!isEnd(grid, snake, stack)) {
const getColorHeuristic = getNextColorHeuristic(grid, snake, stack);
let solution: {
heuristic: number;
chain: Snake[];
color: Color;
} | null = null;
getAvailableInterestingRoutes(
grid,
snake,
(chain: Snake[], color: Color) => {
const heuristic = getColorHeuristic(color);
if (!solution || solution.heuristic < heuristic)
solution = { heuristic, chain, color };
return solution.heuristic === 1;
},
2
);
if (!solution) return null;
const { chain, color } = solution!;
snake = chain[0];
const x = getHeadX(snake);
const y = getHeadY(snake);
setColorEmpty(grid, x, y);
stack.push(color);
for (let i = chain.length; i--; ) fullChain.push(chain[i]);
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 fullChain;
};
export const getBestRoute2 = (grid0: Grid, snake0: Snake) => {
const { isEnd, getHeuristic, getNextColorHeuristic } = createHeuristic(grid0);
const closeList: { grid: Grid; snake: Snake }[] = [];
const openList: OpenListItem[] = [
{
grid: grid0,
stack: [],
snake: snake0,
parent: null,
weight: 0,
heuristic: getHeuristic(grid0, snake0, []),
chain: [],
},
];
while (openList.length) {
const parent = openList.shift()!;
if (isEnd(parent.grid, parent.snake, parent.stack)) return unroll(parent);
const solutions: { snake: Snake; chain: Snake[]; color: Color }[] = [];
const getColorHeuristic = getNextColorHeuristic(
parent.grid,
parent.snake,
parent.stack
);
getAvailableInterestingRoutes(
parent.grid,
parent.snake,
(chain: Snake[], color: Color) => {
if (
!solutions[0] ||
getColorHeuristic(solutions[0].color) <= getColorHeuristic(color)
)
solutions.unshift({ snake: chain[0], chain, color });
return solutions.length >= 3;
},
2
);
for (const { snake, chain, color } of solutions) {
const x = getHeadX(snake);
const y = getHeadY(snake);
const grid = copyGrid(parent.grid);
setColorEmpty(grid, x, y);
const stack = [...parent.stack, color];
const weight = parent.weight + chain.length;
const heuristic = getHeuristic(grid, snake, stack);
const item = { grid, stack, snake, chain, weight, heuristic, parent };
if (!closeList.some((c) => itemEquals(c, item))) {
closeList.push(item);
openList.push(item);
} else console.log("hit");
}
openList.sort((a, b) => a.heuristic - b.heuristic);
}
return null;
return chain.reverse().slice(1);
};

View File

@@ -40,11 +40,47 @@ 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;

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

View File

@@ -0,0 +1,2 @@
export const arrayEquals = <T>(a: T[], b: T[]) =>
a.length === b.length && a.every((_, i) => a[i] === b[i]);

View File

@@ -1,5 +1,5 @@
import { createCanvas } from "./canvas";
import { getBestRoute } from "../compute/getBestRoute";
import { getBestRoute } from "@snk/compute/getBestRoute";
import { Color, copyGrid } from "../compute/grid";
import { grid, snake } from "./sample";
import { step } from "@snk/compute/step";