import random from collections import Counter from dataclasses import dataclass @dataclass class Bid: die_face: int num_dice: int class EngineHandler: def on_game_started(self, game_state: "LiarsDiceEngine"): pass def on_new_round(self): pass def on_bid_placed(self, bid): pass def on_challenge_bid(self): pass def on_player_eliminated(self, player_index): pass def get_players_turn(self) -> Bid | None: pass @dataclass class LiarsDiceEngine: NUM_INITIAL_DICE = 5 WILD_DIE_FACE = 1 listener: EngineHandler num_players: int round_number: int wild_ones_variant: bool active_player_index: int current_bid: Bid | None dice: list[Counter[int]] @staticmethod def new_game( num_players: int, wild_ones_variant: bool, listener: EngineHandler ) -> "LiarsDiceEngine": return LiarsDiceEngine( num_players=num_players, active_player_index=random.randint(0, num_players - 1), wild_ones_variant=wild_ones_variant, current_bid=None, dice=[ Counter([random.randint(1, 6) for _ in range(LiarsDiceEngine.NUM_INITIAL_DICE)]) for _ in range(num_players) ], round_number=0, listener=listener, ) def start_game(self) -> None: self.listener.on_game_started(self) while self.num_players > 1: self.new_round() while True: bid = self.listener.get_players_turn() if bid: self.place_bid(bid) else: self.challenge_current_bid() break def new_round(self): self.current_bid = None self.active_player_index = self.active_player_index % self.num_players self.round_number += 1 self.listener.on_new_round() def place_bid(self, bid: Bid): self.current_bid = bid self.listener.on_bid_placed(bid) self.active_player_index = (self.active_player_index + 1) % self.num_players def challenge_current_bid(self): self.listener.on_challenge_bid() losing_player_index = ( self.active_player_index if self.is_current_bid_successful() else self.active_player_index - 1 ) num_dice_per_player = [c.total() for c in self.dice] if num_dice_per_player[losing_player_index] > 1: num_dice_per_player[losing_player_index] -= 1 else: self.listener.on_player_eliminated(losing_player_index) self.num_players -= 1 num_dice_per_player = [ d for (i, d) in enumerate(num_dice_per_player) if i != losing_player_index ] self.active_player_index = losing_player_index self.dice = [ Counter([random.randint(1, 6) for _ in range(num_dice_per_player[p])]) for p in range(self.num_players) ] def is_current_bid_successful(self) -> bool: num_wild_dice = ( self.num_all_dice_with_value(self.WILD_DIE_FACE) if self.wild_ones_variant else 0 ) return ( self.num_all_dice_with_value(self.current_bid.die_face) + num_wild_dice >= self.current_bid.num_dice ) @property def num_opponent_dice(self) -> int: return sum(c.total() for (i, c) in enumerate(self.dice) if i != self.active_player_index) @property def num_all_dice(self) -> int: return sum(c.total() for c in self.dice) def num_all_dice_with_value(self, die_face: int) -> int: return sum(c[die_face] for c in self.dice)