advent of code 2025 in ts and nix

feat: add day 07

dunkirk.sh 2f5abb06 60053ca0

verified
Changed files
+290
nix
ts
+177
nix/07/solution.nix
···
+
let
+
input = builtins.readFile ../../shared/07/input.txt;
+
lines = builtins.filter (s: builtins.isString s && s != "") (builtins.split "\n" input);
+
+
# Parse grid into 2D array of characters
+
grid = builtins.map (line: builtins.genList (i: builtins.substring i 1 line) (builtins.stringLength line)) lines;
+
+
rows = builtins.length grid;
+
cols = builtins.length (builtins.head grid);
+
+
# Find starting position
+
findStart = grid:
+
let
+
findInRow = rowIdx: row:
+
let
+
colIdx = builtins.foldl' (acc: i:
+
if acc != null then acc
+
else if builtins.elemAt row i == "S" then i
+
else null
+
) null (builtins.genList (i: i) (builtins.length row));
+
in if colIdx != null then { r = rowIdx; c = colIdx; } else null;
+
+
result = builtins.foldl' (acc: i:
+
if acc != null then acc
+
else findInRow i (builtins.elemAt grid i)
+
) null (builtins.genList (i: i) (builtins.length grid));
+
in result;
+
+
startPos = findStart grid;
+
+
# Check if position is in bounds
+
inBounds = r: c: r >= 0 && r < rows && c >= 0 && c < cols;
+
+
# Get cell at position
+
getCell = r: c:
+
if inBounds r c
+
then builtins.elemAt (builtins.elemAt grid r) c
+
else ".";
+
+
# Part 1: Count total splits
+
part1 =
+
let
+
# Process one step of beams
+
stepBeams = state:
+
let
+
beams = state.beams;
+
nextBeams = builtins.concatMap (beam:
+
let
+
r = beam.r;
+
c = beam.c;
+
nr = r + 1;
+
nc = c;
+
in
+
if !inBounds nr nc then []
+
else
+
let cell = getCell nr nc;
+
in
+
if cell == "." || cell == "S" then [{ r = nr; c = nc; }]
+
else if cell == "^" then
+
# Split into left and right
+
let
+
left = if inBounds nr (nc - 1) then [{ r = nr; c = nc - 1; }] else [];
+
right = if inBounds nr (nc + 1) then [{ r = nr; c = nc + 1; }] else [];
+
in left ++ right
+
else [{ r = nr; c = nc; }]
+
) beams;
+
+
# Count splits in this step
+
splits = builtins.foldl' (acc: beam:
+
let
+
r = beam.r;
+
c = beam.c;
+
nr = r + 1;
+
nc = c;
+
cell = if inBounds nr nc then getCell nr nc else ".";
+
in if cell == "^" then acc + 1 else acc
+
) 0 beams;
+
+
# Remove duplicates using string keys
+
uniqueBeams =
+
let
+
keysSet = builtins.foldl' (acc: beam:
+
acc // { "${toString beam.r},${toString beam.c}" = beam; }
+
) {} nextBeams;
+
in builtins.attrValues keysSet;
+
in
+
{
+
beams = uniqueBeams;
+
totalSplits = state.totalSplits + splits;
+
};
+
+
# Run simulation until no beams left
+
simulate = state:
+
if builtins.length state.beams == 0
+
then state.totalSplits
+
else simulate (stepBeams state);
+
+
initialState = {
+
beams = [{ r = startPos.r; c = startPos.c; }];
+
totalSplits = 0;
+
};
+
in simulate initialState;
+
+
# Part 2: Count total timelines
+
part2 =
+
let
+
# Helper to parse "r,c" key
+
parseKey = key:
+
let
+
parts = builtins.match "([0-9]+),([0-9]+)" key;
+
in {
+
r = builtins.fromJSON (builtins.head parts);
+
c = builtins.fromJSON (builtins.elemAt parts 1);
+
};
+
+
# Process one step with timeline counts
+
stepTimelines = states:
+
let
+
# For each position with count, move forward
+
nextStates = builtins.foldl' (acc: key:
+
let
+
count = builtins.getAttr key states;
+
pos = parseKey key;
+
r = pos.r;
+
c = pos.c;
+
nr = r + 1;
+
nc = c;
+
in
+
if !inBounds nr nc then acc
+
else
+
let cell = getCell nr nc;
+
in
+
if cell == "." || cell == "S" then
+
# Continue straight
+
let nkey = "${toString nr},${toString nc}";
+
in acc // { ${nkey} = (acc.${nkey} or 0) + count; }
+
else if cell == "^" then
+
# Split into two timelines
+
let
+
left = if inBounds nr (nc - 1) then
+
let lkey = "${toString nr},${toString (nc - 1)}";
+
in { ${lkey} = (acc.${lkey} or 0) + count; }
+
else {};
+
right = if inBounds nr (nc + 1) then
+
let rkey = "${toString nr},${toString (nc + 1)}";
+
in { ${rkey} = (acc.${rkey} or 0) + count; }
+
else {};
+
in acc // left // right
+
else
+
# Continue straight
+
let nkey = "${toString nr},${toString nc}";
+
in acc // { ${nkey} = (acc.${nkey} or 0) + count; }
+
) {} (builtins.attrNames states);
+
in nextStates;
+
+
# Run simulation for all rows
+
simulate = states: step:
+
let
+
nextStates = if step >= rows then {} else stepTimelines states;
+
hasStates = builtins.length (builtins.attrNames nextStates) > 0;
+
in
+
if step >= rows || !hasStates
+
then states # Return last valid states
+
else simulate nextStates (step + 1);
+
+
initialStates = { "${toString startPos.r},${toString startPos.c}" = 1; };
+
finalStates = simulate initialStates 0;
+
+
# Sum all timeline counts
+
totalTimelines = builtins.foldl' (acc: key:
+
acc + builtins.getAttr key finalStates
+
) 0 (builtins.attrNames finalStates);
+
in totalTimelines;
+
+
in {
+
inherit part1 part2;
+
}
+113
ts/07/index.ts
···
+
const file = await Bun.file("../../shared/07/input.txt").text();
+
const board: string[][] = file
+
.trimEnd()
+
.split("\n")
+
.map((line) => line.split(""));
+
+
type Coord = { r: number; c: number };
+
+
// Find S
+
const S: Coord | null = (() => {
+
for (let r = 0; r < board.length; r++) {
+
const c = board[r]?.indexOf("S") as number;
+
if (c !== -1) return { r, c };
+
}
+
return null;
+
})();
+
+
if (!S) throw "No start position found";
+
+
const inBounds = (r: number, c: number) =>
+
r >= 0 && r < board.length && c >= 0 && c < board[0].length;
+
+
// Part 1: Count unique beam positions and splits
+
(() => {
+
type BeamPosition = [number, number];
+
let activeBeams: BeamPosition[] = [[S.r, S.c]];
+
let totalSplits = 0;
+
+
while (activeBeams.length > 0) {
+
const nextActive: BeamPosition[] = [];
+
+
for (const [r, c] of activeBeams) {
+
const nr = r + 1;
+
const nc = c;
+
+
if (!inBounds(nr, nc)) continue;
+
+
const cell = board[nr][nc];
+
+
if (cell === ".") {
+
nextActive.push([nr, nc]);
+
} else if (cell === "^") {
+
totalSplits++;
+
// Split into left and right beams
+
const leftC = nc - 1;
+
const rightC = nc + 1;
+
if (inBounds(nr, leftC)) nextActive.push([nr, leftC]);
+
if (inBounds(nr, rightC)) nextActive.push([nr, rightC]);
+
} else {
+
// 'S' or other chars - continue straight
+
nextActive.push([nr, nc]);
+
}
+
}
+
+
// Remove duplicates
+
const uniqueBeams = [
+
...new Set(nextActive.map(([r, c]) => `${r},${c}`)),
+
].map((s) => s.split(",").map(Number) as BeamPosition);
+
+
activeBeams = uniqueBeams;
+
}
+
+
console.log("part 1:", totalSplits);
+
})();
+
+
// Part 2: Count total timelines (beams multiply through splitters)
+
(() => {
+
let currentStates: Record<string, number> = { [`${S.r},${S.c}`]: 1 };
+
+
for (let step = 0; step < board.length; step++) {
+
const nextStates: Record<string, number> = {};
+
+
for (const [key, count] of Object.entries(currentStates)) {
+
const [r, c] = key.split(",").map(Number);
+
const nr = r + 1;
+
+
if (nr >= board.length) continue;
+
if (!inBounds(nr, c)) continue;
+
+
const cell = board[nr][c];
+
+
if (cell === ".") {
+
const nkey = `${nr},${c}`;
+
nextStates[nkey] = (nextStates[nkey] || 0) + count;
+
} else if (cell === "^") {
+
// Each timeline splits into two
+
const lc = c - 1;
+
const rc = c + 1;
+
if (inBounds(nr, lc)) {
+
const lkey = `${nr},${lc}`;
+
nextStates[lkey] = (nextStates[lkey] || 0) + count;
+
}
+
if (inBounds(nr, rc)) {
+
const rkey = `${nr},${rc}`;
+
nextStates[rkey] = (nextStates[rkey] || 0) + count;
+
}
+
} else {
+
// 'S' or other chars - continue straight
+
const nkey = `${nr},${c}`;
+
nextStates[nkey] = (nextStates[nkey] || 0) + count;
+
}
+
}
+
+
if (Object.keys(nextStates).length === 0) break;
+
currentStates = nextStates;
+
}
+
+
const totalTimelines = Object.values(currentStates).reduce(
+
(a, b) => a + b,
+
0,
+
);
+
console.log("part 2:", totalTimelines);
+
})();