def part_1(steps=1000): bodies = [ Body(Point3(-1, -4, 0)), Body(Point3(4, 7, -1)), Body(Point3(-14, -10, 9)), Body(Point3(1, 2, 17)), ] last(simulate(bodies, steps_limit=steps)) total_energy = sum(b.energy for b in bodies) print(f"part 1: total energy after {steps} steps is {total_energy}")
def part_1(starting_number: str, iterations: int = 40) -> str: """ Today, the Elves are playing a game called **look-and-say**. They take turns making sequences by reading aloud the previous sequence and using that reading as the next sequence. For example, `211` is read as "one two, two ones", which becomes `1221` (1x `2`, 2x `1`s). >>> las('211') '1221' Look-and-say sequences are generated iteratively, using the previous value as input for the next step. For each step, take the previous value, and replace each run of digits (like `111`) with the number of digits (`3`) followed by the digit itself (`1`). For example: >>> list(las_sequence('1', iterations=5)) ['1', '11', '21', '1211', '111221', '312211'] Starting with the digits in your puzzle input, apply this process **40 times**. What is the length of the result? >>> part_1('1') # doctest: +ELLIPSIS part 1: after 40 iterations, sequence has length 82350 '111312211312111322212321121113121112131112132112311321322112111312212321121113122112131...' """ seq = last(las_sequence(starting_number, iterations)) result = len(seq) print( f"part 1: after {iterations} iterations, sequence has length {result}") return seq
def mark(self, *numbers: int, stop_at_first_win: bool = True) -> Board | None: winning_boards = (board for number in numbers for board in self if board.mark(number)) if stop_at_first_win: return next(winning_boards, None) else: return last(winning_boards, None)
def part_2(starting_number: str, iterations: int = 50) -> str: """ Now, starting again with the digits in your puzzle input, apply this process **50 times**. What is the length of the new result? >>> part_2('1') # doctest: +ELLIPSIS part 2: after 50 iterations, sequence has length 1166642 '311311222113111231133211121312211231131112311211133112111312211213211312111322211231131...' """ seq = last(las_sequence(starting_number, iterations)) result = len(seq) more = " more" if len(starting_number) >= 100 else "" print( f"part 2: after {iterations}{more} iterations, sequence has length {result}" ) return seq
def test_1b(): bodies = [ Body(Point3(-8,-10, 0)), Body(Point3( 5, 5,10)), Body(Point3( 2, -7, 3)), Body(Point3( 9, -8,-3)), ] last_state = last(simulate(bodies, steps_limit=100)) assert last_state == ( (Point3( 8, -12, -9), Vector3(-7, 3, 0)), (Point3( 13, 16, -3), Vector3( 3, -11, -5)), (Point3(-29, -11, -1), Vector3(-3, 7, 4)), (Point3( 16, -13, 23), Vector3( 7, 1, 1)), ) assert sum(b.energy for b in bodies) == 1940 print("passed test_1b")
def part_1(instructions: Iterable['Instr'] | str) -> int: """ You're airdropped near **Easter Bunny Headquarters** in a city somewhere. "Near", unfortunately, is as close as you can get - the instructions on the Easter Bunny Recruiting Document the Elves intercepted start here, and nobody had time to work them out further. The Document indicates that you should start at the given coordinates (where you just landed) and face North. Then, follow the provided sequence: either turn left (`L`) or right (`R`) 90 degrees, then walk forward the given number of blocks, ending at a new intersection. There's no time to follow such ridiculous instructions on foot, though, so you take a moment and work out the destination. Given that you can only walk on the street grid of the city, how far is the shortest path to the destination? For example: - Following `R2, L3` leaves you `2` blocks East and `3` blocks North, or `5` blocks away: >>> last(walk('R2, L3')) (2, -3) >>> distance_from_origin(_) 5 - `R2, R2, R2` leaves you `2` blocks due South of your starting position, or `2` blocks away: >>> last(walk('R2, R2, R2')) (0, 2) **How many blocks away is Easter Bunny HQ?** >>> part_1('R5, L5, R5, R3') part 1: HQ is 12 blocks away at (10, -2) 12 """ hq_pos = last(walk(instructions)) distance = distance_from_origin(hq_pos) print(f"part 1: HQ is {distance} blocks away at {hq_pos}") return distance
def part_1(step_size: int, count: int = 2017) -> int: """ Suddenly, whirling in the distance, you notice what looks like a massive, pixelated hurricane: a deadly [spinlock](https://en.wikipedia.org/wiki/Spinlock). This spinlock isn't just consuming computing power, but memory, too; vast, digital mountains are being ripped from the ground and consumed by the vortex. If you don't move quickly, fixing that printer will be the least of your problems. This spinlock's algorithm is simple but efficient, quickly consuming everything in its path. It starts with a circular buffer containing only the value `0`, which it marks as the **current position**. It then steps forward through the circular buffer some number of steps (your puzzle input) before inserting the first new value, `1`, after the value it stopped on. The inserted value becomes the **current position**. Then, it steps forward from there the same number of steps, and wherever it stops, inserts after it the second new value, `2`, and uses that as the new **current position** again. It repeats this process of **stepping forward**, **inserting a new value**, and **using the location of the inserted value as the new current position** a total of **`2017`** times, inserting `2017` as its final operation, and ending with a total of `2018` values (including `0`) in the circular buffer. For example, if the spinlock were to step `3` times per insert, the circular buffer would begin to evolve like this (using parentheses to mark the current position after each iteration of the algorithm): >>> spinning = spinlock(step_size=3, rounds=2017) - The initial state before any insertions: >>> print_spin(next(spinning)) (0) - The spinlock steps forward three times (`0`, `0`, `0`), and then inserts the first value, `1`, after it. `1` becomes the current position: >>> print_spin(next(spinning)) 0 (1) - The spinlock steps forward three times (`0`, `1`, `0`), and then inserts the second value, `2`, after it. `2` becomes the current position: >>> print_spin(next(spinning)) 0 (2) 1 - The spinlock steps forward three times (`1`, `0`, `2`), and then inserts the third value, `3`, after it. `3` becomes the current position: >>> print_spin(next(spinning)) 0 2 (3) 1 And so on: >>> for _ in range(6): ... print_spin(next(spinning)) 0 2 (4) 3 1 0 (5) 2 4 3 1 0 5 2 4 3 (6) 1 0 5 (7) 2 4 3 6 1 0 5 7 2 4 3 (8) 6 1 0 (9) 5 7 2 4 3 8 6 1 Eventually, after 2017 insertions, the section of the circular buffer near the last insertion looks like this: >>> print_spin(last(spinning), context=3) 1512 1134 151 (2017) 638 1513 851 Perhaps, if you can identify the value that will ultimately be after the last value written (`2017`), you can short-circuit the spinlock. In this example, that would be `638`. **What is the value after `2017`** in your completed circular buffer? >>> part_1(step_size=3) part 1: value after 2017 is 638 638 """ current_pos, numbers = last(spinlock(step_size, count)) assert numbers[current_pos] == count result = numbers[current_pos + 1] print(f"part 1: value after 2017 is {result}") return result
def memory_game(numbers: list[int], turns: int) -> int: assert turns > 0 game = islice(memory_game_it(numbers), turns) return last(tqdm(game, unit=" turns", total=turns, unit_scale=True, delay=1.0))
def generate_nth_key(salt: str, hasher: Hasher, nth: int) -> tuple[int, str]: keys = islice(generate_keys(salt, hasher), nth) desc = f"generating keys with {hasher.__name__}" return last(tqdm(keys, desc=desc, total=nth, unit=" keys", delay=1.0))
def distance_after_steps(directions: Iterable[str]) -> int: origin = (0, 0) target = last(steps(origin, directions)) return distance(origin, target)
def does_hit(initial_velocity: Vector, target: Rect) -> bool: return last(shoot(initial_velocity, target)) in target
def build_sequence(dependencies: Iterable[Dependency]) -> str: return ''.join(last(build(dependencies)).finished)
def floor_number(line: str) -> int: assert len(line) > 0 return last(floors_from_str(line))
def part_1(grid: 'Grid', bursts: int = 10_000) -> int: """ Diagnostics indicate that the local **grid computing cluster** has been contaminated with the **Sporifica Virus**. The grid computing cluster is a seemingly-infinite two-dimensional grid of compute nodes. Each node is either **clean** or **infected** by the virus. To [prevent overloading](https://en.wikipedia.org/wiki/Morris_worm#The_mistake) the nodes (which would render them useless to the virus) or detection by system administrators, exactly one **virus carrier** moves through the network, infecting or cleaning nodes as it moves. The virus carrier is always located on a single node in the network (the **current node**) and keeps track of the **direction** it is facing. To avoid detection, the virus carrier works in bursts; in each burst, it **wakes up**, does some **work**, and goes back to **sleep**. The following steps are all executed in order one time each burst: - If the **current node** is **infected**, it turns to its **right**. Otherwise, it turns to its **left**. (Turning is done in-place; the **current node** does not change.) - If the **current node** is **clean**, it becomes **infected**. Otherwise, it becomes **cleaned**. (This is done **after** the node is considered for the purposes of changing direction.) - The virus carrier moves **forward** one node in the direction it is facing. Diagnostics have also provided a **map of the node infection status** (your puzzle input). **Clean** nodes are shown as `.`; **infected** nodes are shown as `#`. This map only shows the center of the grid; there are many more nodes beyond those shown, but none of them are currently infected. The virus carrier begins in the middle of the map facing **up**. For example, suppose you are given a map like this: >>> example_grid = Grid.from_text(''' ... ... ..# ... #.. ... ... ... ... ''') >>> example_grid Grid({(1, -1): NodeState.INFECTED, (-1, 0): NodeState.INFECTED}) Then, the middle of the infinite grid looks like this, with the virus carrier's position marked with `[ ]`: >>> run = run_virus(example_grid, bursts=70) >>> print(next(run)) · · · · · · · · · · · · · · · · · · # · · · · #[·]· · · (↑) · · · · · · · · · · · · · · The virus carrier is on a **clean** node, so it turns **left**, infects the node, and moves left: >>> print(next(run)) · · · · · · · · · · · · · · · · · · # · · · ·[#]# · · · (←) · · · · · · · · · · · · · · The virus carrier is on an **infected** node, so it turns **right**, **cleans** the node, and moves up: >>> print(next(run)) · · · · · · · · · · · · · · · ·[·]· # · · (↑) · · · # · · · · · · · · · · · · · · · · · Four times in a row, the virus carrier finds a **clean**, **infects** it, turns **left**, and moves forward, ending in the same place and still facing up: >>> from itertools import islice >>> print(last(islice(run, 4))) · · · · · · · · · · · · · · · · · · #[#]· # · · (↑) · · # # # · · · · · · · · · · · · · · · · · · · Now on the same node as before, it sees an infection, which causes it to turn **right**, **clean** the node, and move forward: >>> print(state_7 := next(run)) · · · · · · · · · · · · · · · · · · # ·[·]# · · (→) · · # # # · · · · · · · · · · · · · · · · · · · After the above actions, a total of `7` bursts of activity had taken place. Of them, `5` bursts of activity caused an infection: >>> state_7.infections, state_7.bursts (5, 7) After a total of `70`, the grid looks like this: >>> print(state_70 := last(run)) · · · · · · · · · · · · · · · · · · · · · · · · · · · # # · · · · · · · · # · · # · · · · · · # · · · · # · · · · # · #[·]· · # · · (↑) · · # · # · · # · · · · · · · · # # · · · · · · · · · · · · · · · · · · · · · · · · · · By this time, `41` bursts of activity caused an infection (though most of those nodes have since been cleaned): >>> state_70.infections, state_70.bursts (41, 70) After a total of `10000` bursts of activity, `5587` bursts will have caused an infection. >>> part_1(example_grid) part 1: after 10000 bursts, 5587 nodes got infected 5587 Given your actual map, after `10000` bursts of activity, **how many bursts cause a node to become infected**? (Do not count nodes that begin infected.) """ last_state = last(run_virus(grid, bursts)) print(f"part 1: after {last_state.bursts} bursts, {last_state.infections} nodes got infected") return last_state.infections
def part_2(grid: 'Grid', bursts: int = 10_000_000) -> int: """ As you go to remove the virus from the infected nodes, it **evolves** to resist your attempt. Now, before it infects a clean node, it will **weaken** it to disable your defenses. If it encounters an infected node, it will instead **flag** the node to be cleaned in the future. So: - **Clean** nodes become **weakened**. - **Weakened** nodes become **infected**. - **Infected** nodes become **flagged**. - **Flagged** nodes become **clean**. Every node is always in exactly one of the above states. The virus carrier still functions in a similar way, but now uses the following logic during its bursts of action: - Decide which way to turn based on the current node: - If it is **clean**, it turns **left**. - If it is **weakened**, it does **not** turn, and will continue moving in the same dir. - If it is **infected**, it turns **right**. - If it is **flagged**, it **reverses** direction, and will go back the way it came. - Modify the state of the **current node**, as described above. - The virus carrier moves **forward** one node in the direction it is facing. Start with the same map and still with the virus carrier starting in the middle and facing **up**. Using the same initial state as the previous example, and drawing **weakened** as `o` and **flagged** as `f`, the middle of the infinite grid looks like this, with the virus carrier's position again marked with `[ ]`: >>> example_grid = Grid.from_file('data/22-example.txt') >>> run = run_virus(example_grid, bursts=100, extended_states=True) >>> print(next(run)) · · · · · · · · · · · · · · · · · · # · · · · #[·]· · · (↑) · · · · · · · · · · · · · · This is the same as before, since no initial nodes are **weakened** or **flagged**. The virus carrier is on a clean node, so it still turns left, instead **weakens** the node, and moves left: >>> print(next(run)) · · · · · · · · · · · · · · · · · · # · · · ·[#]o · · · (←) · · · · · · · · · · · · · · The virus carrier is on an infected node, so it still turns right, instead **flags** the node, and moves up: >>> print(next(run)) · · · · · · · · · · · · · · · ·[·]· # · · (↑) · · f o · · · · · · · · · · · · · · · · · This process repeats three more times, ending on the previously-flagged node and facing right: >>> from itertools import islice >>> print(last(islice(run, 3))) · · · · · · · · · · · · · · · · · · o o · # · · · · o[f]o · · · (→) · · · · · · · · · · · · · · · · Finding a flagged node, it reverses direction and **cleans** the node: >>> print(next(run)) · · · · · · · · · · · · · · · · · · o o · # · · · ·[o]· o · · · (←) · · · · · · · · · · · · · · · · The **weakened** node becomes infected, and it continues in the same direction: >>> print(next(run)) · · · · · · · · · · · · · · · · · · · · · o o · # · · · ·[·]# · o · · · (←) · · · · · · · · · · · · · · · · · · Of the first 100 bursts, 26 will result in infection: >>> state_100 = last(run) >>> state_100.infections, state_100.bursts (26, 100) Unfortunately, another feature of this evolved virus is speed; of the first `10_000_000` bursts, `2511944` will result in infection: >>> part_2(example_grid) part 2: after 10000000 bursts, 2511944 nodes got infected 2511944 Given your actual map, after `10_000_000` bursts of activity, **how many bursts cause a node to become infected**? (Do not count nodes that begin infected.) """ last_state = last(run_virus(grid, bursts, extended_states=True)) print(f"part 2: after {last_state.bursts} bursts, {last_state.infections} nodes got infected") return last_state.infections
def part_1(instructions: Iterable['Instruction']) -> int: """ You receive a signal directly from the CPU. Because of your recent assistance with jump instructions, it would like you to compute the result of a series of unusual register instructions. Each instruction consists of several parts: the register to modify, whether to increase or decrease that register's value, the amount by which to increase or decrease it, and a condition. If the condition fails, skip the instruction without modifying the register. The registers all start at `0`. The instructions look like this: >>> example_instructions = instructions_from_text(''' ... b inc 5 if a > 1 ... a inc 1 if b < 5 ... c dec -10 if a >= 1 ... c inc -20 if c == 10 ... ''') >>> example_instructions # doctest: +NORMALIZE_WHITESPACE [Instruction('b', 'inc', 5, Condition('a', '>', 1)), Instruction('a', 'inc', 1, Condition('b', '<', 5)), Instruction('c', 'dec', -10, Condition('a', '>=', 1)), Instruction('c', 'inc', -20, Condition('c', '==', 10))] These instructions would be processed as follows: >>> run = run_instructions(example_instructions) - Because `a` starts at `0`, it is not greater than `1`, and so `b` is not modified: >>> next(run) RunState(head=0, cond_eval=False, registers={}) - `a` is increased by `1` (to `1`) because `b` is less than `5` (it is `0`): >>> next(run) RunState(head=1, cond_eval=True, registers={'a': 1}) - `c` is decreased by `-10` (to `10`) because `a` is now greater than or equal to `1` (it is `1`): >>> next(run) RunState(head=2, cond_eval=True, registers={'a': 1, 'c': 10}) - `c` is increased by `-20` (to `-10`) because `c` is equal to `10`: >>> next(run) RunState(head=3, cond_eval=True, registers={'a': 1, 'c': -10}) After this process, the largest value in any register is `1`. You might also encounter `<=` (less than or equal to) or `!=` (not equal to). However, the CPU doesn't have the bandwidth to tell you what all the registers are named, and leaves that to you to determine. **What is the largest value in any register** after completing the instructions in your puzzle input? >>> part_1(example_instructions) part 1: largest value in registers is a=1 1 """ final_registers = last(run_instructions(instructions)).registers max_value, max_reg = max( (val, reg) for reg, val in final_registers.items()) print(f"part 1: largest value in registers is {max_reg}={max_value}") return max_value