My attempts to work through the 2021 Advent of Code problems.

```commit 3f6cc45aa78990392a25e6dc7ff69e6b5070a82c
parent a13d83511102bf95c1f2fe307a917d1a87fdd3a0
Date:   Tue, 21 Dec 2021 12:38:10 -0500

Solution to day 21, part 1

Diffstat:
```
```1 file changed, 77 insertions(+), 0 deletions(-)
diff --git a/day21_part1.py b/day21_part1.py
@@ -0,0 +1,77 @@
+#!/usr/bin/env python
+"""Advent of Code 2021, day 21 (part 1): Dirac Dice
+Simulating an Advent of Code board game (which is deterministic)"""
+
+# I love how AoC zero indexes things that don't matter (like scanner numbers in
+# day 19), but not when it does (like when we're working with modular
+# arithmetic today).
+#
+# This is a total inelegant hack job; I want to rush through part 1 and see
+# what part 2 has in store for me. If I hate go play using the same rules to a
+# much higher score, I could leverage the fact that the landing positions seem
+# to go through cycles. If I need to deal with random dice, I'll have to do
+# something totally different. So rather than overthink things, I'm just wasing
+# CPU cycles and saving my own time.
+
+from typing import Tuple, cast
+from utils import get_puzzle_input
+
+EXAMPLE_INPUT = \
+"""Player 1 starting position: 4
+Player 2 starting position: 8
+"""
+
+def parse_input(input_string: str) -> Tuple[int, int]:
+    """Given the puzzle input, return a tuple containing player 1 and player
+    2's starting positions, respectively"""
+    # Hacky, leans on the number being either one digit padded with a space or
+    # two digits
+    return cast(Tuple[int, int],
+                tuple(int(l[-2:]) for l in input_string.split('\n')[0:2]))
+
+def simulate_game(player_1_start_position: int,
+                  player_2_start_position: int,
+                  final_score: int) -> Tuple[int, int, int]:
+    """Run a game until either player crosses the `final_score` threshold, then
+    return a tuple containing player 1's score, player 2's score, and the
+    number of die rolls"""
+    # Just hacking this one together to get through, it's been a long day.
+    die_rolls = 0
+    player_1_score = 0
+    player_2_score = 0
+    player_1_position = player_1_start_position
+    player_2_position = player_2_start_position
+    next_roll = 1
+    while max(player_1_score, player_2_score) < final_score:
+        for _ in range(3):
+            player_1_position += next_roll
+            # 100 -> 1 -> 2 ...
+            next_roll = next_roll % 100 + 1
+            die_rolls += 1
+        player_1_position = 1 + (player_1_position - 1) % 10
+        player_1_score += player_1_position
+        if player_1_score >= final_score:
+            break
+        for _ in range(3):
+            player_2_position += next_roll
+            next_roll = next_roll % 100 + 1
+            die_rolls += 1
+        player_2_position = 1 + (player_2_position - 1) % 10
+        player_2_score += player_2_position
+    return (player_1_score, player_2_score, die_rolls)
+
+def solve_puzzle(input_string: str) -> int:
+    """Return the numeric solution to the puzzle"""
+    player_1_score, player_2_score, die_rolls = simulate_game(
+        *(parse_input(input_string) + (1000,))
+    )
+    return min(player_1_score, player_2_score) * die_rolls
+
+def main() -> None:
+    """Run when the file is called as a script"""
+    assert solve_puzzle(EXAMPLE_INPUT) == 739785
+    print("Product of losing score and number of die rolls:""",
+          solve_puzzle(get_puzzle_input(21)))
+
+if __name__ == "__main__":
+    main()
```