advent of code 2025 in ts and nix
1let
2 input = builtins.readFile ../../shared/07/input.txt;
3 lines = builtins.filter (s: builtins.isString s && s != "") (builtins.split "\n" input);
4
5 # Parse grid into 2D array of characters
6 grid = builtins.map (line: builtins.genList (i: builtins.substring i 1 line) (builtins.stringLength line)) lines;
7
8 rows = builtins.length grid;
9 cols = builtins.length (builtins.head grid);
10
11 # Find starting position
12 findStart = grid:
13 let
14 findInRow = rowIdx: row:
15 let
16 colIdx = builtins.foldl' (acc: i:
17 if acc != null then acc
18 else if builtins.elemAt row i == "S" then i
19 else null
20 ) null (builtins.genList (i: i) (builtins.length row));
21 in if colIdx != null then { r = rowIdx; c = colIdx; } else null;
22
23 result = builtins.foldl' (acc: i:
24 if acc != null then acc
25 else findInRow i (builtins.elemAt grid i)
26 ) null (builtins.genList (i: i) (builtins.length grid));
27 in result;
28
29 startPos = findStart grid;
30
31 # Check if position is in bounds
32 inBounds = r: c: r >= 0 && r < rows && c >= 0 && c < cols;
33
34 # Get cell at position
35 getCell = r: c:
36 if inBounds r c
37 then builtins.elemAt (builtins.elemAt grid r) c
38 else ".";
39
40 # Part 1: Count total splits
41 part1 =
42 let
43 # Process one step of beams
44 stepBeams = state:
45 let
46 beams = state.beams;
47 nextBeams = builtins.concatMap (beam:
48 let
49 r = beam.r;
50 c = beam.c;
51 nr = r + 1;
52 nc = c;
53 in
54 if !inBounds nr nc then []
55 else
56 let cell = getCell nr nc;
57 in
58 if cell == "." || cell == "S" then [{ r = nr; c = nc; }]
59 else if cell == "^" then
60 # Split into left and right
61 let
62 left = if inBounds nr (nc - 1) then [{ r = nr; c = nc - 1; }] else [];
63 right = if inBounds nr (nc + 1) then [{ r = nr; c = nc + 1; }] else [];
64 in left ++ right
65 else [{ r = nr; c = nc; }]
66 ) beams;
67
68 # Count splits in this step
69 splits = builtins.foldl' (acc: beam:
70 let
71 r = beam.r;
72 c = beam.c;
73 nr = r + 1;
74 nc = c;
75 cell = if inBounds nr nc then getCell nr nc else ".";
76 in if cell == "^" then acc + 1 else acc
77 ) 0 beams;
78
79 # Remove duplicates using string keys
80 uniqueBeams =
81 let
82 keysSet = builtins.foldl' (acc: beam:
83 acc // { "${toString beam.r},${toString beam.c}" = beam; }
84 ) {} nextBeams;
85 in builtins.attrValues keysSet;
86 in
87 {
88 beams = uniqueBeams;
89 totalSplits = state.totalSplits + splits;
90 };
91
92 # Run simulation until no beams left
93 simulate = state:
94 if builtins.length state.beams == 0
95 then state.totalSplits
96 else simulate (stepBeams state);
97
98 initialState = {
99 beams = [{ r = startPos.r; c = startPos.c; }];
100 totalSplits = 0;
101 };
102 in simulate initialState;
103
104 # Part 2: Count total timelines
105 part2 =
106 let
107 # Helper to parse "r,c" key
108 parseKey = key:
109 let
110 parts = builtins.match "([0-9]+),([0-9]+)" key;
111 in {
112 r = builtins.fromJSON (builtins.head parts);
113 c = builtins.fromJSON (builtins.elemAt parts 1);
114 };
115
116 # Process one step with timeline counts
117 stepTimelines = states:
118 let
119 # For each position with count, move forward
120 nextStates = builtins.foldl' (acc: key:
121 let
122 count = builtins.getAttr key states;
123 pos = parseKey key;
124 r = pos.r;
125 c = pos.c;
126 nr = r + 1;
127 nc = c;
128 in
129 if !inBounds nr nc then acc
130 else
131 let cell = getCell nr nc;
132 in
133 if cell == "." || cell == "S" then
134 # Continue straight
135 let nkey = "${toString nr},${toString nc}";
136 in acc // { ${nkey} = (acc.${nkey} or 0) + count; }
137 else if cell == "^" then
138 # Split into two timelines
139 let
140 left = if inBounds nr (nc - 1) then
141 let lkey = "${toString nr},${toString (nc - 1)}";
142 in { ${lkey} = (acc.${lkey} or 0) + count; }
143 else {};
144 right = if inBounds nr (nc + 1) then
145 let rkey = "${toString nr},${toString (nc + 1)}";
146 in { ${rkey} = (acc.${rkey} or 0) + count; }
147 else {};
148 in acc // left // right
149 else
150 # Continue straight
151 let nkey = "${toString nr},${toString nc}";
152 in acc // { ${nkey} = (acc.${nkey} or 0) + count; }
153 ) {} (builtins.attrNames states);
154 in nextStates;
155
156 # Run simulation for all rows
157 simulate = states: step:
158 let
159 nextStates = if step >= rows then {} else stepTimelines states;
160 hasStates = builtins.length (builtins.attrNames nextStates) > 0;
161 in
162 if step >= rows || !hasStates
163 then states # Return last valid states
164 else simulate nextStates (step + 1);
165
166 initialStates = { "${toString startPos.r},${toString startPos.c}" = 1; };
167 finalStates = simulate initialStates 0;
168
169 # Sum all timeline counts
170 totalTimelines = builtins.foldl' (acc: key:
171 acc + builtins.getAttr key finalStates
172 ) 0 (builtins.attrNames finalStates);
173 in totalTimelines;
174
175in {
176 inherit part1 part2;
177}