From 9118e9231303c784ca808c70b14ec12acb15a518 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Abiga=C3=ABlle=20Martin?= Date: Fri, 12 Sep 2025 10:18:08 +0200 Subject: [PATCH] feat/#0: added code from before the initialization of the repository. --- src/__init__.py | 0 src/board.py | 321 ++++++++++++++++++++++++++++++++++++++++++++++++ src/decision.py | 51 ++++++++ src/main.py | 6 + 4 files changed, 378 insertions(+) create mode 100644 src/__init__.py create mode 100644 src/board.py create mode 100644 src/decision.py create mode 100644 src/main.py diff --git a/src/__init__.py b/src/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/board.py b/src/board.py new file mode 100644 index 0000000..2b1d06b --- /dev/null +++ b/src/board.py @@ -0,0 +1,321 @@ +# Python STD. +import random +from collections import defaultdict +from dataclasses import dataclass +from enum import StrEnum +from typing import Callable, Final + + +# Project packages. + + +# Number of players that can play the game +MIN_NB_PLAYERS: int = 3 +MAX_NB_PLAYERS: int = 8 + + +class TrapType(StrEnum): + """Type of traps that can be encountered during a round.""" + + BOULDER = "Boulder" + LAVA = "Lava" + LOG = "Log" + SERPENT = "Serpent" + SPIDER = "Spider" + +@dataclass +class Relic: + """A Relic that can be encountered during a round.""" + + # The value of the relic, i.e. its equivalence with gems. + value: int + + +class Deck: + """ + Content of the game's deck, it should contain all the logic about drawing card for + the exploration of a single dungeon. + """ + + # List of cards to be put in a deck. + CARDS_GEMS: Final[tuple[int]] = [1, 2, 3, 4, 5, 5, 7, 7, 9, 11, 11, 13, 14, 15, 17] + CARDS_RELIC: Final[tuple[Relic]] = [ + Relic(5), + Relic(7), + Relic(8), + Relic(10), + Relic(12), + ] + CARDS_TRAPS: Final[tuple[str]] = [ + TrapType.BOULDER, + TrapType.LAVA, + TrapType.LOG, + TrapType.SERPENT, + TrapType.SPIDER, + ] + NB_TRAPS: Final[int] = 3 + + def __init__(self, i_round: int, lst_traps_removed: list[TrapType]) -> None: + """ + Initialize the deck where cards used to explore the dungeon are going to be drawn. + + :param lst_traps_removed: The list of trap cards to remove, e.g. if a trap was the reason + for a cave collapse, it needs to be removed. + """ + # The deck from which cards can be drawn. + self._lst_cards: list[int | Relic | str] = ( + list(Deck.CARDS_GEMS) + + [Deck.CARDS_RELIC[i_round]] + + list(Deck.CARDS_TRAPS * Deck.NB_TRAPS) + ) + # Remove from the deck traps already encountered. + for trap in lst_traps_removed: + self._lst_cards.remove(trap) + + # Number of cards drawn. + self.nb_cards_drawn: int = 0 + + def draw(self) -> int | Relic | str: + drawn_card: int | Relic | str = random.choice(self._lst_cards) + self._lst_cards.remove(drawn_card) + self.nb_cards_drawn += 1 + return drawn_card + + +class Round: + """ + Model a round, i.e. the exploration of a cave until it collapses or until everyone leaves. + """ + + def __init__(self, nb_player: int, deck: Deck, lst_player_decision: list[Callable]) -> None: + """ + Initialize the round. + :param nb_player: The number of players in the game. + :param deck: The deck that the round will use. + """ + # The list of players still playing the round. + self._lst_player_exploring: list[bool] = [True] * nb_player + # Entry point for players' decision. + self._lst_player_decision: list[Callable] = lst_player_decision + + # The deck that will be used during this round. + self._deck: Deck = deck + + # Value of gems held by each player during a round. + # During a round, if two similar traps are discovered all those gems are lost. + self._lst_gems_held: list[int] = [0] * nb_player + # Value of gems saved by each player by quitting the cave. + self.lst_gems_saved: list[int] = [0] * nb_player + + # Value of gems left on the floor + self._lst_gems_onfloor: list[int] = [] + # The relic on the floor if it has been dropped and not collected yet. + self._relic_onfloor: Relic | None = None + + # Initialize the collapse conditions. + self._dct_trap_activated: dict[TrapType, bool] = defaultdict(lambda: False) + self.collapse_reason: TrapType | None = None + self._cave_collapsed: bool = False + self._everyone_quit: bool = False + + def _debug_round_status(self) -> str: + """ + Text with the status of each player. + """ + txt_debug: str = "(" + + # + lst_txt_player_status: list[str] = [] + for _i_player, is_player_exploring in enumerate(self._lst_player_exploring): + lst_txt_player_status.append( + f"P{_i_player}: " + f"{is_player_exploring}/" + f"{self._lst_gems_held[_i_player]}H/" + f"{self.lst_gems_saved[_i_player]}S" + ) + txt_debug += ", ".join(lst_txt_player_status) + ")" + txt_debug += f" + {self._lst_gems_onfloor}" + if self._relic_onfloor is not None: + txt_debug += f" + {self._relic_onfloor.value}R" + else: + txt_debug += " + None" + + return txt_debug + + def play_round(self) -> None: + """ + Play one round of the game until everyone quit the cave or until the cave collapses. + """ + # Draw cards until everyone quit the cave or until the cave collapses. + while not self._cave_collapsed and not self._everyone_quit: + # Draw a card. + card_drawn: int | Relic | str = self._deck.draw() + + match card_drawn: + case int(gems_value): + self.on_gems_drawn(gems_value) + case Relic() as relic: + self.on_relic_drawn(relic) + case TrapType() as trap_type: + self.on_trap_drawn(trap_type) + case _: + print("Error") + break + + if not self._cave_collapsed: + self.make_players_decision() + + def on_gems_drawn(self, gem_value: int) -> None: + """ + Logic executed when a gem card is drawn. + :param gem_value: The number of gems found with the gem card. + """ + # Split the gems between every remaining player equally. + nb_player = self._lst_player_exploring.count(True) + gem_per_player: int = gem_value // nb_player + gem_onfloor: int = gem_value % nb_player + + # Place the gems on the floor. + self._lst_gems_onfloor.append(gem_onfloor) + + # Give the gems to every remaining players. + for _i_player, is_player_exploring in enumerate(self._lst_player_exploring): + if is_player_exploring: + self._lst_gems_held[_i_player] += gem_per_player + + print(f"Gem found: {gem_value} -> " + self._debug_round_status()) + + def on_relic_drawn(self, relic: Relic) -> None: + """ + Logic executed when a relic card is drawn. + :param relic: The relic found. + """ + print(f"Relic found: {relic.value}") + # Put the relic on the floor. + self._relic_onfloor = relic + + def on_trap_drawn(self, trap_type: TrapType) -> None: + """ + Logic executed when a trap card is drawn. + :param trap_type: The trap triggered. + """ + + if self._dct_trap_activated[trap_type]: + # Since the trap has made the cave collapse, it should be removed for + # the next round. + self.collapse_reason = trap_type + self._cave_collapsed = True + else: + self._dct_trap_activated[trap_type] = True + + print(f"Trap activated: {trap_type} -> " + self._debug_round_status()) + + def make_players_decision(self) -> None: + """ + Logic about players making their decision about staying in the cave or not. + """ + # For each player that is exploring, ask them if they want to leave the cave or continue + # its exploration. + lst_i_player_quitting: list[int] = [] + for _i_player, is_player_exploring in enumerate(self._lst_player_exploring): + # If the player is not exploring, skip their turn. + if not is_player_exploring: + continue + + # Player's decision about quitting. + is_player_quitting: bool = self._lst_player_decision[_i_player](i_card=self._deck.nb_cards_drawn) + + # Keep if the player is quitting or not. + if is_player_quitting: + lst_i_player_quitting.append(_i_player) + + + # Check if the gems needs to be collected. + nb_quitting: int = len(lst_i_player_quitting) + if nb_quitting != 0: + # Compute the gems each leaving player will have. + gems_per_player: int = sum([ + _nb_gems // nb_quitting for _nb_gems in self._lst_gems_onfloor + ]) + # Update the number of gems that will remain on the floor. + self._lst_gems_onfloor = [ + _nb_gems % nb_quitting for _nb_gems in self._lst_gems_onfloor + ] + + # Give the gems to the leaving players and mark them as leavers. + for _i_player in lst_i_player_quitting: + self.lst_gems_saved[_i_player] = self._lst_gems_held[_i_player] + gems_per_player + self._lst_gems_held[_i_player] = 0 + self._lst_player_exploring[_i_player] = False + + # Check if only one person is leaving and if the relic is on the floor. + if nb_quitting == 1 and self._relic_onfloor is not None: + self.lst_gems_saved[lst_i_player_quitting[0]] += self._relic_onfloor.value + self._relic_onfloor = None + + # Check if all players have quit the cave or not. + if self._lst_player_exploring.count(True) == 0: + self._everyone_quit = True + +class Board: + """ + Model the whole game board. + """ + + # Number of rounds in a single game. + NB_ROUND: Final[int] = 5 + + def __init__(self) -> None: + # The number of player playing the game. + self._nb_player: int = 3 + + # Value of gems in each player's chest. + self._lst_chest_content: list[int] = [0] * self._nb_player + + # Entry point for players' decision. + self._lst_player_decision: list[Callable] = [] + + # ##### Board Status ##### # + # Number of caves already explored. + self.i_round: int = 0 + + # List of traps removed from the next rounds. + self._lst_traps_removed: list[TrapType] = [] + + def play_game(self) -> None: + """ + Play the whole five rounds. + """ + current_round: Round + while self.i_round < Board.NB_ROUND: + print("------------------------") + print(f"Round: {self.i_round + 1}") + + # Initialize the deck used for this round. + deck = Deck(self.i_round, self._lst_traps_removed) + + # Initialize the round and play it. + current_round = Round(self._nb_player, deck, self._lst_player_decision) + current_round.play_round() + + # Retrieve the result of the current round. + collapse_reason = current_round.collapse_reason + if collapse_reason is not None: + print(f"Trap removed: {collapse_reason}") + self._lst_traps_removed.append(collapse_reason) + + lst_saved_gems: list[int] = current_round.lst_gems_saved + for i_player in range(self._nb_player): + self._lst_chest_content[i_player] += lst_saved_gems[i_player] + print( + "(" + + ", ".join( + [ + f"P{_i_player}: {self._lst_chest_content[_i_player]} gems" + for _i_player in range(self._nb_player) + ] + ) + + ")" + ) + + self.i_round += 1 \ No newline at end of file diff --git a/src/decision.py b/src/decision.py new file mode 100644 index 0000000..3fae62a --- /dev/null +++ b/src/decision.py @@ -0,0 +1,51 @@ +# Python STD. +import random +import math + +# Project packages. +from board import Board + +class MetaBoard(Board): + + + def __init__(self) -> None: + super().__init__() + + self._lst_player_decision = [Quit5Decision().is_quitting, RandomHalfDecision().is_quitting, RandomAtanDecision().is_quitting] + + +class BasicDecision: + """Basic decision about leaving the cave or not.""" + + def is_quitting(self, **kwargs) -> bool: + return False + + +class RandomHalfDecision(BasicDecision): + + def is_quitting(self, **kwargs) -> bool: + return random.random() < 0.5 + + +class RandomAtanDecision(BasicDecision): + + def is_quitting(self, **kwargs) -> bool: + + if "i_card" not in kwargs: + print("RandomAtanDecision needs parameter \"i_card\" for decision.") + return True + + # Probability limit for the player to leave the game depending on the number of cards + # discovered. + threshold: float = math.atan((kwargs["i_card"] - 1) / 5) / (math.pi / 2) + return random.random() < threshold + +class Quit5Decision(BasicDecision): + + def is_quitting(self, **kwargs) -> bool: + if "i_card" not in kwargs: + print("RandomAtanDecision needs parameter \"i_card\" for decision.") + return True + + # Quit after five rounds. + return kwargs["i_card"] > 5 \ No newline at end of file diff --git a/src/main.py b/src/main.py new file mode 100644 index 0000000..4230f57 --- /dev/null +++ b/src/main.py @@ -0,0 +1,6 @@ +from decision import MetaBoard + + +if __name__ == "__main__": + board = MetaBoard() + board.play_game()