diff --git a/examples/aoc2024/day21/part1.jou b/examples/aoc2024/day21/part1.jou
new file mode 100644
index 00000000..15098a10
--- /dev/null
+++ b/examples/aoc2024/day21/part1.jou
@@ -0,0 +1,187 @@
+import "stdlib/io.jou"
+import "stdlib/ascii.jou"
+import "stdlib/math.jou"
+import "stdlib/mem.jou"
+import "stdlib/str.jou"
+
+
+class List:
+ ptr: byte[100]*
+ length: int
+ alloc: int
+
+ def append(self, string: byte*) -> None:
+ if self->length == self->alloc:
+ if self->alloc == 0:
+ self->alloc = 4
+ else:
+ self->alloc *= 2
+ self->ptr = realloc(self->ptr, sizeof(self->ptr[0]) * self->alloc)
+ assert self->ptr != NULL
+
+ assert strlen(string) < sizeof(self->ptr[0])
+ strcpy(self->ptr[self->length++], string)
+
+ def dedup(self) -> None:
+ i = 0
+ while i < self->length:
+ exists_before = False
+ for k = 0; k < i; k++:
+ if strcmp(self->ptr[k], self->ptr[i]) == 0:
+ exists_before = True
+ break
+ if exists_before:
+ self->ptr[k] = self->ptr[--self->length]
+ else:
+ i++
+
+
+class KeyPad:
+ rows: byte[3][4]
+
+ def find_button(self, button_label: byte) -> int[2]:
+ for x = 0; x < 3; x++:
+ for y = 0; y < 4; y++:
+ if self->rows[y][x] == button_label:
+ return [x, y]
+ assert False
+
+ def goes_to_blank(self, directions: byte*) -> bool:
+ pos = self->find_button('A')
+ x = pos[0]
+ y = pos[1]
+
+ for p = directions; *p != '\0'; p++:
+ if *p == '<':
+ x--
+ elif *p == '>':
+ x++
+ elif *p == '^':
+ y--
+ elif *p == 'v':
+ y++
+ else:
+ assert *p == 'A'
+ continue
+
+ assert 0 <= x and x < 3
+ assert 0 <= y and y < 4
+ if self->rows[y][x] == ' ':
+ return True
+
+ return False
+
+ def get_presses(self, what_to_write: byte*) -> List:
+ result = List{}
+ result.append("")
+
+ for p = &what_to_write[0]; *p != '\0'; p++:
+ if p == &what_to_write[0]:
+ prev = 'A'
+ else:
+ prev = p[-1]
+
+ pos1 = self->find_button(prev)
+ pos2 = self->find_button(*p)
+
+ dx = pos2[0] - pos1[0]
+ dy = pos2[1] - pos1[1]
+
+ h: byte[100] = ""
+ v: byte[100] = ""
+ if dx < 0:
+ memset(&h, '<', abs(dx))
+ else:
+ memset(&h, '>', dx)
+ if dy < 0:
+ memset(&v, '^', abs(dy))
+ else:
+ memset(&v, 'v', dy)
+
+ result2 = List{}
+ for i = 0; i < result.length; i++:
+ # Do horizontal and vertical moves in both possible orders. This
+ # may affect other robots, because their arms move less if we use
+ # consecutive presses in the same direction.
+ assert strlen(result.ptr[i]) + strlen(h) + strlen(v) + strlen("A") < 100
+
+ h_then_v: byte[100] = ""
+ strcat(h_then_v, result.ptr[i])
+ strcat(h_then_v, h)
+ strcat(h_then_v, v)
+ strcat(h_then_v, "A")
+
+ v_then_h: byte[100] = ""
+ strcat(v_then_h, result.ptr[i])
+ strcat(v_then_h, v)
+ strcat(v_then_h, h)
+ strcat(v_then_h, "A")
+
+ if not self->goes_to_blank(h_then_v):
+ result2.append(h_then_v)
+ if not self->goes_to_blank(v_then_h):
+ result2.append(v_then_h)
+
+ free(result.ptr)
+ result = result2
+ result.dedup() # must be done right away so we don't run out of memory
+
+ return result
+
+ def get_presses_for_each(self, things_we_could_write: List) -> List:
+ result = List{}
+ for i = 0; i < things_we_could_write.length; i++:
+ adding = self->get_presses(things_we_could_write.ptr[i])
+ for k = 0; k < adding.length; k++:
+ result.append(adding.ptr[k])
+ free(adding.ptr)
+
+ result.dedup()
+ return result
+
+
+def main() -> int:
+ numeric_keypad = KeyPad{rows = [
+ ['7', '8', '9'],
+ ['4', '5', '6'],
+ ['1', '2', '3'],
+ [' ', '0', 'A'],
+ ]}
+
+ arrow_keypad = KeyPad{rows = [
+ [' ', '^', 'A'],
+ ['<', 'v', '>'],
+ [' ', ' ', ' '],
+ [' ', ' ', ' '],
+ ]}
+
+ f = fopen("sampleinput.txt", "r")
+ assert f != NULL
+
+ result = 0
+
+ line: byte[20]
+ while fgets(line, sizeof(line) as int, f) != NULL:
+ trim_ascii_whitespace(line)
+
+ presses = numeric_keypad.get_presses(line)
+
+ repeat = 2
+ while repeat --> 0:
+ presses2 = arrow_keypad.get_presses_for_each(presses)
+ free(presses.ptr)
+ presses = presses2
+
+ assert presses.length != 0
+
+ shortest_len = strlen(presses.ptr[0]) as int
+ for i = 1; i < presses.length; i++:
+ shortest_len = min(shortest_len, strlen(presses.ptr[i]) as int)
+ free(presses.ptr)
+
+ result += shortest_len * atoi(line)
+
+ printf("%d\n", result) # Output: 126384
+
+ fclose(f)
+ return 0
diff --git a/examples/aoc2024/day21/part2.jou b/examples/aoc2024/day21/part2.jou
new file mode 100644
index 00000000..21b05e1a
--- /dev/null
+++ b/examples/aoc2024/day21/part2.jou
@@ -0,0 +1,245 @@
+# The code looks simple, but it took many days for me to come up with this
+# simple approach :)
+
+import "stdlib/io.jou"
+import "stdlib/ascii.jou"
+import "stdlib/math.jou"
+import "stdlib/str.jou"
+import "stdlib/mem.jou"
+
+
+# arrow_keypad_table['^']['>'] = [">A", NULL]
+#
+# This means that if robot arm previously pressed ^ and we want it to press >,
+# we must move right and then press A.
+global arrow_keypad_table: byte*[2][128][128]
+
+# Locations of each character in numeric keypad
+# For example, numpad_table['7'] = [0, 0] because it's in top left corner.
+global numpad_table: int[2][128]
+
+# Cost is number of button presses by user.
+#
+# Indexed by level. Level 0 is the directional keypad where user types (cost is
+# always 1). Levels 1-25 are operated by robots, so there are 26 levels.
+global cost_cache: long[128][128][26]
+
+
+def init_tables() -> None:
+ memset(arrow_keypad_table, 0, sizeof(arrow_keypad_table))
+ memset(numpad_table, 0, sizeof(numpad_table))
+ memset(cost_cache, 0, sizeof(cost_cache))
+
+ # ^ A
+ # < v >
+ arrow_keypad_table['^']['^'] = ["A", NULL as byte*]
+ arrow_keypad_table['^']['A'] = [">A", NULL as byte*]
+ arrow_keypad_table['^']['<'] = ["v'] = ["v>A", ">vA"]
+
+ arrow_keypad_table['A']['^'] = ["'] = ["vA", NULL as byte*]
+
+ arrow_keypad_table['<']['^'] = [">^A", NULL as byte*]
+ arrow_keypad_table['<']['A'] = [">>^A", NULL as byte*]
+ arrow_keypad_table['<']['<'] = ["A", NULL as byte*]
+ arrow_keypad_table['<']['v'] = [">A", NULL as byte*]
+ arrow_keypad_table['<']['>'] = [">>A", NULL as byte*]
+
+ arrow_keypad_table['v']['^'] = ["^A", NULL as byte*]
+ arrow_keypad_table['v']['A'] = [">^A", "^>A"]
+ arrow_keypad_table['v']['<'] = ["'] = [">A", NULL as byte*]
+
+ arrow_keypad_table['>']['^'] = ["<^A", "^']['A'] = ["^A", NULL as byte*]
+ arrow_keypad_table['>']['<'] = ["<']['v'] = ["']['>'] = ["A", NULL as byte*]
+
+ # 7 8 9
+ # 4 5 6
+ # 1 2 3
+ # 0 A
+ numpad_table['7'] = [0, 0]
+ numpad_table['8'] = [1, 0]
+ numpad_table['9'] = [2, 0]
+
+ numpad_table['4'] = [0, 1]
+ numpad_table['5'] = [1, 1]
+ numpad_table['6'] = [2, 1]
+
+ numpad_table['1'] = [0, 2]
+ numpad_table['2'] = [1, 2]
+ numpad_table['3'] = [2, 2]
+
+ # nothing on the left of zero
+ numpad_table['0'] = [1, 3]
+ numpad_table['A'] = [2, 3]
+
+
+def add_dx_string(dx: int, dest: byte*) -> None:
+ offset = strlen(dest)
+ if dx >= 0:
+ memset(&dest[offset], '>', dx)
+ else:
+ memset(&dest[offset], '<', abs(dx))
+ dest[offset + abs(dx)] = '\0'
+
+
+def add_dy_string(dy: int, dest: byte*) -> None:
+ offset = strlen(dest)
+ if dy >= 0:
+ memset(&dest[offset], 'v', dy)
+ else:
+ memset(&dest[offset], '^', abs(dy))
+ dest[offset + abs(dy)] = '\0'
+
+
+# Each sequence of moves fits in 20 characters:
+# - numpad is 3x4, so each move is at most 5 characters (2 horizontal, 3 vertical)
+# - there are 4 moves
+#
+# There are at most 16 sequences of moves:
+# - start with no moves (1 empty sequence)
+# - for each move, we have in the worst case 2 different ways to do it, doubles 4 times
+#
+# I used bigger size in case there's mistake :D
+def get_numpad_move_sequences(code: byte*) -> byte[50][50]:
+ assert strlen(code) == 4
+ assert is_ascii_digit(code[0])
+ assert is_ascii_digit(code[1])
+ assert is_ascii_digit(code[2])
+ assert code[3] == 'A'
+
+ results: byte[50][50]
+ memset(results, 0, sizeof(results))
+ nresults = 1
+
+ for p = code; *p != '\0'; p++:
+ if p == code:
+ prev = 'A'
+ else:
+ prev = p[-1]
+
+ old_pos = numpad_table[prev]
+ new_pos = numpad_table[*p]
+ dx = new_pos[0] - old_pos[0]
+ dy = new_pos[1] - old_pos[1]
+
+ if dx == 0:
+ for i = 0; i < nresults; i++:
+ add_dy_string(dy, results[i])
+ elif dy == 0:
+ for i = 0; i < nresults; i++:
+ add_dx_string(dx, results[i])
+ elif old_pos[0] == 0 and new_pos[1] == 3:
+ # Can't do vertical first, would go to forbidden part
+ for i = 0; i < nresults; i++:
+ add_dx_string(dx, results[i])
+ add_dy_string(dy, results[i])
+ elif old_pos[1] == 3 and new_pos[0] == 0:
+ # Can't do horizontal first, would go to forbidden part
+ for i = 0; i < nresults; i++:
+ add_dy_string(dy, results[i])
+ add_dx_string(dx, results[i])
+ else:
+ # We can move first horizontal then vertical, or first vertical then horizontal.
+ # Duplicate results and do both.
+ assert 2 * nresults < sizeof(results) / sizeof(results[0])
+ memcpy(&results[nresults], results, sizeof(results[0]) * nresults)
+ for i = 0; i < nresults; i++:
+ add_dx_string(dx, results[i])
+ add_dy_string(dy, results[i])
+ for i = nresults; i < 2*nresults; i++:
+ add_dy_string(dy, results[i])
+ add_dx_string(dx, results[i])
+ nresults *= 2
+
+ for i = 0; i < nresults; i++:
+ strcat(results[i], "A")
+
+ return results
+
+
+# Level 0 = user
+# Level 25 = keypad closest to door
+#
+# Returns the cost (number of user presses) needed to press the given key on
+# the keypad at the level. Assumes smaller-numbered levels start at A. This
+# makes sense, because they also go to A at end of pressing the key.
+def keypad_cost(level: int, prev_key: byte, key: byte) -> long:
+ cacheptr = &cost_cache[level][prev_key][key]
+ if *cacheptr != 0:
+ return *cacheptr
+
+ if level == 0:
+ # Single key press from user
+ best_cost = 1L
+ else:
+ moves = arrow_keypad_table[prev_key][key]
+
+ best_cost = -1L
+ for i = 0; i < 2 and moves[i] != NULL; i++:
+ cost = 0L
+ prev = 'A' # robot is still aimed at activate button after previous key press
+ for p = moves[i]; *p != '\0'; p++:
+ # Press keys on smaller-numbered level
+ cost += keypad_cost(level-1, prev, *p)
+ prev = *p
+
+ if best_cost == -1 or cost < best_cost:
+ best_cost = cost
+
+ *cacheptr = best_cost
+ return best_cost
+
+
+# No caching
+def numpad_cost(code: byte*, initial_level: int) -> long:
+ moves = get_numpad_move_sequences(code)
+
+ best_cost = -1L
+ for i = 0; moves[i][0] != '\0'; i++:
+ cost = 0L
+ prev = 'A'
+ for p = &moves[i][0]; *p != '\0'; p++:
+ cost += keypad_cost(initial_level, prev, *p)
+ prev = *p
+
+ if best_cost == -1 or cost < best_cost:
+ best_cost = cost
+
+ return best_cost
+
+
+def main() -> int:
+ init_tables()
+
+ # Sample input parameters. Calculates same thing as part 1 (expected output
+ # is given on AoC website), but this optimized version also solves part 2.
+ filename = "sampleinput.txt"
+ initial_level = 2
+
+ # Actual input parameters for part 2
+ #filename = "input"
+ #initial_level = 25
+
+ f = fopen(filename, "r")
+ assert f != NULL
+
+ result = 0L
+ line: byte[20]
+ while fgets(line, sizeof(line) as int, f) != NULL:
+ trim_ascii_whitespace(line)
+ result += numpad_cost(line, initial_level) * atoi(line)
+
+ printf("%lld\n", result) # Output: 126384
+
+ fclose(f)
+ return 0
diff --git a/examples/aoc2024/day21/sampleinput.txt b/examples/aoc2024/day21/sampleinput.txt
new file mode 100644
index 00000000..4cf0c29b
--- /dev/null
+++ b/examples/aoc2024/day21/sampleinput.txt
@@ -0,0 +1,5 @@
+029A
+980A
+179A
+456A
+379A