112 lines
3.2 KiB
Python
Executable file
112 lines
3.2 KiB
Python
Executable file
#!/usr/bin/env python
|
|
|
|
import itertools
|
|
import math
|
|
import sys
|
|
from typing import Iterator, List, NamedTuple
|
|
|
|
|
|
class Point(NamedTuple):
|
|
x: int
|
|
y: int
|
|
|
|
|
|
class Probe(NamedTuple):
|
|
position: Point
|
|
velocity: Point
|
|
|
|
|
|
class Area(NamedTuple):
|
|
min: Point
|
|
max: Point
|
|
|
|
|
|
def solve(input: List[str]) -> int:
|
|
def parse(line: str) -> Area:
|
|
x_range = line.split("x=")[1].split(",")[0]
|
|
y_range = line.split("y=")[1]
|
|
|
|
min_x, max_x = map(int, x_range.split(".."))
|
|
min_y, max_y = map(int, y_range.split(".."))
|
|
|
|
# Sanity check
|
|
assert min_x <= max_x
|
|
assert min_y <= max_y
|
|
|
|
return Area(Point(min_x, min_y), Point(max_x, max_y))
|
|
|
|
def trajectory(p: Probe) -> Iterator[Probe]:
|
|
def step(p: Probe) -> Probe:
|
|
def drag(x: int) -> int:
|
|
if x < 0:
|
|
return x + 1
|
|
if x > 0:
|
|
return x - 1
|
|
return 0
|
|
|
|
def gravity(y: int) -> int:
|
|
return y - 1
|
|
|
|
pos, vel = p
|
|
|
|
new_pos = Point(pos.x + vel.x, pos.y + vel.y)
|
|
new_vel = Point(drag(vel.x), gravity(vel.y))
|
|
|
|
return Probe(new_pos, new_vel)
|
|
|
|
while True:
|
|
yield (p := step(p))
|
|
|
|
def hits_target(probe: Probe, area: Area) -> bool:
|
|
# Too lazy to find an actual good condition on this loop, early break is enough
|
|
for p in trajectory(probe):
|
|
x, y = p.position
|
|
# Early exit when we cannot possibly get to the area
|
|
if y < area.min.y and p.velocity.y <= 0:
|
|
break
|
|
if x < area.min.x and p.velocity.x <= 0:
|
|
break
|
|
if x > area.max.x and p.velocity.x >= 0:
|
|
break
|
|
# Keep going if we're not in bounds
|
|
if x < area.min.x or x > area.max.x:
|
|
continue
|
|
if y < area.min.y or y > area.max.y:
|
|
continue
|
|
# We are in the area
|
|
return True
|
|
return False
|
|
|
|
def find_velocities(area: Area) -> Iterator[Point]:
|
|
position = Point(0, 0)
|
|
assert area.min.y < 0 # Sanity check, due to lower bound in loop
|
|
|
|
# Can't overshoot after a single step
|
|
for vx in range(0, area.max.x + 1):
|
|
# Can't overshoot after a single step, symmetric velocity when coming down
|
|
for vy in range(area.min.y, abs(area.min.y) + 1):
|
|
velocity = Point(vx, vy)
|
|
if hits_target(Probe(position, velocity), area):
|
|
yield velocity
|
|
|
|
def highest_point(velocity: Point) -> Point:
|
|
# When the y velocity is negative, the height can only go down
|
|
of_interest = itertools.takewhile(
|
|
lambda p: p.velocity.y >= 0, trajectory(Probe(Point(0, 0), velocity))
|
|
)
|
|
points = [p.position for p in of_interest]
|
|
return max(points, key=lambda p: p.y, default=Point(0, 0))
|
|
|
|
target_area = parse(input[0])
|
|
velocities = set(find_velocities(target_area))
|
|
return max(highest_point(v).y for v in velocities)
|
|
|
|
|
|
def main() -> None:
|
|
input = [line.strip() for line in sys.stdin.readlines()]
|
|
print(solve(input))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|