Files
snk/packages/compute/cleanColoredLayer.ts
2020-10-29 23:27:08 +01:00

148 lines
4.1 KiB
TypeScript

import { Color, getColor, isEmpty, setColorEmpty } from "@snk/types/grid";
import {
getHeadX,
getHeadY,
getSnakeLength,
nextSnake,
} from "@snk/types/snake";
import type { Snake } from "@snk/types/snake";
import type { Grid } from "@snk/types/grid";
import type { Point } from "@snk/types/point";
import { getBestTunnel, trimTunnelEnd, trimTunnelStart } from "./getBestTunnel";
import { getPathTo } from "./getPathTo";
import { getTunnels } from "./getTunnels";
/**
* eat all the cell for which the color is smaller or equals to color and are reachable without going though cells with color+1 or higher
* attempt to eat the smaller color first
*/
export const cleanColoredLayer = (grid: Grid, snake0: Snake, color: Color) => {
const chain: Snake[] = [snake0];
const snakeN = getSnakeLength(snake0);
const tunnels = getTunnels(grid, getSnakeLength(snake0), color)
.map((tunnel) => ({ tunnel, f: tunnelScore(grid, color, tunnel) }))
.sort((a, b) => a.f - b.f);
while (tunnels.length) {
// get the best candidates
const candidates = tunnels.filter((a, _, [a0]) => a.f === a0.f);
// get the closest one
{
const x = getHeadX(chain[0]);
const y = getHeadY(chain[0]);
candidates.sort(
({ tunnel: [a] }, { tunnel: [b] }) =>
distanceSq(x, y, a.x, a.y) - distanceSq(x, y, b.x, b.y)
);
}
// pick tunnel and recompute it
// it might not be relevant since the grid changes
// in some edge case, it could lead to the snake reaching the first cell from the initial exit side
// causing it to self collide when on it's way through the tunnel
const { tunnel: tunnelCandidate } = candidates[0];
const tunnel = getBestTunnel(
grid,
tunnelCandidate[0].x,
tunnelCandidate[0].y,
color,
snakeN
)!;
// move to the start of the tunnel
chain.unshift(...getPathTo(grid, chain[0], tunnel[0].x, tunnel[0].y)!);
// move into the tunnel
chain.unshift(...getTunnelPath(chain[0], tunnel));
// update grid
for (const { x, y } of tunnel) setColorEmpty(grid, x, y);
// update other tunnels
// eventually remove the ones made empty
for (let i = tunnels.length; i--; ) {
updateTunnel(grid, tunnels[i].tunnel, tunnel);
if (tunnels[i].tunnel.length === 0) tunnels.splice(i, 1);
else tunnels[i].f = tunnelScore(grid, color, tunnels[i].tunnel);
}
tunnels.sort((a, b) => a.f - b.f);
}
return chain.slice(0, -1);
};
/**
* get the score of the tunnel
* prioritize tunnel with maximum color smaller than <color> and with minimum <color>
* with some tweaks
*/
const tunnelScore = (grid: Grid, color: Color, tunnel: Point[]) => {
let nColor = 0;
let nLess = 0;
let nLessLead = -1;
for (const { x, y } of tunnel) {
const c = getColor(grid, x, y);
if (!isEmpty(c)) {
if (c === color) {
nColor++;
if (nLessLead === -1) nLessLead = nLess;
} else nLess += color - c;
}
}
if (nLess === 0) return 999999;
return -nLessLead * 100 + (1 - nLess / nColor);
};
const distanceSq = (ax: number, ay: number, bx: number, by: number) =>
(ax - bx) ** 2 + (ay - by) ** 2;
/**
* get the sequence of snake to cross the tunnel
*/
const getTunnelPath = (snake0: Snake, tunnel: Point[]) => {
const chain: Snake[] = [];
let snake = snake0;
for (let i = 1; i < tunnel.length; i++) {
const dx = tunnel[i].x - getHeadX(snake);
const dy = tunnel[i].y - getHeadY(snake);
snake = nextSnake(snake, dx, dy);
chain.unshift(snake);
}
return chain;
};
/**
* assuming the grid change and the colors got deleted, update the tunnel
*/
const updateTunnel = (grid: Grid, tunnel: Point[], toDelete: Point[]) => {
trimTunnelStart(grid, tunnel);
trimTunnelEnd(grid, tunnel);
while (tunnel.length) {
const { x, y } = tunnel[0];
if (toDelete.some((p) => p.x === x && p.y === y)) {
tunnel.shift();
trimTunnelStart(grid, tunnel);
} else break;
}
while (tunnel.length) {
const { x, y } = tunnel[tunnel.length - 1];
if (toDelete.some((p) => p.x === x && p.y === y)) {
tunnel.pop();
trimTunnelEnd(grid, tunnel);
} else break;
}
};