import { readFileSync } from 'node:fs'; const input = readFileSync('input', 'utf-8'); export class InfiniteGrid { constructor() { this.ox = null; this.oy = null; this.rows = []; } get height() { return this.rows.length; } get width() { return this.rows[0]?.length ?? 0; } get(x, y) { return this.rows[y - (this.oy ?? y)]?.[x - (this.ox ?? x)]; } set(x, y, v) { this.expandToFit(x, y); const ax = x - this.ox; const ay = y - this.oy; this.rows[ay][ax] = v; } expandToFit(x, y) { if (this.ox === null) this.ox = x; if (this.oy === null) this.oy = y; const left = Math.max(0, this.ox - x); const up = Math.max(0, this.oy - y); const right = Math.max(0, (x - this.ox + 1) - this.width); const down = Math.max(0, (y - this.oy + 1) - this.height); if (up || right || down || left) { this.rows = [ ...new Array(up).fill(new Array(this.width + left + right).fill()), ...this.rows.map(row => [...new Array(left), ...row, ...new Array(right)] ), ...new Array(down).fill(new Array(this.width + left + right).fill()) ]; } this.ox -= left; this.oy -= up; } getBounds() { return { minX: this.ox, maxX: this.ox + (this.rows[0]?.length ?? 1) - 1, minY: this.oy, maxY: this.oy + (this.rows.length || 1) - 1 }; } printDefined() { console.log(this.rows .map(row => row .map(cell => cell ? '#' : '.') .join('') ) .join('\n') ); } countEmpty() { return this.rows.reduce( (total, row) => total + row.reduce( (rowTotal, cell) => rowTotal + Number(!cell), 0 ), 0 ); } } class Elf { constructor(grid) { this.grid = grid; this.pos = undefined; } moveTo(x, y) { if (this.pos) { this.grid.set(...this.pos, undefined); } this.pos = [x, y]; this.grid.set(...this.pos, this); } proposePosition(directions) { if (!this.pos) throw new Error('Elf is not on the grid'); const [x, y] = this.pos; const N = this.grid.get(x, y - 1); const NE = this.grid.get(x + 1, y - 1); const E = this.grid.get(x + 1, y); const SE = this.grid.get(x + 1, y + 1); const S = this.grid.get(x, y + 1); const SW = this.grid.get(x - 1, y + 1); const W = this.grid.get(x - 1, y); const NW = this.grid.get(x - 1, y - 1); if (!(N || NE || E || SE || S || SW || W || NW)) { // Don't move return; } for (const direction of directions) { if (direction === 'N' && !(NW || N || NE)) { return [x, y - 1]; } else if (direction === 'E' && !(NE || E || SE)) { return [x + 1, y]; } else if (direction === 'S' && !(SW || S || SE)) { return [x, y + 1]; } else if (direction === 'W' && !(NW || W || SW)) { return [x - 1, y]; } } } } const elfPositions = input .split('\n') .map(line => line.trim()) .filter(line => !!line) .flatMap((line, y) => line .split('') .map((c, x) => { if (c === '#') { return [x, y]; } }) .filter(coords => !!coords) ); const grid = new InfiniteGrid(); const elves = []; for (const pos of elfPositions) { const elf = new Elf(grid); elves.push(elf); elf.moveTo(...pos); } const directions = ['N', 'S', 'W', 'E']; let round = 0; function processRound() { const propositions = elves.flatMap(elf => { const pos = elf.proposePosition(directions); if (!pos) return []; return [{elf, x: pos[0], y: pos[1]}]; }); propositions.sort((p1, p2) => (p1.x - p2.x) || (p1.y - p2.y)); const validPropositions = propositions .reduce((groups, proposition) => { if (groups.length) { const lastGroup = groups[groups.length - 1]; const {x: lastX, y: lastY} = lastGroup[0]; if (proposition.x === lastX && proposition.y === lastY) { lastGroup.push(proposition); return groups; } } groups.push([proposition]); return groups; }, []) .filter(group => group.length === 1) .flat(); const moved = validPropositions.length; for (const proposition of validPropositions) { proposition.elf.moveTo(proposition.x, proposition.y); } // Reorder directions directions.push(directions.shift()); round++; return moved; } do { processRound(); } while (round <= 10) console.log('The number of empty ground tiles is:', grid.countEmpty()); while (processRound() !== 0) {}; console.log('The number of rounds to completion is:', round);