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

```commit 805a0082908f673dccd443df9cea48490d59899e
parent 95669e57118d046bdd93359d61c8749f7a65ca83
Date:   Fri, 17 Dec 2021 12:52:32 -0500

Solution for day 17

Diffstat:
```
```1 file changed, 98 insertions(+), 0 deletions(-)
diff --git a/day17_part2.py b/day17_part2.py
@@ -0,0 +1,98 @@
+#!/usr/bin/env python
+"""Advent of Code 2021, day 17 (part 2): Trick Shot
+Find the starting x and y velocities that end inside a target area"""
+
+# No part 1 file for today; I solved it with my phone calculator while I was
+# feeding my baby. Once I realized that any positive y velocity of _n_ would
+# have a velocity of -n when it returned to the vertical position y=0, it was
+# easy to work out without code.
+#
+# I am coding part 2, but it seems like a simple problem so late in the AoC. I
+# will admit that I made one naive assumption that was incorrect: since the
+# updating process for x and y are independent, I thought that x and y would be
+# independent for the solution, but I didn't consider that both the x and y
+# position need to be inside the target area _on the same step_. Oops.
+#
+# The initial x velocity is bounded by that which will stop moving forward
+# immediately upon reaching the trench, and that which finishes its first step
+# at the far end of the trench. The initial y velocity is bounded by the one
+# that solved part 1, and that which finishes its first step at the low end of
+# the trench. I'll use more lax bounds than this because it's so quick it
+# doesn't hurt.
+
+from typing import List, Tuple
+import re
+from utils import get_puzzle_input
+
+EXAMPLE_INPUT = \
+"""target area: x=20..30, y=-10..-5
+"""
+
+def parse_input(input_string: str) -> Tuple[Tuple[int, int], Tuple[int, int]]:
+    """Given the puzzle input, return the min and max of x and the min and max
+    of y as tuples"""
+    match = re.match(r"^target area: x=(?P<x1>\d+)..(?P<x2>\d+), " + \
+                     r"y=(?P<y1>-?\d+)..(?P<y2>-?\d+)",
+                     input_string)
+    if match is None:
+        raise RuntimeError("improperly foratted input string")
+
+    return (tuple(sorted([int(x) for x in match.group('x1', 'x2')])),
+            tuple(sorted([int(x) for x in match.group('y1', 'y2')])))
+
+def update_x(x_position: int, x_velocity: int) -> Tuple[int, int]:
+    """Given an x position and velocity, return an updated position and
+    velocity"""
+    # x position increases by velocity and the velocity increments or
+    # decrements by 1 toward 0. This code is a little hacky, but the expression
+    # `(n>0)*2-1` evaluates to 1 if n is positive, and -1 if it's negative,
+    # providing a concise `sign` function.
+    return (x_position + x_velocity,
+            ((x_velocity > 0) * 2 - 1) * max(0, abs(x_velocity) - 1))
+
+def update_y(y_position: int, y_velocity: int) -> Tuple[int, int]:
+    """Given a y position and velocity, return an updated position and
+    velocity"""
+    # y position increases by velocity and the velocity decreases by 1
+    return (y_position + y_velocity, y_velocity - 1)
+
+# XXX: These are pretty hacky in that they assume that x will be entirely
+# greater than 0 and y will be entirely less than 0.
+
+def test_xy(x_velocity: int, y_velocity: int,
+            x_bounds: Tuple[int, int], y_bounds: Tuple[int, int]) -> bool:
+    """Return True iff a probe with initial starting position of (0, 0), and
+    velocity vector of (`x_velocity`, `y_velocity`) would finish any step
+    inside the area bounded by `x_bound` and `y_bounds`."""
+    x_position, y_position = (0, 0)
+    while x_position <= x_bounds and y_position >= y_bounds:
+        if x_bounds <= x_position <= x_bounds and \
+                y_bounds <= y_position <= y_bounds:
+            return True
+        x_position, x_velocity = update_x(x_position, x_velocity)
+        y_position, y_velocity = update_y(y_position, y_velocity)
+    return False
+
+def find_xy(x_bounds: Tuple[int, int],
+            y_bounds: Tuple[int, int]) -> List[Tuple[int, int]]:
+    """Given the specified boundaries, return a list of all the starting x and
+    y velocities that would finish a step inside of them"""
+    in_bounds_velocities = []
+    for x_velocity in range(x_bounds+1):
+        for y_velocity in range(y_bounds, -y_bounds+1):
+            if test_xy(x_velocity, y_velocity, x_bounds, y_bounds):
+                in_bounds_velocities.append((x_velocity, y_velocity))
+    return in_bounds_velocities
+
+def solve_puzzle(input_string: str) -> int:
+    """Return the numeric solution to the puzzle"""
+    return len(find_xy(*parse_input(input_string)))
+
+def main() -> None:
+    """Run when the file is called as a script"""
+    assert solve_puzzle(EXAMPLE_INPUT) == 112
+    print("Total distinct initial velocities:",
+          solve_puzzle(get_puzzle_input(17)))
+
+if __name__ == "__main__":
+    main()
```