Пример #1
0
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")
Пример #2
0
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")
Пример #3
0
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")
Пример #4
0
    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()}")
Пример #5
0
    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)
Пример #6
0
 def __iter__(self) -> Iterator:
     link = self._current_link
     while True:
         yield link
         link = some(link.next_link)
         if link == self._current_link:
             break
Пример #7
0
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)
Пример #8
0
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
Пример #9
0
    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
Пример #10
0
    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
Пример #11
0
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")
Пример #12
0
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)
Пример #13
0
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)
Пример #14
0
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
Пример #15
0
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)))
Пример #16
0
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
Пример #17
0
 def winning_score(self) -> int:
     return sum(self.unmarked_numbers()) * some(self.winning_number)
Пример #18
0
def repeated_frequency(increments: Iterable[int]) -> int:
    return some(first_repeat(cycling_frequencies(increments)))
Пример #19
0
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
Пример #20
0
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
Пример #21
0
 def backtrack(node: Node) -> Iterable[Edge]:
     while node != start:
         path = visited_nodes[node]
         yield some(path.edge)
         node = some(path.prev_node)
Пример #22
0
 def shift(self, steps: int):
     self._current_link = some(self._current_link.follow(steps))
Пример #23
0
 def bottom_card(self) -> Link:
     try:
         return some(self._bottom_card)
     except AssertionError as exc:
         raise IndexError("empty deck") from exc
Пример #24
0
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])
Пример #25
0
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
Пример #26
0
    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
Пример #27
0
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
Пример #28
0
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())