You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
200 lines
4.5 KiB
200 lines
4.5 KiB
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);
|
|
|