Solve day 7

This commit is contained in:
Nettika
2023-12-12 22:35:04 -08:00
parent d36993c895
commit 2cd901b1de
6 changed files with 1448 additions and 0 deletions

14
07/__main__.py Normal file
View File

@@ -0,0 +1,14 @@
from pathlib import Path
from puzzle import solve
def main():
input = Path(__file__).parent.joinpath("hands.txt").read_text().strip().split("\n")
print("Total Winnings:")
print("Part 1 -", solve(input, False))
print("Part 2 -", solve(input, True))
if __name__ == "__main__":
main()

145
07/hand.py Normal file
View File

@@ -0,0 +1,145 @@
from __future__ import annotations
import re
from copy import copy
from dataclasses import dataclass
from enum import IntEnum
from functools import cached_property
class Strength(IntEnum):
FiveOfAKind = 6
FourOfAKind = 5
FullHouse = 4
ThreeOfAKind = 3
TwoPair = 2
OnePair = 1
HighCard = 0
class Card(IntEnum):
Joker = 1
Two = 2
Three = 3
Four = 4
Five = 5
Six = 6
Seven = 7
Eight = 8
Nine = 9
Ten = 10
Jack = 11
Queen = 12
King = 13
Ace = 14
@classmethod
def parse(cls, symbol: str, joker_mode=False) -> Card:
match symbol:
case "2":
return Card.Two
case "3":
return Card.Three
case "4":
return Card.Four
case "5":
return Card.Five
case "6":
return Card.Six
case "7":
return Card.Seven
case "8":
return Card.Eight
case "9":
return Card.Nine
case "T":
return Card.Ten
case "J":
return Card.Joker if joker_mode else Card.Jack
case "Q":
return Card.Queen
case "K":
return Card.King
case "A":
return Card.Ace
raise ValueError(f'The symbol "{symbol}" does not correspond to a valid card.')
hand_pattern = re.compile(r"^([2-9TJQKA]{5}) (\d+)$")
@dataclass(frozen=True)
class Hand:
cards: tuple[Card, Card, Card, Card, Card]
bid: int
@cached_property
def _groups(self) -> dict[Card, int]:
groups: dict[Card, int] = {}
for card in self.cards:
groups[card] = groups.get(card, 0) + 1
return groups
@cached_property
def _counts(self) -> list[int]:
groups = copy(self._groups)
if Card.Joker in groups:
del groups[Card.Joker]
return list(
reversed(
sorted(count for count in groups.values()),
),
)
@cached_property
def _jokers(self) -> int:
return self._groups.get(Card.Joker, 0)
@cached_property
def strength(self):
match (self._counts, self._jokers):
case ([5], 0) | ([4], 1) | ([3], 2) | ([2], 3) | ([1], 4) | ([], 5):
return Strength.FiveOfAKind
case ([4, 1], 0) | ([3, 1], 1) | ([2, 1], 2) | ([1, 1], 3):
return Strength.FourOfAKind
case ([3, 2], 0) | ([2, 2], 1):
return Strength.FullHouse
case ([3, 1, 1], 0) | ([2, 1, 1], 1) | ([1, 1, 1], 2):
return Strength.ThreeOfAKind
case ([2, 2, 1], 0):
return Strength.TwoPair
case ([2, 1, 1, 1], 0) | ([1, 1, 1, 1], 1):
return Strength.OnePair
case _:
return Strength.HighCard
@classmethod
def parse(self, hand_desc: str, joker_mode=False) -> Hand:
match = hand_pattern.match(hand_desc)
if not match:
raise ValueError(f'Invalid hand description: "{hand_desc}"')
return Hand(
tuple(Card.parse(symbol, joker_mode) for symbol in match.group(1)), # type: ignore
int(match.group(2)),
)
def __lt__(self, other: Hand) -> bool:
if self.strength < other.strength:
return True
if self.strength > other.strength:
return False
return self.cards < other.cards
def __gt__(self, other: Hand) -> bool:
if self.strength > other.strength:
return True
if self.strength < other.strength:
return False
return self.cards > other.cards
def __eq__(self, other: object) -> bool:
if not isinstance(other, Hand):
return False
if self.strength != other.strength:
return False
return self.cards == other.cards

269
07/hand_test.py Normal file
View File

@@ -0,0 +1,269 @@
import pytest
from hand import Card, Hand, Strength
def test_card_parse():
assert Card.parse("2") == Card.Two
assert Card.parse("3") == Card.Three
assert Card.parse("4") == Card.Four
assert Card.parse("5") == Card.Five
assert Card.parse("6") == Card.Six
assert Card.parse("7") == Card.Seven
assert Card.parse("8") == Card.Eight
assert Card.parse("9") == Card.Nine
assert Card.parse("T") == Card.Ten
assert Card.parse("J") == Card.Jack
assert Card.parse("Q") == Card.Queen
assert Card.parse("K") == Card.King
assert Card.parse("A") == Card.Ace
with pytest.raises(ValueError):
Card.parse("X")
def test_hand_parse():
assert Hand.parse("32T3K 765") == Hand(
(
Card.Three,
Card.Two,
Card.Ten,
Card.Three,
Card.King,
),
765,
)
with pytest.raises(ValueError):
Hand.parse("invalid")
def test_hand_groups():
assert Hand(
(
Card.Five,
Card.Two,
Card.Five,
Card.Two,
Card.Ace,
),
0,
)._groups == {
Card.Five: 2,
Card.Two: 2,
Card.Ace: 1,
}
assert Hand(
(
Card.Two,
Card.Eight,
Card.Nine,
Card.Queen,
Card.Jack,
),
0,
)._groups == {
Card.Two: 1,
Card.Eight: 1,
Card.Nine: 1,
Card.Queen: 1,
Card.Jack: 1,
}
def test_hand_counts():
assert Hand(
(
Card.Five,
Card.Two,
Card.Five,
Card.Two,
Card.Ace,
),
0,
)._counts == [2, 2, 1]
assert Hand(
(
Card.Four,
Card.Four,
Card.Seven,
Card.Four,
Card.Seven,
),
0,
)._counts == [3, 2]
assert Hand(
(
Card.Two,
Card.Three,
Card.Four,
Card.Five,
Card.Six,
),
0,
)._counts == [1, 1, 1, 1, 1]
assert Hand(
(
Card.Two,
Card.Joker,
Card.Four,
Card.Joker,
Card.Six,
),
0,
)._counts == [1, 1, 1]
assert (
Hand(
(
Card.Joker,
Card.Joker,
Card.Joker,
Card.Joker,
Card.Joker,
),
0,
)._counts
== []
)
def test_hand_card_strength():
assert (
Hand(
(Card.Ace, Card.Ace, Card.Ace, Card.Ace, Card.Ace),
0,
).strength
== Strength.FiveOfAKind
)
assert (
Hand(
(Card.Ace, Card.Ace, Card.Ace, Card.Ace, Card.Joker),
0,
).strength
== Strength.FiveOfAKind
)
assert (
Hand(
(Card.Ace, Card.Joker, Card.Ace, Card.Ace, Card.Joker),
0,
).strength
== Strength.FiveOfAKind
)
assert (
Hand(
(Card.Joker, Card.Joker, Card.Ace, Card.Ace, Card.Joker),
0,
).strength
== Strength.FiveOfAKind
)
assert (
Hand(
(Card.Joker, Card.Joker, Card.Ace, Card.Joker, Card.Joker),
0,
).strength
== Strength.FiveOfAKind
)
assert (
Hand(
(Card.Joker, Card.Joker, Card.Joker, Card.Joker, Card.Joker),
0,
).strength
== Strength.FiveOfAKind
)
assert (
Hand(
(Card.King, Card.King, Card.King, Card.King, Card.Five),
0,
).strength
== Strength.FourOfAKind
)
assert (
Hand(
(Card.King, Card.Joker, Card.King, Card.King, Card.Five),
0,
).strength
== Strength.FourOfAKind
)
assert (
Hand(
(Card.Joker, Card.Joker, Card.King, Card.King, Card.Five),
0,
).strength
== Strength.FourOfAKind
)
assert (
Hand(
(Card.Joker, Card.Joker, Card.King, Card.Joker, Card.Five),
0,
).strength
== Strength.FourOfAKind
)
assert (
Hand(
(Card.King, Card.Six, Card.King, Card.King, Card.Six),
0,
).strength
== Strength.FullHouse
)
assert (
Hand(
(Card.King, Card.Six, Card.Joker, Card.King, Card.Six),
0,
).strength
== Strength.FullHouse
)
assert (
Hand(
(Card.Ace, Card.Ace, Card.Two, Card.Five, Card.Ace),
0,
).strength
== Strength.ThreeOfAKind
)
assert (
Hand(
(Card.Joker, Card.Ace, Card.Two, Card.Five, Card.Ace),
0,
).strength
== Strength.ThreeOfAKind
)
assert (
Hand(
(Card.Joker, Card.Ace, Card.Two, Card.Five, Card.Joker),
0,
).strength
== Strength.ThreeOfAKind
)
assert (
Hand(
(Card.Five, Card.Two, Card.Five, Card.Two, Card.Ace),
0,
).strength
== Strength.TwoPair
)
assert (
Hand(
(Card.Five, Card.Two, Card.Five, Card.Nine, Card.Ace),
0,
).strength
== Strength.OnePair
)
assert (
Hand(
(Card.Five, Card.Two, Card.Joker, Card.Nine, Card.Ace),
0,
).strength
== Strength.OnePair
)
assert (
Hand(
(Card.Five, Card.Two, Card.Jack, Card.Nine, Card.Ace),
0,
).strength
== Strength.HighCard
)
def test_hand_comparison():
hand_a = Hand((Card.Two, Card.Three, Card.Five, Card.Eight, Card.Ace), 1)
hand_b = Hand((Card.Ace, Card.Ace, Card.Ace, Card.Ace, Card.Ace), 1)
assert hand_a < hand_b
assert hand_b > hand_a
assert hand_a == hand_a

1000
07/hands.txt Normal file

File diff suppressed because it is too large Load Diff

6
07/puzzle.py Normal file
View File

@@ -0,0 +1,6 @@
from hand import Hand
def solve(hand_descs: list[str], joker_mode=False):
hands = sorted(Hand.parse(hand_desc, joker_mode) for hand_desc in hand_descs)
return sum((index + 1) * hand.bid for index, hand in enumerate(hands))

14
07/puzzle_test.py Normal file
View File

@@ -0,0 +1,14 @@
from puzzle import solve
mock_hand_descriptions = [
"32T3K 765",
"T55J5 684",
"KK677 28",
"KTJJT 220",
"QQQJA 483",
]
def test_solve():
assert solve(mock_hand_descriptions, False) == 6440
assert solve(mock_hand_descriptions, True) == 5905