updating latest from mac
This commit is contained in:
231
playground/hearts.py
Normal file
231
playground/hearts.py
Normal file
@@ -0,0 +1,231 @@
|
||||
# hearts.py
|
||||
|
||||
from collections import Counter
|
||||
import random
|
||||
import sys
|
||||
from typing import Any, Dict, List, Optional, Sequence, Tuple, Union
|
||||
from typing import overload
|
||||
|
||||
|
||||
class Card:
|
||||
SUITS = "♠ ♡ ♢ ♣".split()
|
||||
RANKS = "2 3 4 5 6 7 8 9 10 J Q K A".split()
|
||||
|
||||
def __init__(self, suit: str, rank: str) -> None:
|
||||
self.suit = suit
|
||||
self.rank = rank
|
||||
|
||||
@property
|
||||
def value(self) -> int:
|
||||
"""The value of a card is rank as a number"""
|
||||
return self.RANKS.index(self.rank)
|
||||
|
||||
@property
|
||||
def points(self) -> int:
|
||||
"""Points this card is worth"""
|
||||
if self.suit == "♠" and self.rank == "Q":
|
||||
return 13
|
||||
if self.suit == "♡":
|
||||
return 1
|
||||
return 0
|
||||
|
||||
def __eq__(self, other: Any) -> Any:
|
||||
return self.suit == other.suit and self.rank == other.rank
|
||||
|
||||
def __lt__(self, other: Any) -> Any:
|
||||
return self.value < other.value
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"{self.suit}{self.rank}"
|
||||
|
||||
|
||||
class Deck(Sequence[Card]):
|
||||
def __init__(self, cards: List[Card]) -> None:
|
||||
self.cards = cards
|
||||
|
||||
@classmethod
|
||||
def create(cls, shuffle: bool = False) -> "Deck":
|
||||
"""Create a new deck of 52 cards"""
|
||||
cards = [Card(s, r) for r in Card.RANKS for s in Card.SUITS]
|
||||
if shuffle:
|
||||
random.shuffle(cards)
|
||||
return cls(cards)
|
||||
|
||||
def play(self, card: Card) -> None:
|
||||
"""Play one card by removing it from the deck"""
|
||||
self.cards.remove(card)
|
||||
|
||||
def deal(self, num_hands: int) -> Tuple["Deck", ...]:
|
||||
"""Deal the cards in the deck into a number of hands"""
|
||||
return tuple(self[i::num_hands] for i in range(num_hands))
|
||||
|
||||
def add_cards(self, cards: List[Card]) -> None:
|
||||
"""Add a list of cards to the deck"""
|
||||
self.cards += cards
|
||||
|
||||
def __len__(self) -> int:
|
||||
return len(self.cards)
|
||||
|
||||
@overload
|
||||
def __getitem__(self, key: int) -> Card:
|
||||
...
|
||||
|
||||
@overload
|
||||
def __getitem__(self, key: slice) -> "Deck": # noqa
|
||||
...
|
||||
|
||||
def __getitem__( # noqa
|
||||
self, key: Union[int, slice]
|
||||
) -> Union[Card, "Deck"]:
|
||||
if isinstance(key, int):
|
||||
return self.cards[key]
|
||||
elif isinstance(key, slice):
|
||||
cls = self.__class__
|
||||
return cls(self.cards[key])
|
||||
else:
|
||||
raise TypeError("Indices must be integers or slices")
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return " ".join(repr(c) for c in self.cards)
|
||||
|
||||
|
||||
class Player:
|
||||
def __init__(self, name: str, hand: Optional[Deck] = None) -> None:
|
||||
self.name = name
|
||||
self.hand = Deck([]) if hand is None else hand
|
||||
|
||||
def playable_cards(self, played: List[Card], hearts_broken: bool) -> Deck:
|
||||
"""List which cards in hand are playable this round"""
|
||||
if Card("♣", "2") in self.hand:
|
||||
return Deck([Card("♣", "2")])
|
||||
|
||||
lead = played[0].suit if played else None
|
||||
playable = Deck([c for c in self.hand if c.suit == lead]) or self.hand
|
||||
if lead is None and not hearts_broken:
|
||||
playable = Deck([c for c in playable if c.suit != "♡"])
|
||||
return playable or Deck(self.hand.cards)
|
||||
|
||||
def non_winning_cards(self, played: List[Card], playable: Deck) -> Deck:
|
||||
"""List playable cards that are guaranteed to not win the trick"""
|
||||
if not played:
|
||||
return Deck([])
|
||||
|
||||
lead = played[0].suit
|
||||
best_card = max(c for c in played if c.suit == lead)
|
||||
return Deck([c for c in playable if c < best_card or c.suit != lead])
|
||||
|
||||
def play_card(self, played: List[Card], hearts_broken: bool) -> Card:
|
||||
"""Play a card from a cpu player's hand"""
|
||||
playable = self.playable_cards(played, hearts_broken)
|
||||
non_winning = self.non_winning_cards(played, playable)
|
||||
|
||||
# Strategy
|
||||
if non_winning:
|
||||
# Highest card not winning the trick, prefer points
|
||||
card = max(non_winning, key=lambda c: (c.points, c.value))
|
||||
elif len(played) < 3:
|
||||
# Lowest card maybe winning, avoid points
|
||||
card = min(playable, key=lambda c: (c.points, c.value))
|
||||
else:
|
||||
# Highest card guaranteed winning, avoid points
|
||||
card = max(playable, key=lambda c: (-c.points, c.value))
|
||||
self.hand.cards.remove(card)
|
||||
print(f"{self.name} -> {card}")
|
||||
return card
|
||||
|
||||
def has_card(self, card: Card) -> bool:
|
||||
return card in self.hand
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"{self.__class__.__name__}({self.name!r}, {self.hand})"
|
||||
|
||||
|
||||
class HumanPlayer(Player):
|
||||
def play_card(self, played: List[Card], hearts_broken: bool) -> Card:
|
||||
"""Play a card from a human player's hand"""
|
||||
playable = sorted(self.playable_cards(played, hearts_broken))
|
||||
p_str = " ".join(f"{n}: {c}" for n, c in enumerate(playable))
|
||||
np_str = " ".join(repr(c) for c in self.hand if c not in playable)
|
||||
print(f" {p_str} (Rest: {np_str})")
|
||||
while True:
|
||||
try:
|
||||
card_num = int(input(f" {self.name}, choose card: "))
|
||||
card = playable[card_num]
|
||||
except (ValueError, IndexError):
|
||||
pass
|
||||
else:
|
||||
break
|
||||
self.hand.play(card)
|
||||
print(f"{self.name} => {card}")
|
||||
return card
|
||||
|
||||
|
||||
class HeartsGame:
|
||||
def __init__(self, *names: str) -> None:
|
||||
self.names = (list(names) + "P1 P2 P3 P4".split())[:4]
|
||||
self.players = [Player(n) for n in self.names[1:]]
|
||||
self.players.append(HumanPlayer(self.names[0]))
|
||||
|
||||
def play(self) -> None:
|
||||
"""Play a game of Hearts until one player go bust"""
|
||||
score = Counter({n: 0 for n in self.names})
|
||||
while all(s < 100 for s in score.values()):
|
||||
print("\nStarting new round:")
|
||||
round_score = self.play_round()
|
||||
score.update(Counter(round_score))
|
||||
print("Scores:")
|
||||
for name, total_score in score.most_common(4):
|
||||
print(f"{name:<15} {round_score[name]:>3} {total_score:>3}")
|
||||
|
||||
winners = [n for n in self.names if score[n] == min(score.values())]
|
||||
print(f"\n{' and '.join(winners)} won the game")
|
||||
|
||||
def play_round(self) -> Dict[str, int]:
|
||||
"""Play a round of the Hearts card game"""
|
||||
deck = Deck.create(shuffle=True)
|
||||
for player, hand in zip(self.players, deck.deal(4)):
|
||||
player.hand.add_cards(hand.cards)
|
||||
start_player = next(
|
||||
p for p in self.players if p.has_card(Card("♣", "2"))
|
||||
)
|
||||
tricks = {p.name: Deck([]) for p in self.players}
|
||||
hearts = False
|
||||
|
||||
# Play cards from each player's hand until empty
|
||||
while start_player.hand:
|
||||
played: List[Card] = []
|
||||
turn_order = self.player_order(start=start_player)
|
||||
for player in turn_order:
|
||||
card = player.play_card(played, hearts_broken=hearts)
|
||||
played.append(card)
|
||||
start_player = self.trick_winner(played, turn_order)
|
||||
tricks[start_player.name].add_cards(played)
|
||||
print(f"{start_player.name} wins the trick\n")
|
||||
hearts = hearts or any(c.suit == "♡" for c in played)
|
||||
return self.count_points(tricks)
|
||||
|
||||
def player_order(self, start: Optional[Player] = None) -> List[Player]:
|
||||
"""Rotate player order so that start goes first"""
|
||||
if start is None:
|
||||
start = random.choice(self.players)
|
||||
start_idx = self.players.index(start)
|
||||
return self.players[start_idx:] + self.players[:start_idx]
|
||||
|
||||
@staticmethod
|
||||
def trick_winner(trick: List[Card], players: List[Player]) -> Player:
|
||||
lead = trick[0].suit
|
||||
valid = [
|
||||
(c.value, p) for c, p in zip(trick, players) if c.suit == lead
|
||||
]
|
||||
return max(valid)[1]
|
||||
|
||||
@staticmethod
|
||||
def count_points(tricks: Dict[str, Deck]) -> Dict[str, int]:
|
||||
return {n: sum(c.points for c in cards) for n, cards in tricks.items()}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Read player names from the command line
|
||||
player_names = sys.argv[1:]
|
||||
game = HeartsGame(*player_names)
|
||||
game.play()
|
||||
Reference in New Issue
Block a user