136 lines
3.7 KiB
Python
136 lines
3.7 KiB
Python
|
|
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)
|