def maxk(values: Iterable[T], key: Callable[[T], V]) -> tuple[T, V]: """ >>> maxk(["dog", "cat", "monkey"], key=len) ('monkey', 6) >>> maxk([1, 5, 10, 20], key=str) (5, '5') >>> maxk([], key=lambda x: 1) Traceback (most recent call last): ... ValueError: maxk() arg is an empty sequence """ max_k = None max_v = None any_value = False for value in values: k = key(value) if max_k is None or k > max_k: max_k, max_v = k, value any_value = True if any_value: return some(max_v), some(max_k) else: raise ValueError("maxk() arg is an empty sequence")
def minmax(values: Iterable[T], key: Callable[[T], K] = None) -> tuple[T, T]: """ >>> minmax([1, 4, 8, 10, 4, -4, 15, -2]) (-4, 15) >>> minmax(["cat", "dog", "antelope"]) ('antelope', 'dog') >>> minmax(["cat", "dog", "antelope"], key=len) ('cat', 'antelope') >>> minmax([4, 4, 4]) (4, 4) >>> minmax([3]) (3, 3) >>> minmax([]) Traceback (most recent call last): ... ValueError: minmax() arg is an empty sequence """ min_k, max_k = None, None min_v, max_v = None, None any_value = False for value in values: k = key(value) if key is not None else value if min_k is None or k < min_k: min_k, min_v = k, value if max_k is None or k > max_k: max_k, max_v = k, value any_value = True if any_value: return some(min_v), some(max_v) else: raise ValueError("minmax() arg is an empty sequence")
def mink(values: Iterable[T], key: Callable[[T], V]) -> tuple[T, V]: """ >>> mink(["dog", "zebra", "monkey"], key=len) ('dog', 3) >>> mink([5, 6, 8, 10, 12], key=str) (10, '10') >>> mink([], key=lambda x: 1) Traceback (most recent call last): ... ValueError: mink() arg is an empty sequence """ min_k = None min_v = None any_value = False for value in values: k = key(value) if min_k is None or k < min_k: min_k, min_v = k, value any_value = True if any_value: return some(min_v), some(min_k) else: raise ValueError("mink() arg is an empty sequence")
def finish(self, *, print_map: bool = True, print_result: bool = True, result_padding: int = 0) -> None: lines_initial = list( self.drawn_lines(show_header=False, show_unit_status=False)) while self.winning_team is None: self.do_round() if print_map: lines_divider = [ " --> " if y == self.height // 2 else " " for y in range(self.height) ] lines_final = list( self.drawn_lines(show_header=False, show_unit_numbers=False)) for lines in zip(lines_initial, lines_divider, lines_final): print("".join(lines)) print() if print_result: padding = " " * result_padding rounds = self.full_rounds_completed winning_team = some(self.winning_team).name winning_hps = sum(u.hp for u in self.active_units()) print(padding + f"Combat ends after {rounds} full rounds") print( padding + f"{winning_team} win with {winning_hps} total hit points left") print(padding + f"Outcome: {rounds} * {winning_hps} = {self.final_score()}")
def apply_floating(self, value: int) -> Iterable[int]: # "fbit" = floating bit = bit in mask marked `X` which can have both balues fbits_at = [index for index, bit in enumerate(self.mask_string) if bit == 'X'] # This algorithm yields (2 ** F) values, where F is the number of floating bits: possible_values_count = 2 ** len(fbits_at) # For each such value, we'll first create a "submask", a helper bitmask, which will then be # applied to the `value` in "normal" way. The submask is created using this key: # 0 -> X # 1 -> 1 # X -> 0/1 # # ... where `0/1` marks the floating bit and `X` means "no change" (as in `apply()`) # # X01X -> 0X10, 0X11, 1X10, 1X11 submask_template = [ {'0': 'X', '1': '1', 'X': None}[bit] for bit in self.mask_string ] # Let's generate each possible combination of values for floating bits: for value_index in range(possible_values_count): # Adjust the submask template by writing fixed values into floating bit positions: for fbit_index, fbit_at in enumerate(reversed(fbits_at)): submask_template[fbit_at] = '1' if value_index & (1 << fbit_index) else '0' # Create current submask out of the template and apply it to the bvalue: submask = BitMask(''.join(some(bit) for bit in submask_template)) yield submask.apply(value)
def __iter__(self) -> Iterator: link = self._current_link while True: yield link link = some(link.next_link) if link == self._current_link: break
def records_from_events(events: Iterable[Event]) -> Iterable[NightRecord]: current_date: Date | None = None current_guard_id: int | None = None current_sleeps: list[range] = [] last_sleep_minute: int | None = None last_timestamp: Timestamp | None = None for event in events: if last_timestamp: assert event.time > last_timestamp last_timestamp = event.time if event.guard_id is not None: assert event.what == "begins shift" if current_sleeps: yield NightRecord(some(current_date), some(current_guard_id), current_sleeps) current_sleeps = [] current_guard_id = event.guard_id current_date = None else: assert event.time.hour == 0 current_date = event.date if event.what == "falls asleep": assert last_sleep_minute is None last_sleep_minute = event.time.minute elif event.what == "wakes up": wake_up_minute = event.time.minute current_sleeps.append( range(some(last_sleep_minute), wake_up_minute)) last_sleep_minute = None else: raise ValueError(event.what) if current_sleeps: yield NightRecord(some(current_date), some(current_guard_id), current_sleeps)
def part_2(numbers_drawn: list[int], boards: 'Boards') -> int: """ Instructions for part 2. """ last_winning_board = some( boards.mark(*numbers_drawn, stop_at_first_win=False)) result = last_winning_board.winning_score() print(f"part 2: last winning board has score {result}") return result
def run(self, minutes: int, draw_each: int = 0, detect_cycles: bool = True): while self.minute < minutes: cycling = self.step() if draw_each > 0 and self.minute % draw_each == 0: self.draw() if detect_cycles and cycling: cycle_length = self._detect_cycle() assert cycle_length is not None for _ in range((minutes - self.minute) % cycle_length): self.current_hash = self.next_hash[some(self.current_hash)] self.board = self.hash_to_board[some(self.current_hash)] self.minute = minutes break return self
def _detect_cycle(self) -> Optional[int]: hash_ = self.current_hash for step in count(1): hash_ = self.next_hash.get(some(hash_)) if hash_ is None: return None elif hash_ == self.current_hash: return step # unreachable assert False
def run_single_program(tape: Tape) -> int: # pylint: disable=assignment-from-no-return program = run_program(0, tape) sent_value = None while True: signal = next(program) if signal is not None: # program sent something using `snd` -> remember it sent_value = signal else: # program triggered `rcv` -> return the last sent value return some(sent_value, "no previously sent value")
def spinlock_optimized(step_size: int, rounds: int) -> int: head = 0 last_value_at_1: int | None = None # we can do this without the buffer! for next_value in tqdm(range(1, rounds + 1), desc="inserting", unit_scale=True, delay=0.5): head = ((head + step_size) % next_value) + 1 if head == 1: last_value_at_1 = next_value return some(last_value_at_1)
def zip1(items: Iterable[T], wrap: bool = False) -> Iterable[tuple[T, T]]: """ >>> list(zip1([1, 2, 3, 4])) [(1, 2), (2, 3), (3, 4)] >>> list(zip1([1, 2, 3, 4], wrap=True)) [(1, 2), (2, 3), (3, 4), (4, 1)] >>> list(zip1([], wrap=True)) [] """ first_item = None last_item = None yielded_count = 0 for item in items: if last_item is not None: yield last_item, item yielded_count += 1 else: first_item = item last_item = item if wrap and yielded_count > 0: yield some(last_item), some(first_item)
def part_2(instructions: Iterable['Instr'] | str) -> int: """ Then, you notice the instructions continue on the back of the Recruiting Document. Easter Bunny HQ is actually at the first location you visit twice. For example, if your instructions are `R8, R4, R4, R8`, the first location you visit twice is `4` blocks away, due East. >>> first_repeat(walk('R8, R4, R4, R8')) (4, 0) **How many blocks away** is the first location you visit twice? >>> part_2('R8, R4, R4, R8') part 2: HQ is 4 blocks away at (4, 0) 4 """ hq_pos = some(first_repeat(walk(instructions))) distance = distance_from_origin(hq_pos) print(f"part 2: HQ is {distance} blocks away at {hq_pos}") return distance
def longest_and_strongest_bridge(links: Iterable[Link]) -> Link: return some( max_bridge(from_port=0, links=set(links), key=lambda b: (len(b), b.strength)))
def part_1(numbers_drawn: Iterable[int], boards: 'Boards') -> int: """ Bingo is played on a set of boards each consisting of a 5x5 grid of numbers. Numbers are chosen at random, and the chosen number is **marked** on all boards on which it appears. (Numbers may not appear on all boards.) If all numbers in any row or any column of a board are marked, that board **wins**. (Diagonals don't count.) The submarine has a bingo subsystem to help passengers pass the time. It automatically generates a random order in which to draw numbers and a random set of boards (your puzzle input). For example: >>> numbers, boards = game_from_text(''' ... 7,4,9,5,11,17,23,2,0,14,21,24,10,16,13,6,15,25,12,22,18,20,8,19,3,26,1 ... ... 22 13 17 11 0 ... 8 2 23 4 24 ... 21 9 14 16 7 ... 6 10 3 18 5 ... 1 12 20 15 19 ... ... 3 15 0 2 22 ... 9 18 13 17 5 ... 19 8 7 25 23 ... 20 11 10 24 4 ... 14 21 16 12 6 ... ... 14 21 17 24 4 ... 10 16 15 9 19 ... 18 8 23 26 20 ... 22 11 13 6 5 ... 2 0 12 3 7 ... ''') >>> len(boards) 3 >>> boards[0].row(0) [22, 13, 17, 11, 0] >>> boards[1].column(2) [0, 13, 7, 10, 16] After the first five numbers are drawn, ... >>> drawn = iter(numbers) >>> list(islice(drawn, 5)) [7, 4, 9, 5, 11] ... there are no winners, but the boards are marked as follows: >>> boards.mark(*_) >>> print(boards) 22 13 17 1̶1̶ 0 3 15 0 2 22 14 21 17 24 4̶ 8 2 23 4̶ 24 9̶ 18 13 17 5̶ 10 16 15 9̶ 19 21 9̶ 14 16 7̶ 19 8 7̶ 25 23 18 8 23 26 20 6 10 3 18 5̶ 20 1̶1̶ 10 24 4̶ 22 1̶1̶ 13 6 5̶ 1 12 20 15 19 14 21 16 12 6 2 0 12 3 7̶ After the next six numbers are drawn, there are still no winners: >>> list(islice(drawn, 6)) [17, 23, 2, 0, 14, 21] >>> boards.mark(*_) >>> print(boards) 22 13 1̶7̶ 1̶1̶ 0̶ 3 15 0̶ 2̶ 22 1̶4̶ 2̶1̶ 1̶7̶ 24 4̶ 8 2̶ 2̶3̶ 4̶ 24 9̶ 18 13 1̶7̶ 5̶ 10 16 15 9̶ 19 2̶1̶ 9̶ 1̶4̶ 16 7̶ 19 8 7̶ 25 2̶3̶ 18 8 2̶3̶ 26 20 6 10 3 18 5̶ 20 1̶1̶ 10 24 4̶ 22 1̶1̶ 13 6 5̶ 1 12 20 15 19 1̶4̶ 2̶1̶ 16 12 6 2̶ 0̶ 12 3 7̶ Finally, `24` is drawn: >>> next(drawn) 24 >>> winning_board = boards.mark(_) >>> print(boards) 22 13 1̶7̶ 1̶1̶ 0̶ 3 15 0̶ 2̶ 22 1̶4̶ 2̶1̶ 1̶7̶ 2̶4̶ 4̶ 8 2̶ 2̶3̶ 4̶ 2̶4̶ 9̶ 18 13 1̶7̶ 5̶ 10 16 15 9̶ 19 2̶1̶ 9̶ 1̶4̶ 16 7̶ 19 8 7̶ 25 2̶3̶ 18 8 2̶3̶ 26 20 6 10 3 18 5̶ 20 1̶1̶ 10 2̶4̶ 4̶ 22 1̶1̶ 13 6 5̶ 1 12 20 15 19 1̶4̶ 2̶1̶ 16 12 6 2̶ 0̶ 12 3 7̶ At this point, the third board **wins** because it has at least one complete row or column of marked numbers (in this case, the entire top row is marked: `1̶4̶ 2̶1̶ 1̶7̶ 2̶4̶ 4̶`). >>> winning_board is boards[2] True >>> winning_board.is_winner() True The **score** of the winning board can now be calculated. Start by finding the **sum of all unmarked numbers** on that board; in this case, the sum is `188`. >>> sum(winning_board.unmarked_numbers()) 188 Then, multiply that sum by **the number that was just called** when the board won, `24`, to get the final score, `188 * 24 = 4512`. >>> winning_board.winning_number 24 >>> winning_board.winning_score() 4512 Figure out which board will win first. **What will your final score be if you choose that board?** >>> boards.reset() >>> part_1(numbers, boards) part 1: first winning board has score 4512 4512 TODO move to part 2 >>> boards.reset() >>> part_2(numbers, boards) part 2: last winning board has score 1924 1924 """ first_winning_board = some(boards.mark(*numbers_drawn)) result = first_winning_board.winning_score() print(f"part 1: first winning board has score {result}") return result
def winning_score(self) -> int: return sum(self.unmarked_numbers()) * some(self.winning_number)
def repeated_frequency(increments: Iterable[int]) -> int: return some(first_repeat(cycling_frequencies(increments)))
def part_1(deck_1: 'Deck', deck_2: 'Deck', print_progress: bool = False) -> int: """ Before the game starts, split the cards so each player has their own deck (your puzzle input). Then, the game consists of a series of *rounds*: both players draw their top card, and the player with the higher-valued card wins the round. The winner keeps both cards, placing them on the bottom of their own deck so that the winner's card is above the other card. If this causes a player to have all of the cards, they win, and the game ends. For example, consider the following starting decks: >>> d1, d2 = decks_from_text(''' ... Player 1: ... 9 ... 2 ... 6 ... 3 ... 1 ... ... Player 2: ... 5 ... 8 ... 4 ... 7 ... 10 ... ''') This means that player 1's deck contains 5 cards, with `9` on top and `1` on the bottom: >>> d1 Deck([9, 2, 6, 3, 1]) >>> len(d1), d1.peek_top(), d1.peek_bottom() (5, 9, 1) Player 2's deck also contains 5 cards, with `5` on top and `10` on the bottom: >>> d2 Deck([5, 8, 4, 7, 10]) >>> len(d2), d2.peek_top(), d2.peek_bottom() (5, 5, 10) The first round begins with both players drawing the top card of their decks: `9` and `5`. Player 1 has the higher card, so both cards move to the bottom of player 1's deck such that `9` is above `5`: >>> game = Game(d1, d2) >>> game.play(rounds=5, print_progress=True) -- Round 1 -- Player 1's deck: 9, 2, 6, 3, 1 Player 2's deck: 5, 8, 4, 7, 10 Player 1 plays: 9 Player 2 plays: 5 Player 1 wins the round! -- Round 2 -- Player 1's deck: 2, 6, 3, 1, 9, 5 Player 2's deck: 8, 4, 7, 10 Player 1 plays: 2 Player 2 plays: 8 Player 2 wins the round! -- Round 3 -- Player 1's deck: 6, 3, 1, 9, 5 Player 2's deck: 4, 7, 10, 8, 2 Player 1 plays: 6 Player 2 plays: 4 Player 1 wins the round! -- Round 4 -- Player 1's deck: 3, 1, 9, 5, 6, 4 Player 2's deck: 7, 10, 8, 2 Player 1 plays: 3 Player 2 plays: 7 Player 2 wins the round! -- Round 5 -- Player 1's deck: 1, 9, 5, 6, 4 Player 2's deck: 10, 8, 2, 7, 3 Player 1 plays: 1 Player 2 plays: 10 Player 2 wins the round! ...several more rounds pass... >>> game.play(rounds=21, print_progress=False) In total, it takes 29 rounds before a player has all of the cards: >>> winner = game.play(print_progress=True) -- Round 27 -- Player 1's deck: 5, 4, 1 Player 2's deck: 8, 9, 7, 3, 2, 10, 6 Player 1 plays: 5 Player 2 plays: 8 Player 2 wins the round! -- Round 28 -- Player 1's deck: 4, 1 Player 2's deck: 9, 7, 3, 2, 10, 6, 8, 5 Player 1 plays: 4 Player 2 plays: 9 Player 2 wins the round! -- Round 29 -- Player 1's deck: 1 Player 2's deck: 7, 3, 2, 10, 6, 8, 5, 9, 4 Player 1 plays: 1 Player 2 plays: 7 Player 2 wins the round! == Post-game results == Player 1's deck: (empty) Player 2's deck: 3, 2, 10, 6, 8, 5, 9, 4, 7, 1 >>> winner.winning_player 2 >>> winner.winning_deck Deck([3, 2, 10, 6, 8, 5, 9, 4, 7, 1]) Once the game ends, you can calculate the winning player's *score*. The bottom card in their deck is worth the value of the card multiplied by 1, the second-from-the-bottom card is worth the value of the card multiplied by 2, and so on. With 10 cards, the top card is worth the value on the card multiplied by 10. In this example, the winning player's score is: >>> print(' + '.join(f'{c}*{m}' for c, m in zip(winner.winning_deck, range(10, 0, -1)))) 3*10 + 2*9 + 10*8 + 6*7 + 8*6 + 5*5 + 9*4 + 4*3 + 7*2 + 1*1 So, once the game ends, the winning player's score is *`306`*. >>> winner.final_score 306 Play the small crab in a game of Combat using the two decks you just dealt. *What is the winning player's score?* >>> part_1(d1, d2) part 1: player 2 wins with score 306 306 """ victory = some(Game(deck_1, deck_2).play(print_progress=print_progress)) result = victory.final_score print(f"part 1: player {victory.winning_player} wins with score {result}") return result
def part_2(deck_1: 'Deck', deck_2: 'Deck', print_progress: bool = False) -> int: """ *Recursive Combat* still starts by splitting the cards into two decks (you offer to play with the same starting decks as before - it's only fair). Then, the game consists of a series of *rounds* with a few changes: - Before either player deals a card, if there was a previous round in this game that had exactly the same cards in the same order in the same players' decks, the *game* instantly ends in a win for *player 1*. Previous rounds from other games are not considered. (This prevents infinite games of Recursive Combat, which everyone agrees is a bad idea.) - Otherwise, this round's cards must be in a new configuration; the players begin the round by each drawing the top card of their deck as normal. - If both players have at least as many cards remaining in their deck as the value of the card they just drew, the winner of the round is determined by playing a new game of Recursive Combat (see below). - Otherwise, at least one player must not have enough cards left in their deck to recurse; the winner of the round is the player with the higher-value card. As in regular Combat, the winner of the round (even if they won it by winning a sub-game) takes the two cards dealt at the beginning of the round and places them on the bottom of their own deck (again so that the winner's card is above the other card). Note that the winner's card might be the lower-valued of the two cards if they won the round due to winning a sub-game. If collecting cards by winning the round causes a player to have all of the cards, they win, and the game ends. During a round of Recursive Combat, if both players have at least as many cards in their own decks as the number on the card they just dealt, the winner of the round is determined by recursing into a sub-game of Recursive Combat. (For example, if player 1 draws the `3` card, and player 2 draws the `7` card, this would occur if player 1 has at least 3 cards left and player 2 has at least 7 cards left, not counting the `3` and `7` cards that were drawn.) To play a sub-game of Recursive Combat, each player creates a new deck by making a *copy* of the next cards in their deck (the quantity of cards copied is equal to the number on the card they drew to trigger the sub-game). During this sub-game, the game that triggered it is on hold and completely unaffected; no cards are removed from players' decks to form the sub-game. (For example, if player 1 drew the 3 card, their deck in the sub-game would be copies of the next three cards in their deck.) Here is a complete example of gameplay, where Game 1 is the primary game of Recursive Combat: >>> d1, d2 = Deck([9, 2, 6, 3, 1]), Deck([5, 8, 4, 7, 10]) >>> game = Game(d1, d2, recursive=True) >>> winner = game.play(print_progress=True) === Game 1 === -- Round 1 (Game 1) -- Player 1's deck: 9, 2, 6, 3, 1 Player 2's deck: 5, 8, 4, 7, 10 Player 1 plays: 9 Player 2 plays: 5 Player 1 wins round 1 of game 1! -- Round 2 (Game 1) -- Player 1's deck: 2, 6, 3, 1, 9, 5 Player 2's deck: 8, 4, 7, 10 Player 1 plays: 2 Player 2 plays: 8 Player 2 wins round 2 of game 1! -- Round 3 (Game 1) -- Player 1's deck: 6, 3, 1, 9, 5 Player 2's deck: 4, 7, 10, 8, 2 Player 1 plays: 6 Player 2 plays: 4 Player 1 wins round 3 of game 1! -- Round 4 (Game 1) -- Player 1's deck: 3, 1, 9, 5, 6, 4 Player 2's deck: 7, 10, 8, 2 Player 1 plays: 3 Player 2 plays: 7 Player 2 wins round 4 of game 1! -- Round 5 (Game 1) -- Player 1's deck: 1, 9, 5, 6, 4 Player 2's deck: 10, 8, 2, 7, 3 Player 1 plays: 1 Player 2 plays: 10 Player 2 wins round 5 of game 1! -- Round 6 (Game 1) -- Player 1's deck: 9, 5, 6, 4 Player 2's deck: 8, 2, 7, 3, 10, 1 Player 1 plays: 9 Player 2 plays: 8 Player 1 wins round 6 of game 1! -- Round 7 (Game 1) -- Player 1's deck: 5, 6, 4, 9, 8 Player 2's deck: 2, 7, 3, 10, 1 Player 1 plays: 5 Player 2 plays: 2 Player 1 wins round 7 of game 1! -- Round 8 (Game 1) -- Player 1's deck: 6, 4, 9, 8, 5, 2 Player 2's deck: 7, 3, 10, 1 Player 1 plays: 6 Player 2 plays: 7 Player 2 wins round 8 of game 1! -- Round 9 (Game 1) -- Player 1's deck: 4, 9, 8, 5, 2 Player 2's deck: 3, 10, 1, 7, 6 Player 1 plays: 4 Player 2 plays: 3 Playing a sub-game to determine the winner... === Game 2 === -- Round 1 (Game 2) -- Player 1's deck: 9, 8, 5, 2 Player 2's deck: 10, 1, 7 Player 1 plays: 9 Player 2 plays: 10 Player 2 wins round 1 of game 2! -- Round 2 (Game 2) -- Player 1's deck: 8, 5, 2 Player 2's deck: 1, 7, 10, 9 Player 1 plays: 8 Player 2 plays: 1 Player 1 wins round 2 of game 2! -- Round 3 (Game 2) -- Player 1's deck: 5, 2, 8, 1 Player 2's deck: 7, 10, 9 Player 1 plays: 5 Player 2 plays: 7 Player 2 wins round 3 of game 2! -- Round 4 (Game 2) -- Player 1's deck: 2, 8, 1 Player 2's deck: 10, 9, 7, 5 Player 1 plays: 2 Player 2 plays: 10 Player 2 wins round 4 of game 2! -- Round 5 (Game 2) -- Player 1's deck: 8, 1 Player 2's deck: 9, 7, 5, 10, 2 Player 1 plays: 8 Player 2 plays: 9 Player 2 wins round 5 of game 2! -- Round 6 (Game 2) -- Player 1's deck: 1 Player 2's deck: 7, 5, 10, 2, 9, 8 Player 1 plays: 1 Player 2 plays: 7 Player 2 wins round 6 of game 2! The winner of game 2 is player 2! ...anyway, back to game 1. Player 2 wins round 9 of game 1! -- Round 10 (Game 1) -- Player 1's deck: 9, 8, 5, 2 Player 2's deck: 10, 1, 7, 6, 3, 4 Player 1 plays: 9 Player 2 plays: 10 Player 2 wins round 10 of game 1! -- Round 11 (Game 1) -- Player 1's deck: 8, 5, 2 Player 2's deck: 1, 7, 6, 3, 4, 10, 9 Player 1 plays: 8 Player 2 plays: 1 Player 1 wins round 11 of game 1! -- Round 12 (Game 1) -- Player 1's deck: 5, 2, 8, 1 Player 2's deck: 7, 6, 3, 4, 10, 9 Player 1 plays: 5 Player 2 plays: 7 Player 2 wins round 12 of game 1! -- Round 13 (Game 1) -- Player 1's deck: 2, 8, 1 Player 2's deck: 6, 3, 4, 10, 9, 7, 5 Player 1 plays: 2 Player 2 plays: 6 Playing a sub-game to determine the winner... === Game 3 === -- Round 1 (Game 3) -- Player 1's deck: 8, 1 Player 2's deck: 3, 4, 10, 9, 7, 5 Player 1 plays: 8 Player 2 plays: 3 Player 1 wins round 1 of game 3! -- Round 2 (Game 3) -- Player 1's deck: 1, 8, 3 Player 2's deck: 4, 10, 9, 7, 5 Player 1 plays: 1 Player 2 plays: 4 Playing a sub-game to determine the winner... === Game 4 === -- Round 1 (Game 4) -- Player 1's deck: 8 Player 2's deck: 10, 9, 7, 5 Player 1 plays: 8 Player 2 plays: 10 Player 2 wins round 1 of game 4! The winner of game 4 is player 2! ...anyway, back to game 3. Player 2 wins round 2 of game 3! -- Round 3 (Game 3) -- Player 1's deck: 8, 3 Player 2's deck: 10, 9, 7, 5, 4, 1 Player 1 plays: 8 Player 2 plays: 10 Player 2 wins round 3 of game 3! -- Round 4 (Game 3) -- Player 1's deck: 3 Player 2's deck: 9, 7, 5, 4, 1, 10, 8 Player 1 plays: 3 Player 2 plays: 9 Player 2 wins round 4 of game 3! The winner of game 3 is player 2! ...anyway, back to game 1. Player 2 wins round 13 of game 1! -- Round 14 (Game 1) -- Player 1's deck: 8, 1 Player 2's deck: 3, 4, 10, 9, 7, 5, 6, 2 Player 1 plays: 8 Player 2 plays: 3 Player 1 wins round 14 of game 1! -- Round 15 (Game 1) -- Player 1's deck: 1, 8, 3 Player 2's deck: 4, 10, 9, 7, 5, 6, 2 Player 1 plays: 1 Player 2 plays: 4 Playing a sub-game to determine the winner... === Game 5 === -- Round 1 (Game 5) -- Player 1's deck: 8 Player 2's deck: 10, 9, 7, 5 Player 1 plays: 8 Player 2 plays: 10 Player 2 wins round 1 of game 5! The winner of game 5 is player 2! ...anyway, back to game 1. Player 2 wins round 15 of game 1! -- Round 16 (Game 1) -- Player 1's deck: 8, 3 Player 2's deck: 10, 9, 7, 5, 6, 2, 4, 1 Player 1 plays: 8 Player 2 plays: 10 Player 2 wins round 16 of game 1! -- Round 17 (Game 1) -- Player 1's deck: 3 Player 2's deck: 9, 7, 5, 6, 2, 4, 1, 10, 8 Player 1 plays: 3 Player 2 plays: 9 Player 2 wins round 17 of game 1! The winner of game 1 is player 2! == Post-game results == Player 1's deck: (empty) Player 2's deck: 7, 5, 6, 2, 4, 1, 10, 8, 9, 3 After the game, the winning player's score is calculated from the cards they have in their original deck using the same rules as regular Combat. In the above game, the winning player's score is *`291`*. >>> winner.final_score 291 Defend your honor as Raft Captain by playing the small crab in a game of Recursive Combat using the same two decks as before. *What is the winning player's score?* >>> part_2(d1, d2) part 2: player 2 wins with score 291 291 """ victory = some(Game(deck_1, deck_2, recursive=True).play(print_progress=print_progress)) result = victory.final_score print(f"part 2: player {victory.winning_player} wins with score {result}") return result
def backtrack(node: Node) -> Iterable[Edge]: while node != start: path = visited_nodes[node] yield some(path.edge) node = some(path.prev_node)
def shift(self, steps: int): self._current_link = some(self._current_link.follow(steps))
def bottom_card(self) -> Link: try: return some(self._bottom_card) except AssertionError as exc: raise IndexError("empty deck") from exc
def play_quantum(player_1_start: int, player_2_start: int, target_score: int, die_sides: int, die_rolls_per_turn: int = 3, board_size: int = 10, starting_player: int = 1) -> QuantumGameResult: assert 1 <= player_1_start <= board_size assert 1 <= player_2_start <= board_size assert target_score > 0 assert die_sides > 0 assert die_rolls_per_turn > 0 assert board_size > 1 assert starting_player in (1, 2) roll_sum_probs = Counter( sum(rolls) for rolls in itertools.product(range(1, die_sides + 1), repeat=die_rolls_per_turn)) # at the start there is only one universe initial_universe = GameState.initial(player_1_start, player_2_start) universe_counts: Counter[GameState] = Counter({initial_universe: 1}) # split the universes one turn at a time for turn in itertools.count(0): active_player = 1 + (starting_player + turn - 1) % 2 new_universe_counts: Counter[GameState] = Counter() any_split = False # split each universe without a winner for universe, universe_quantity in universe_counts.items(): if universe.winner is None: for roll, roll_quantity in roll_sum_probs.items(): new_universe = universe.after_turn( active_player=active_player, roll=roll, board_size=board_size, target_score=target_score, ) new_universe_counts[ new_universe] += universe_quantity * roll_quantity any_split = True else: # already has a winner, just copy new_universe_counts[universe] += universe_quantity if not any_split: # no more universe splits -> we are done break universe_counts = new_universe_counts # evaluation wins_count: Counter[int] = Counter() for universe, quantity in universe_counts.items(): wins_count[some(universe.winner)] += quantity assert len(wins_count) == 2 return QuantumGameResult(player_1_wins=wins_count[1], player_2_wins=wins_count[2])
def part_2(tower: 'Tower') -> int: """ The programs explain the situation: they can't get down. Rather, they **could** get down, if they weren't expending all of their energy trying to keep the tower balanced. Apparently, one program has the **wrong weight**, and until it's fixed, they're stuck here. For any program holding a disc, each program standing on that disc forms a sub-tower. Each of those sub-towers are supposed to be the same weight, or the disc itself isn't balanced. The weight of a tower is the sum of the weights of the programs in that tower. In the example above, this means that for `ugml`'s disc to be balanced, `gyxo`, `ebii`, and `jptl` must all have the same weight, and they do: `61`. >>> example = Tower.from_file('data/07-example.txt') >>> print(format(example, 'weights')) tknk (41 + 737 = 778) ├── ugml (68 + 183 = 251) │ ├── gyxo (61) │ ├── ebii (61) │ └── jptl (61) ├── padx (45 + 198 = 243) │ ├── pbga (66) │ ├── havc (66) │ └── qoyq (66) └── fwft (72 + 171 = 243) ├── ktlj (57) ├── cntj (57) └── xhth (57) However, for `tknk` to be balanced, each of the programs standing on its disc and all programs above it must each match. This means that their sums be all the same. But as you can see, `tknk`'s disc is unbalanced: `ugml`'s stack is heavier than the other two. Even though the nodes above `ugml` are balanced, `ugml` itself is too heavy: it needs to be `8` units lighter for its stack to weigh `243` and keep the towers balanced: >>> unb, correct_weight = example.find_unbalanced() >>> unb # doctest: +ELLIPSIS Tower('ugml', 68, [...]) >>> unb.total_weight 251 >>> correct_weight 243 If this change were made, its weight would be `60`: >>> correct_weight - unb.sub_towers_weight 60 Given that exactly one program is the wrong weight, **what would its weight need to be** to balance the entire tower? >>> part_2(example) part 2: to weigh total 243 and balance the tower, 'ugml' itself needs to weigh 60 60 """ unbalanced, target_total_weight = some(tower.find_unbalanced()) target_weight = target_total_weight - unbalanced.sub_towers_weight print( f"part 2: to weigh total {target_total_weight} and balance the tower, " f"{unbalanced.name!r} itself needs to weigh {target_weight}" ) return target_weight
def play(self, rounds: int = None, print_progress: bool = False) -> Victory | None: def log(text: str): if print_progress: print(" " * self.level + text) if self.recursive: log(f"=== Game {self.game_number} ===") remaining_rounds = rounds seen_states: set[tuple[int, int]] = set() while remaining_rounds != 0 and self.victory is None: self.round_number += 1 # peek top cards value_1 = self.deck_1.peek_top() value_2 = self.deck_2.peek_top() # report current state if self.recursive: log(f"-- Round {self.round_number} (Game {self.game_number}) --") else: log(f"-- Round {self.round_number} --") log(f"Player 1's deck: {self.deck_1}") log(f"Player 2's deck: {self.deck_2}") log(f"Player 1 plays: {value_1}") log(f"Player 2 plays: {value_2}") # decide round winner decide_by_subgame = ( self.recursive and (value_1 < len(self.deck_1)) and (value_2 < len(self.deck_2)) ) if decide_by_subgame: # ... either by subgame subgame = Game( deck_1=self.deck_1[1:value_1+1], deck_2=self.deck_2[1:value_2+1], recursive=True, game_number=self.next_subgame_number, level=self.level + 1 ) log("Playing a sub-game to determine the winner...") subgame_winner = some(subgame.play(print_progress=print_progress)) log(f"...anyway, back to game {self.game_number}.") self.next_subgame_number = subgame.next_subgame_number round_winner = subgame_winner.winning_player else: # ... or simply by card values if value_1 > value_2: round_winner = 1 elif value_2 > value_1: round_winner = 2 else: raise ValueError(f"round is draw: {value_1} == {value_2}") # report round winner if self.recursive: log(f"Player {round_winner} wins round {self.round_number} " f"of game {self.game_number}!") else: log(f"Player {round_winner} wins the round!") # modify decks card_1 = self.deck_1.draw_top() card_2 = self.deck_2.draw_top() if round_winner == 1: self.deck_1.extend_bottom([card_1, card_2]) else: self.deck_2.extend_bottom([card_2, card_1]) # check for victory by gaining all cards if not self.deck_1 or not self.deck_2: self.victory = Victory(self.deck_1, self.deck_2) # check for victory by infinite loop state_hashes = (self.deck_1.state_hash(), self.deck_2.state_hash()) if state_hashes not in seen_states: seen_states.add(state_hashes) else: # infinite loop detected -> player 1 wins self.victory = Victory(self.deck_1, self.deck_2) # report winner if self.victory: if self.recursive: log(f"The winner of game {self.game_number} is " f"player {self.victory.winning_player}!") if self.level == 0: log("== Post-game results ==") log(f"Player 1's deck: {self.deck_1}") log(f"Player 2's deck: {self.deck_2}") # next round ... if remaining_rounds is not None: remaining_rounds -= 1 # game finished return self.victory
def part_1(tiles: list['Tile']) -> tuple[int, 'Image']: r""" After decoding the satellite messages, you discover that the data actually contains many small images created by the satellite's *camera array*. The camera array consists of many cameras; rather than produce a single square image, they produce many smaller square image *tiles* that need to be *reassembled back into a single image*. Each camera in the camera array returns a single monochrome *image tile* with a random unique *ID number*. The tiles (your puzzle input) arrived in a random order. Worse yet, the camera array appears to be malfunctioning: each image tile has been *rotated and flipped to a random orientation*. Your first task is to reassemble the original image by orienting the tiles so they fit together. To show how the tiles should be reassembled, each tile's image data includes a border that should line up exactly with its adjacent tiles. All tiles have this border, and the border lines up exactly when the tiles are both oriented correctly. Tiles at the edge of the image also have this border, but the outermost edges won't line up with any other tiles. For example, suppose you have the following nine tiles: >>> example_tiles = tiles_from_text(''' ... ... Tile 2311: Tile 1951: Tile 1171: Tile 1427: Tile 1489: ... ..###..### #.##...##. ####...##. ###.##.#.. ##.#.#.... ... ###...#.#. #.####...# #..##.#..# .#..#.##.. ..##...#.. ... ..#....#.. .....#..## ##.#..#.#. .#.##.#..# .##..##... ... .#.#.#..## #...###### .###.####. #.#.#.##.# ..#...#... ... ##...#.### .##.#....# ..###.#### ....#...## #####...#. ... ##.##.###. .###.##### .##....##. ...##..##. #..#.#.#.# ... ####.#...# ###.##.##. .#...####. ...#.##### ...#.#.#.. ... #...##..#. .###....#. #.##.####. .#.####.#. ##.#...##. ... ##..#..... ..#.#..#.# ####..#... ..#..###.# ..##.##.## ... ..##.#..#. #...##.#.. .....##... ..##.#..#. ###.##.#.. ... ... Tile 2473: Tile 2971: Tile 2729: Tile 3079: ... #....####. ..#.#....# ...#.#.#.# #.#.#####. ... #..#.##... #...###... ####.#.... .#..###### ... #.##..#... #.#.###... ..#.#..... ..#....... ... ######.#.# ##.##..#.. ....#..#.# ######.... ... .#...#.#.# .#####..## .##..##.#. ####.#..#. ... .######### .#..####.# .#.####... .#...#.##. ... .###.#..#. #..#.#..#. ####.#.#.. #.#####.## ... ########.# ..####.### ##.####... ..#.###... ... ##...##.#. ..#.#.###. ##..#.##.. ..#....... ... ..###.#.#. ...#.#.#.# #.##...##. ..#.###... ... ... ''') >>> len(example_tiles) 9 Tiles have ID, square size, and four borders: >>> tile_0 = example_tiles[0] >>> tile_0.tile_id, tile_0.size, tile_0.borders (2311, 10, ('..###..###', '#..##.#...', '..##.#..#.', '.#..#####.')) >>> tile_8 = example_tiles[-1] >>> tile_8.tile_id, tile_8.size, tile_8.borders (3079, 10, ('#.#.#####.', '.#....#...', '..#.###...', '#..##.#...')) Tiles can be rotated and flipped: >>> print(tile_0) ..###..### ###...#.#. ..#....#.. .#.#.#..## ##...#.### ##.##.###. ####.#...# #...##..#. ##..#..... ..##.#..#. >>> print(tile_0.rotated_cw()) .#####..#. .#.####.#. #..#...### #..##.#..# .##.#....# #.##.##... ....#...#. ....##.#.# #.#.###.## ...#.##..# >>> print(tile_0.flipped_x()) ..##.#..#. ##..#..... #...##..#. ####.#...# ##.##.###. ##...#.### .#.#.#..## ..#....#.. ###...#.#. ..###..### By rotating, flipping, and rearranging them, you can find a square arrangement that causes all adjacent borders to line up: >>> img = Image.assemble(example_tiles) >>> img.width, img.height, img.tiles_size (3, 3, 10) >>> print(img) #...##.#.. ..###..### #.#.#####. ..#.#..#.# ###...#.#. .#..###### .###....#. ..#....#.. ..#....... ###.##.##. .#.#.#..## ######.... .###.##### ##...#.### ####.#..#. .##.#....# ##.##.###. .#...#.##. #...###### ####.#...# #.#####.## .....#..## #...##..#. ..#.###... #.####...# ##..#..... ..#....... #.##...##. ..##.#..#. ..#.###... <BLANKLINE> #.##...##. ..##.#..#. ..#.###... ##..#.##.. ..#..###.# ##.##....# ##.####... .#.####.#. ..#.###..# ####.#.#.. ...#.##### ###.#..### .#.####... ...##..##. .######.## .##..##.#. ....#...## #.#.#.#... ....#..#.# #.#.#.##.# #.###.###. ..#.#..... .#.##.#..# #.###.##.. ####.#.... .#..#.##.. .######... ...#.#.#.# ###.##.#.. .##...#### <BLANKLINE> ...#.#.#.# ###.##.#.. .##...#### ..#.#.###. ..##.##.## #..#.##..# ..####.### ##.#...##. .#.#..#.## #..#.#..#. ...#.#.#.. .####.###. .#..####.# #..#.#.#.# ####.###.. .#####..## #####...#. .##....##. ##.##..#.. ..#...#... .####...#. #.#.###... .##..##... .####.##.# #...###... ..##...#.. ...#..#### ..#.#....# ##.#.#.... ...##..... >>> print("\n".join(" ".join(str(t.tile_id) for t in row) for row in img.tile_rows)) 1951 2311 3079 2729 1427 2473 2971 1489 1171 To check that you've assembled the image correctly, multiply the IDs of the four corner tiles together. >>> corner_ids = [t.tile_id for t in img.corner_tiles] >>> corner_ids [1951, 3079, 2971, 1171] >>> math.prod(corner_ids) 20899048083289 Assemble the tiles into an image. *What do you get if you multiply together the IDs of the four corner tiles?* >>> res, _ = part_1(example_tiles) part 1: assembled image has corner tiles 1951 * 3079 * 2971 * 1171 = 20899048083289 >>> res 20899048083289 """ image = some(Image.assemble(tiles)) corner_tiles_ids = [corner_tile.tile_id for corner_tile in image.corner_tiles] result = math.prod(corner_tiles_ids) corners_text = " * ".join(str(tid) for tid in corner_tiles_ids) print(f"part 1: assembled image has corner tiles {corners_text} = {result}") return result, image
def part_1(battle: 'Battle') -> int: """ Having perfected their hot chocolate, the Elves have a new problem: the Goblins that live in these caves will do anything to steal it. Looks like they're here for a fight. You scan the area, generating a map of the walls (`#`), open cavern (`.`), and starting position of every Goblin (`G`) and Elf (`E`) (your puzzle input). Combat proceeds in **rounds**; in each round, each unit that is still alive takes a **turn**, resolving all of its actions before the next unit's turn begins. On each unit's turn, it tries to **move** into range of an enemy (if it isn't already) and then **attack** (if it's in range). ## Order of moves All units are very disciplined and always follow very strict combat rules. Units never move or attack diagonally, as doing so would be dishonorable. When multiple choices are equally valid, ties are broken in **reading order**: top-to-bottom, then left-to-right. For instance, the order in which units take their turns within a round is the **reading order of their starting positions** in that round, regardless of the type of unit or whether other units have moved after the round started. For example: would take their These units: turns in this order: ####### ####### #.G.E.# #.1.2.# #E.G.E# #3.4.5# #.G.E.# #.6.7.# ####### ####### ## Targeting Each unit begins its turn by identifying all possible **targets** (enemy units). If no targets remain, combat ends. Then, the unit identifies all of the open squares (`.`) that are **in range** of each target; these are the squares which are **adjacent** (immediately up, down, left, or right) to any target and which aren't already occupied by a wall or another unit. Alternatively, the unit might **already** be in range of a target. If the unit is not already in range of a target, and there are no open squares which are in range of a target, the unit ends its turn. If the unit is already in range of a target, it does not **move**, but continues its turn with an **attack**. Otherwise, since it is not in range of a target, it **moves**. ## Movement To **move**, the unit first considers the squares that are **in range** and determines **which of those squares it could reach in the fewest steps**. A **step** is a single movement to any adjacent (immediately up, down, left, or right) open (`.`) square. Units cannot move into walls or other units. The unit does this while considering the **current positions of units** and does not do any prediction about where units will be later. If the unit cannot reach (find an open path to) any of the squares that are in range, it ends its turn. If multiple squares are in range and **tied** for being reachable in the fewest steps, the square which is first in **reading order** is chosen. For example: Targets: In range: Reachable: Nearest: Chosen: ####### ####### ####### ####### ####### #E..G.# #E.?G?# #E.@G.# #E.!G.# #E.+G.# #...#.# --> #.?.#?# --> #.@.#.# --> #.!.#.# --> #...#.# #.G.#G# #?G?#G# #@G@#G# #!G.#G# #.G.#G# ####### ####### ####### ####### ####### In the above scenario, the Elf has three targets (the three Goblins): - Each of the Goblins has open, adjacent squares which are **in range* (marked with a `?` on the map). - Of those squares, four are **reachable** (marked `@`); the other two (on the right) would require moving through a wall or unit to reach. - Three of these reachable squares are **nearest**, requiring the fewest steps (only 2) to reach (marked `!`). - Of those, the square which is first in reading order is **chosen** (`+`). The unit then takes a single **step** toward the chosen square along **the shortest path** to that square. If multiple steps would put the unit equally closer to its destination, the unit chooses the step which is first in reading order. (This requires knowing when there is **more than one shortest path** so that you can consider the first step of each such path.) For example: In range: Nearest: Chosen: Distance: Step: ####### ####### ####### ####### ####### #.E...# #.E...# #.E...# #4E212# #..E..# #...?.# --> #...!.# --> #...+.# --> #32101# --> #.....# #..?G?# #..!G.# #...G.# #432G2# #...G.# ####### ####### ####### ####### ####### The Elf sees three squares in range of a target (`?`), two of which are nearest (`!`), and so the first in reading order is chosen (`+`). Under "Distance", each open square is marked with its distance from the destination square; the two squares to which the Elf could move on this turn (down and to the right) are both equally good moves and would leave the Elf `2` steps from being in range of the Goblin. Because the step which is first in reading order is chosen, the Elf moves **right** one square. Here's a larger example of movement: >>> example_map = Battle.from_file('data/15-example-movement.txt') >>> print(format(example_map, '!status')) Initially: ######### #G..G..G# #.......# #.......# #G..E..G# #.......# #.......# #G..G..G# ######### >>> example_map.do_round() >>> print(format(example_map, '!status')) After 1 round: ######### #.G...G.# #...G...# #...E..G# #.G.....# #.......# #G..G..G# #.......# ######### >>> example_map.do_round() >>> print(format(example_map, '!status')) After 2 rounds: ######### #..G.G..# #...G...# #.G.E.G.# #.......# #G..G..G# #.......# #.......# ######### >>> example_map.do_round() >>> print(format(example_map, '!status')) After 3 rounds: ######### #.......# #..GGG..# #..GEG..# #G..G...# #......G# #.......# #.......# ######### Once the Goblins and Elf reach the positions above, they all are either in range of a target or cannot find any square in range of a target, and so none of the units can move until a unit dies. >>> example_map.do_round() >>> print(format(example_map, '!status')) After 4 rounds: ######### #.......# #..GGG..# #..GEG..# #G..G...# #......G# #.......# #.......# ######### After moving (or if the unit began its turn in range of a target), the unit **attacks**. ## Attacking To **attack**, the unit first determines **all** of the targets that are **in range** of it by being immediately **adjacent** to it. If there are no such targets, the unit ends its turn. Otherwise, the adjacent target with **the fewest hit points** is selected; in a tie, the adjacent target with the fewest hit points which is first in reading order is selected. The unit deals damage equal to its **attack power** to the selected target, reducing its hit points by that amount. If this reduces its hit points to `0` or fewer, the selected target **dies**: its square becomes `.` and it takes no further turns. Each **unit**, either Goblin or Elf, has `3` **attack power** and starts with `200` **hit points**. For example, suppose the only Elf is about to attack: HP: HP: G.... 9 G.... 9 ..G.. 4 ..G.. 4 ..EG. 2 --> ..E.. ..G.. 2 ..G.. 2 ...G. 1 ...G. 1 The "HP" column shows the hit points of the Goblin to the left in the corresponding row. The Elf is in range of three targets: the Goblin above it (with `4` hit points), the Goblin to its right (with `2` hit points), and the Goblin below it (also with `2` hit points). Because three targets are in range, the ones with the lowest hit points are selected: the two Goblins with `2` hit points each (one to the right of the Elf and one below the Elf). Of those, the Goblin first in reading order (the one to the right of the Elf) is selected. The selected Goblin's hit points (`2`) are reduced by the Elf's attack power (`3`), reducing its hit points to `-1`, killing it. After attacking, the unit's turn ends. Regardless of how the unit's turn ends, the next unit in the round takes its turn. If all units have taken turns in this round, the round ends, and a new round begins. ## Combat The Elves look quite outnumbered. You need to determine the **outcome** of the battle: the **number of full rounds that were completed** (not counting the round in which combat ends) multiplied by **the sum of the hit points of all remaining units** at the moment combat ends. (Combat only ends when a unit finds no targets during its turn.) Below is an entire sample combat. Next to each map, each row's units' hit points are listed from left to right. >>> example_combat = Battle.from_file('data/15-example-0.txt') >>> print(example_combat) Initially: ####### #.G...# G1(200) #...EG# E1(200), G2(200) #.#.#G# G3(200) #..G#E# G4(200), E2(200) #.....# ####### >>> example_combat.do_round() >>> print(example_combat) After 1 round: ####### #..G..# G1(200) #...EG# E1(197), G2(197) #.#G#G# G4(200), G3(197) #...#E# E2(197) #.....# ####### >>> example_combat.do_round() >>> print(example_combat) After 2 rounds: ####### #...G.# G1(200) #..GEG# G4(200), E1(188), G2(194) #.#.#G# G3(194) #...#E# E2(194) #.....# ####### Combat ensues ... >>> while example_combat.round < 23: ... example_combat.do_round() Eventually, the top Elf dies: >>> print(example_combat) After 23 rounds: ####### #...G.# G1(200) #..G.G# G4(200), G2(131) #.#.#G# G3(131) #...#E# E2(131) #.....# ####### >>> example_combat.do_round() >>> print(example_combat) After 24 rounds: ####### #..G..# G1(200) #...G.# G2(131) #.#G#G# G4(200), G3(128) #...#E# E2(128) #.....# ####### >>> example_combat.do_round() >>> print(example_combat) After 25 rounds: ####### #.G...# G1(200) #..G..# G2(131) #.#.#G# G3(125) #..G#E# G4(200), E2(125) #.....# ####### >>> example_combat.do_round() >>> print(example_combat) After 26 rounds: ####### #G....# G1(200) #.G...# G2(131) #.#.#G# G3(122) #...#E# E2(122) #..G..# G4(200) ####### >>> example_combat.do_round() >>> print(example_combat) After 27 rounds: ####### #G....# G1(200) #.G...# G2(131) #.#.#G# G3(119) #...#E# E2(119) #...G.# G4(200) ####### >>> example_combat.do_round() >>> print(example_combat) After 28 rounds: ####### #G....# G1(200) #.G...# G2(131) #.#.#G# G3(116) #...#E# E2(113) #....G# G4(200) ####### More combat ensues ... >>> while example_combat.round < 47: ... _ = example_combat.do_round() Eventually, the bottom Elf dies: >>> print(example_combat) After 47 rounds: ####### #G....# G1(200) #.G...# G2(131) #.#.#G# G3(59) #...#.# #....G# G4(200) ####### Before the 48th round can finish, the top-left Goblin finds that there are no targets remaining, and so combat ends. >>> example_combat.winning_team Team(name='Goblins', code='G', attack=3, hp=200) So, the number of full rounds that were completed is **`47`**, and the sum of the hit points of all remaining units is `200+131+59+200 = **590**`: >>> sum(u.hp for u in example_combat.active_units()) 590 From these, the outcome of the battle is: >>> 47 * 590 27730 Here are a few example summarized combats: >>> Battle.from_file('data/15-example-1.txt').finish() ####### ####### #G..#E# #...#E# E(200) #E#E.E# #E#...# E(197) #G.##.# --> #.E##.# E(185) #...#E# #E..#E# E(200), E(200) #...E.# #.....# ####### ####### <BLANKLINE> Combat ends after 37 full rounds Elves win with 982 total hit points left Outcome: 37 * 982 = 36334 >>> Battle.from_file('data/15-example-2.txt').finish() ####### ####### #E..EG# #.E.E.# E(164), E(197) #.#G.E# #.#E..# E(200) #E.##E# --> #E.##.# E(98) #G..#.# #.E.#.# E(200) #..E#.# #...#.# ####### ####### <BLANKLINE> Combat ends after 46 full rounds Elves win with 859 total hit points left Outcome: 46 * 859 = 39514 >>> Battle.from_file('data/15-example-3.txt').finish() ####### ####### #E.G#.# #G.G#.# G(200), G(98) #.#G..# #.#G..# G(200) #G.#.G# --> #..#..# #G..#.# #...#G# G(95) #...E.# #...G.# G(200) ####### ####### <BLANKLINE> Combat ends after 35 full rounds Goblins win with 793 total hit points left Outcome: 35 * 793 = 27755 >>> Battle.from_file('data/15-example-4.txt').finish() ####### ####### #.E...# #.....# #.#..G# #.#G..# G(200) #.###.# --> #.###.# #E#G#G# #.#.#.# #...#G# #G.G#G# G(98), G(38), G(200) ####### ####### <BLANKLINE> Combat ends after 54 full rounds Goblins win with 536 total hit points left Outcome: 54 * 536 = 28944 >>> Battle.from_file('data/15-example-5.txt').finish() ######### ######### #G......# #.G.....# G(137) #.E.#...# #G.G#...# G(200), G(200) #..##..G# #.G##...# G(200) #...##..# --> #...##..# #...#...# #.G.#...# G(200) #.G...G.# #.......# #.....G.# #.......# ######### ######### <BLANKLINE> Combat ends after 20 full rounds Goblins win with 937 total hit points left Outcome: 20 * 937 = 18740 **What is the outcome** of the combat described in your puzzle input? >>> part_1(Battle.from_file('data/15-example-5.txt')) part 1: Combat ends after 20 full rounds Goblins win with 937 total hit points left Outcome: 20 * 937 = 18740 18740 """ battle = battle.copy() print("part 1:") battle.finish(print_map=False, result_padding=4) return some(battle.final_score())