83 lines
4.0 KiB
JavaScript
83 lines
4.0 KiB
JavaScript
import { clone, trace, rotateBoard, rotateTracks, key } from '../../src/core.js';
|
|
import { pathPoints, position, PORTS, sweepPath } from '../../src/path-math.js';
|
|
|
|
export function createState(level) {
|
|
return { ...clone(level), orientation: 0, fired: [] };
|
|
}
|
|
export function routeFor(state, tile) {
|
|
let route = trace(state, tile, state.tiles, true);
|
|
let points = pathPoints(route);
|
|
if (state.mode === 'relay') {
|
|
const index = route.steps.findIndex((s, i) => i > 0 && state.stops.some(p => p.q === s.q && p.r === s.r));
|
|
if (index >= 0) {
|
|
const stop = route.steps[index];
|
|
const center = position(stop.q, stop.r), port = PORTS[stop.enter];
|
|
points = [...pathPoints({ steps: route.steps.slice(0, index) }), { x: center.x + port.x, y: center.y + port.y }, center];
|
|
route = { ...route, status: 'stop', steps: route.steps.slice(0, index + 1), end: { q: stop.q, r: stop.r }, dir: stop.exit };
|
|
}
|
|
}
|
|
if (route.status === 'exit') {
|
|
const last = points.at(-1), port = PORTS[route.steps.at(-1).exit];
|
|
points.push({ x: last.x + port.x * 4, y: last.y + port.y * 4 });
|
|
}
|
|
const collision = sweepPath(points, state.tiles.filter(t => t.id !== tile.id).map(t => ({ ...position(t.q, t.r), id: t.id })));
|
|
if (collision) return { ...route, status: 'blocked', blocker: collision.blocker, points: collision.points };
|
|
if (route.status === 'exit' && state.mode === 'color') {
|
|
const last = route.steps.at(-1);
|
|
const gate = state.gates.find(g => g.q === last.q && g.r === last.r && g.dir === last.exit);
|
|
if (!gate || gate.color !== tile.color) return { ...route, status: 'wrong', points };
|
|
}
|
|
return { ...route, points };
|
|
}
|
|
export function act(state, action) {
|
|
if (action.type === 'rotate') {
|
|
const next = rotateBoard(state, action.direction);
|
|
next.orientation = (state.orientation + action.direction + 6) % 6;
|
|
const count = (action.direction + 6) % 6;
|
|
next.cells = next.cells.map(c => {
|
|
if (!c.alternate) return c;
|
|
let alternate = c.alternate;
|
|
for (let i = 0; i < count; i++) alternate = rotateTracks(alternate);
|
|
return { ...c, alternate };
|
|
});
|
|
return { state: next, status: 'rotate' };
|
|
}
|
|
const tile = state.tiles.find(t => t.id === action.id);
|
|
if (!tile) return { state, status: 'invalid' };
|
|
const route = routeFor(state, tile);
|
|
if (!['exit', 'stop'].includes(route.status)) return { state, ...route };
|
|
const next = clone(state);
|
|
if (route.status === 'stop') {
|
|
Object.assign(next.tiles.find(t => t.id === tile.id), route.end, { dir: route.dir });
|
|
} else {
|
|
next.tiles = next.tiles.filter(t => t.id !== tile.id);
|
|
if (state.mode === 'switch' && tile.trigger) {
|
|
next.cells = next.cells.map(c => c.switchId === tile.trigger ? { ...c, tracks: c.alternate, alternate: c.tracks } : c);
|
|
next.fired.push(tile.trigger);
|
|
}
|
|
}
|
|
return { state: next, ...route };
|
|
}
|
|
export function signature(state) {
|
|
return `${state.orientation}|${state.fired.join(',')}|${state.tiles.map(t => `${t.id}:${key(t.q, t.r)}:${t.dir}`).join(';')}`;
|
|
}
|
|
// Unlike the main campaign, departures can change tracks and stops change occupancy.
|
|
// Search every action; never greedily drain all currently available pieces.
|
|
export function solve(state, limit = 12000) {
|
|
const queue = [{ state, actions: [] }], seen = new Set([signature(state)]);
|
|
for (let head = 0; head < queue.length && head < limit; head++) {
|
|
const node = queue[head];
|
|
if (!node.state.tiles.length) return { solved: true, actions: node.actions, states: head + 1 };
|
|
const actions = [...node.state.tiles.map(t => ({ type: 'tap', id: t.id })), { type: 'rotate', direction: 1 }, { type: 'rotate', direction: -1 }];
|
|
for (const action of actions) {
|
|
const result = act(node.state, action);
|
|
if (result.state === node.state) continue;
|
|
const id = signature(result.state);
|
|
if (seen.has(id)) continue;
|
|
seen.add(id);
|
|
queue.push({ state: result.state, actions: [...node.actions, action] });
|
|
}
|
|
}
|
|
return { solved: false, exhausted: queue.length > limit, actions: [] };
|
|
}
|