diff --git a/docs/index.rst b/docs/index.rst index f67aaef..d3ebd6b 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -27,11 +27,13 @@ /src/games/tutorials/hanoi /src/games/tutorials/slicing /src/games/tutorials/nqueens + /src/games/tutorials/fieldwalk /src/games/tutorials/coins /src/games/tutorials/nim /src/games/tutorials/tictactoe /src/games/tutorials/mastermind /src/games/tutorials/prisoner + /src/games/tutorials/highestcard .. toctree:: diff --git a/docs/resources/images/cards.jpeg b/docs/resources/images/cards.jpeg new file mode 100644 index 0000000..1361fe1 Binary files /dev/null and b/docs/resources/images/cards.jpeg differ diff --git a/docs/resources/images/fieldwalk.png b/docs/resources/images/fieldwalk.png new file mode 100644 index 0000000..0158115 Binary files /dev/null and b/docs/resources/images/fieldwalk.png differ diff --git a/docs/src/games/games.rst b/docs/src/games/games.rst index 0448afe..a5b5f17 100644 --- a/docs/src/games/games.rst +++ b/docs/src/games/games.rst @@ -174,6 +174,15 @@ This is a list of all the games supported and its characteristics: - Perfect information - *Min score*: 0 + * - **FieldWalk** + - ``IArena.games.FieldWalk`` + - :ref:`fieldwalk_tutorial` + - Minimum cost path search. + - 1 + - Deterministic + - Perfect information + - + * - **Coins** - ``IArena.games.Coins`` - :ref:`coins_tutorial` @@ -218,3 +227,12 @@ This is a list of all the games supported and its characteristics: - Deterministic - Hidden information - + + * - **Highest card** + - ``IArena.games.HighestCard`` + - :ref:`highestcard_tutorial` + - Highest card N player game. + - N + - Random + - Hidden information + - diff --git a/docs/src/games/tutorials/fieldwalk.rst b/docs/src/games/tutorials/fieldwalk.rst new file mode 100644 index 0000000..d5ac07f --- /dev/null +++ b/docs/src/games/tutorials/fieldwalk.rst @@ -0,0 +1,141 @@ +.. _fieldwalk_tutorial: + +######### +FieldWalk +######### + +.. figure:: /resources/images/fieldwalk.png + :scale: 60% + +This game represents a search in a square graph of the shortest path. +In a square of ``NxM``, each cell has a cost that represents the cost of passing through that cell. +From each cell, the player can go to one of the 4 adjacent cells (up, down, left, right). +The objective is to reach the bottom-right cell ``[N-1,M-1]`` from the top-left cell ``[0,0]`` with the minimum cost. + +==== +Goal +==== + +Starting at ``[0,0]``, the goal is to reach ``[N-1,M-1]`` with the minimum cost. + +----- +Score +----- + +The score of the game is the sum of the values of the squares that the player has passed through (initial square does not count). + + +====== +Import +====== + +.. code-block:: python + + import IArena.games.FieldWalk.FieldWalkPosition as FieldWalkPosition + import IArena.games.FieldWalk.FieldWalkMovement as FieldWalkMovement + import IArena.games.FieldWalk.FieldWalkRules as FieldWalkRules + + +======== +Movement +======== + +A movement is represented an ``int``: ``direction``. + +- ``direction`` + - ``int`` + - ``0 <= direction <= 3`` + - Indicates the direction of the movement + - Some movements are not possible if they go out of the board + +--------- +Direction +--------- + +Enumeration of the possible directions: + +- ``Up`` + - ``0`` +- ``Down`` + - ``1`` +- ``Left`` + - ``2`` +- ``Right`` + - ``3`` + + +.. code-block:: python + + # To move up + movement = FieldWalkMovement.up() + # or + movement = FieldWalkMovement( + direction=FieldWalkMovement.Direction.Up) + + + +======== +Position +======== + +A position is represented by 3 ``int``. +One indicates the x axis, other the y axis and the last one the cost to arrive to the current position. + + +.. code-block:: python + + # position : FieldWalkPosition + position.x # X axis [0, N-1] + position.y # Y axis [0, M-1] + position.cost # Cost to arrive to this position + + +===== +Rules +===== + +This games has every methods of :ref:`IRules `. + +----------- +Constructor +----------- + +Can receive the map, or let it be created randomly. + +.. code-block:: python + + # Initiate with a map of 2x2 with cost 1 + rules = FieldWalkRules(initial_map=FieldWalkMap([[1,1],[1,1]])) + + # Initial position board 5x4 with random cost + rules = FieldWalkRules(rows=5, cols=4) + + # Replicable initial position board 5x4 with random cost + rules = FieldWalkRules(rows=5, cols=4, seed=0) + + +--- +Map +--- + +This game counts with a class ``FieldWalkMap`` that represents the grid of the game. +This is created from a ``List[List[int]]``. +The method ``get_matrix()`` returns the list of lists with all the values. + +.. code-block:: python + + # get the FieldWalkMap + fw_map = rules.get_map() + + # Get the size + N, M = len(fw_map) + # or + N, M = fw_map.goal() + + # Get the matrix of the map + fw_map.get_matrix().get_matrix() + + # Get the value of the final position + value = fw_map.get_matrix()[N-1][M-1] + # or + value = fw_map[N-1,M-1] diff --git a/docs/src/games/tutorials/highestcard.rst b/docs/src/games/tutorials/highestcard.rst new file mode 100644 index 0000000..b05356f --- /dev/null +++ b/docs/src/games/tutorials/highestcard.rst @@ -0,0 +1,113 @@ +.. _highestcard_tutorial: + +############ +Highest Card +############ + +.. figure:: /resources/images/cards.jpeg + :scale: 80% + +This game is a card game with ``N`` players and ``NxM`` cards. +The objective of the game is to guess the number of rounds that the player will win with its ``M`` cards. +Each player guesses in secret at the begining knowing only their ``M`` cards, for how many rounds it will win. +Then, ``M`` rounds are played in which each player plays its highest card. +The player that gets closer to the real number of rounds that it wins without passing it, wins the game. + + +==== +Goal +==== + +Guess the number of rounds your card will be the higher, trying not to pass it. + +----- +Score +----- + +The score is calculated as: + +- If the player guesses correctly, it gets ``-5`` point. +- If the player guesses lower than the real number, it gets ``1`` point for each round it passes. +- If the player guesses higher than the real number, it gets ``2`` point for each round it passes. + + +====== +Import +====== + +.. code-block:: python + + import IArena.games.HighestCard.HighestCardPosition as HighestCardPosition + import IArena.games.HighestCard.HighestCardMovement as HighestCardMovement + import IArena.games.HighestCard.HighestCardRules as HighestCardRules + + +======== +Movement +======== + +A movement is an ``int`` with the guess of number of rounds: + +- ``bet`` + - ``int`` + - ``0 <= bet <= M`` + - Number of rounds it guesses it will win + + +.. code-block:: python + + # For guessing 0 rounds win + movement = HighestCardMovement(bet=0) + + # For guessing 3 rounds win + movement = HighestCardMovement(bet=3) + + +======== +Position +======== + +Each player can get its cards from the position, and only its cards. + +.. code-block:: python + + # position = HighestCardPosition + + # For getting the cards of the player + cards = position.get_cards() + + # Get first card + card = cards[0] + + +===== +Rules +===== + +This games has every methods of :ref:`IRules `. + +It counts with 2 method to retrieve the values of the game + +- ``rules.n_players() -> int`` +- ``rules.m_cards() -> int`` + + +----------- +Constructor +----------- + +The rules can be created with the cards already deal or with a seed to generate random decks. + + .. code-block:: python + + # Default game + rules = HighestCardRules() + # or + rules = HighestCardRules(n_players=3, m_cards=4) + + # Replicable game + rules = HighestCardRules(n_players=3, m_cards=4, seed=0) + + # With cards already deal for 2 player game with 2 cards + cards_distribution = {0: [0, 1], 1: [2, 3]} + rules = HighestCardRules(cards_distribution=cards_distribution) diff --git a/docs/src/games/tutorials/mastermind.rst b/docs/src/games/tutorials/mastermind.rst index e63b5d3..aeb79e6 100644 --- a/docs/src/games/tutorials/mastermind.rst +++ b/docs/src/games/tutorials/mastermind.rst @@ -5,7 +5,7 @@ Mastermind ########## .. figure:: /resources/images/mastermind.png - :scale: 40% + :scale: 2% This game is the classical Mastermind game. The objective of the game is to guess the *secret code*, this is a sequence of *N* numbers (color pegs) chosen from *M* numbers available ``[0,M)``. @@ -119,7 +119,7 @@ Constructor There are 2 ways to construct the rules: -#. Using a secret code already defined. +1. Using a secret code already defined. .. code-block:: python @@ -130,7 +130,7 @@ There are 2 ways to construct the rules: rules = MastermindRules(secret=[0, 0, 0, 0, 0, 0, 0, 7], m=8) -#. Setting arguments ``n: int`` and ``m: int`` in order to generate a random secret code. +2. Setting arguments ``n: int`` and ``m: int`` in order to generate a random secret code. Using argument ``seed: int`` the random generation can be reproduced. .. code-block:: python diff --git a/docs/src/games/tutorials/slicing.rst b/docs/src/games/tutorials/slicing.rst index 5cc4b0a..8ae70b2 100644 --- a/docs/src/games/tutorials/slicing.rst +++ b/docs/src/games/tutorials/slicing.rst @@ -5,7 +5,7 @@ Slicing Puzzle ############## .. figure:: /resources/images/slicing.jpg - :scale: 60% + :scale: 10% This game is the classical Slicing puzzle. In a board of ``N x N`` squares, where every square has a number between ``[0, N-2]``, there is one of the squares that is *empty* (``-1``). diff --git a/src/IArena/games/FieldWalk.py b/src/IArena/games/FieldWalk.py index 2a2255f..4997ff5 100644 --- a/src/IArena/games/FieldWalk.py +++ b/src/IArena/games/FieldWalk.py @@ -9,6 +9,7 @@ from IArena.interfaces.IGameRules import IGameRules from IArena.interfaces.PlayerIndex import PlayerIndex from IArena.utils.decorators import override +from IArena.interfaces.Score import ScoreBoard """ This game represents a grid search where each square has a different weight. @@ -29,12 +30,39 @@ class FieldWalkMovement(IMovement): Right: 3 - Move right. """ - class Values(Enum): + class Direction(Enum): Up = 0 Down = 1 Left = 2 Right = 3 + def __init__( + self, + direction: Direction): + self.direction = direction + + def __eq__( + self, + other: "FieldWalkMovement"): + return self.direction == other.direction + + def __str__(self): + return f'{self.direction.name}' + + def up() -> "FieldWalkMovement": + return FieldWalkMovement(FieldWalkMovement.Direction.Up) + + def down() -> "FieldWalkMovement": + return FieldWalkMovement(FieldWalkMovement.Direction.Down) + + def left() -> "FieldWalkMovement": + return FieldWalkMovement(FieldWalkMovement.Direction.Left) + + def right() -> "FieldWalkMovement": + return FieldWalkMovement(FieldWalkMovement.Direction.Right) + + + class FieldWalkPosition(IPosition): """ @@ -48,9 +76,11 @@ class FieldWalkPosition(IPosition): def __init__( self, + rules: "FieldWalkRules", x: int, y: int, cost: int): + super().__init__(rules) self.x = x self.y = y self.cost = cost @@ -68,6 +98,9 @@ def __eq__( def __str__(self): return f'{{[x: {self.x}, y: {self.y}] accumulated cost: {self.cost}}}' + def cost(self) -> int: + return self.cost + class FieldWalkMap: @@ -77,10 +110,10 @@ def __init__( self.squares = squares def __str__(self): - return '\n'.join([' '.join(["%0:4d".format(square) for square in row]) for row in self.squares]) + return '\n'.join([' '.join(["{0:4d}".format(square) for square in row]) for row in self.squares]) def __len__(self): - return len(self.squares) + return (len(self.squares), len(self.squares[0])) def __getitem__(self, i, j): return self.squares[i][j] @@ -95,8 +128,11 @@ def get_matrix(self) -> List[List[int]]: return self.squares @staticmethod - def generate_random_map(rows: int, cols: int, seed: int = 0): - random.seed(seed) + def generate_random_map(rows: int, cols: int, seed: int = None): + + if seed is not None: + random.seed(seed) + lambda_parameter = 0.5 # You can adjust this to your preference def exponential_random_number(): @@ -108,24 +144,28 @@ def exponential_random_number(): def get_possible_movements(self, position: FieldWalkPosition) -> List[FieldWalkMovement]: result = [] if position.x > 0: - result[FieldWalkMovement.Up] = self.squares[position.x - 1][position.y] + result.append(FieldWalkMovement(FieldWalkMovement.Direction.Up)) if position.x < len(self.squares) - 1: - result[FieldWalkMovement.Down] = self.squares[position.x + 1][position.y] + result.append(FieldWalkMovement(FieldWalkMovement.Direction.Down)) if position.y > 0: - result[FieldWalkMovement.Left] = self.squares[position.x][position.y - 1] + result.append(FieldWalkMovement(FieldWalkMovement.Direction.Left)) if position.y < len(self.squares[position.x]) - 1: - result[FieldWalkMovement.Right] = self.squares[position.x][position.y + 1] + result.append(FieldWalkMovement(FieldWalkMovement.Direction.Right)) return result - def get_next_position(self, position: FieldWalkPosition, movement: FieldWalkMovement) -> FieldWalkPosition: - if movement == FieldWalkMovement.Up: - return FieldWalkPosition(position.x - 1, position.y, position.cost + self.squares[position.x - 1][position.y]) - if movement == FieldWalkMovement.Down: - return FieldWalkPosition(position.x + 1, position.y, position.cost + self.squares[position.x + 1][position.y]) - if movement == FieldWalkMovement.Left: - return FieldWalkPosition(position.x, position.y - 1, position.cost + self.squares[position.x][position.y - 1]) - if movement == FieldWalkMovement.Right: - return FieldWalkPosition(position.x, position.y + 1, position.cost + self.squares[position.x][position.y + 1]) + def get_next_position( + self, + position: FieldWalkPosition, + movement: FieldWalkMovement, + rules: "FieldWalkRules") -> FieldWalkPosition: + if movement.direction == FieldWalkMovement.Direction.Up: + return FieldWalkPosition(rules, position.x - 1, position.y, position.cost + self.squares[position.x - 1][position.y]) + if movement.direction == FieldWalkMovement.Direction.Down: + return FieldWalkPosition(rules, position.x + 1, position.y, position.cost + self.squares[position.x + 1][position.y]) + if movement.direction == FieldWalkMovement.Direction.Left: + return FieldWalkPosition(rules, position.x, position.y - 1, position.cost + self.squares[position.x][position.y - 1]) + if movement.direction == FieldWalkMovement.Direction.Right: + return FieldWalkPosition(rules, position.x, position.y + 1, position.cost + self.squares[position.x][position.y + 1]) class FieldWalkRules(IGameRules): @@ -135,7 +175,7 @@ def __init__( initial_map: FieldWalkMap = None, rows: int = 10, cols: int = 10, - seed: int = 0): + seed: int = None): """ Args: initial_map: The map of the game. If none, it is generated randomly. @@ -158,6 +198,7 @@ def n_players(self) -> int: @override def first_position(self) -> FieldWalkPosition: return FieldWalkPosition( + rules=self, x=0, y=0, cost=0) @@ -167,7 +208,7 @@ def next_position( self, movement: FieldWalkMovement, position: FieldWalkPosition) -> FieldWalkPosition: - return self.map.get_next_position(position, movement) + return self.map.get_next_position(position, movement, self) @override def possible_movements( @@ -179,10 +220,12 @@ def possible_movements( def finished( self, position: FieldWalkPosition) -> bool: - return position.map.is_goal(position) + return self.map.is_goal(position) @override def score( self, - position: FieldWalkPosition) -> dict[PlayerIndex, float]: - return {PlayerIndex.FirstPlayer : position.cost} + position: FieldWalkPosition) -> ScoreBoard: + s = ScoreBoard() + s.add_score(PlayerIndex.FirstPlayer, position.cost) + return s diff --git a/src/IArena/games/HighestCard.py b/src/IArena/games/HighestCard.py index 3b23f0d..4c16c83 100644 --- a/src/IArena/games/HighestCard.py +++ b/src/IArena/games/HighestCard.py @@ -8,6 +8,7 @@ from IArena.interfaces.IGameRules import IGameRules from IArena.interfaces.PlayerIndex import PlayerIndex from IArena.utils.decorators import override +from IArena.interfaces.Score import ScoreBoard """ This game represents the HighestCard game. @@ -61,7 +62,7 @@ def __init__( else: self.__cards = deepcopy(previous.__cards) self.__bet = deepcopy(previous.__bet) - self.__bet[self.next_player()](next_bet) + self.__bet[self.next_player()] = next_bet @override def next_player( @@ -89,7 +90,7 @@ def __str__(self): # Print each guess in a line together with the correctness return f'{{ Next player: {self.next_player()} | Cards: {self.__cards[self.next_player()]}}}' - def calculate_score(self): + def _calculate_score(self) -> ScoreBoard: # Calculate how many rounds each player has won round_win = [0 for _ in range(self.number_players())] for i in range(self.number_cards()): @@ -103,13 +104,16 @@ def calculate_score(self): # Calculate the score of each player depending on its bet score = {} + score = ScoreBoard() for i in range(self.number_players()): - if self.__bet[i] == round_win[i]: - score[i] = -5 - elif self.__bet[i] < round_win[i]: - score[i] = 1 * (round_win[i] - self.__bet[i]) + if self.__bet[i].bet == round_win[i]: + score.add_score(i, -5) + elif self.__bet[i].bet < round_win[i]: + score.add_score(i, 1 * (round_win[i] - self.__bet[i].bet)) else: - score[i] = 2 * (round_win[i] - self.__bet[i]) + score.add_score(i, 2 * (self.__bet[i].bet - round_win[i])) + + return score class HighestCardRules(IGameRules): @@ -119,11 +123,14 @@ def __init__( cards_distribution: Dict[PlayerIndex, List[int]] = None, n_players: int = 3, m_cards: int = 4, - seed: int = 0): + seed: int = None): if cards_distribution is None: cards = [i for i in range(n_players*m_cards)] - random.seed(seed) + + if seed is not None: + random.seed(seed) + random.shuffle(cards) self.__cards = {} @@ -148,6 +155,9 @@ def __init__( def n_players(self) -> int: return self.n + def m_cards(self) -> int: + return self.m + @override def first_position(self) -> HighestCardPosition: return HighestCardPosition( @@ -181,5 +191,5 @@ def finished( @override def score( self, - position: HighestCardPosition) -> dict[PlayerIndex, float]: - return position.calculate_score() + position: HighestCardPosition) -> ScoreBoard: + return position._calculate_score() diff --git a/src/IArena/games/PrisonerDilemma.py b/src/IArena/games/PrisonerDilemma.py index 3afd0db..5171015 100644 --- a/src/IArena/games/PrisonerDilemma.py +++ b/src/IArena/games/PrisonerDilemma.py @@ -43,7 +43,10 @@ def __eq__( return self.decision == other.decision def __str__(self): - return f'{self.decision}' + if self.decision == PrisonerDilemmaMovement.Cooperate: + return "" + else: + return "" def cooperate() -> int: return PrisonerDilemmaMovement.Cooperate @@ -66,8 +69,11 @@ def __init__(self, score_table: Dict[PrisonerDilemmaMovement, Dict[PrisonerDilem def __str__(self): return str(self.score_table) - def generate_random_table(seed: int = 0) -> "PrisonerDilemmaScoreTable": - random.seed(seed) + def generate_random_table(seed: int = None) -> "PrisonerDilemmaScoreTable": + + if seed is not None: + random.seed(seed) + scores = sorted([random.random() for _ in range(4)]) return PrisonerDilemmaScoreTable({ PrisonerDilemmaMovement.Cooperate: { @@ -81,7 +87,7 @@ def generate_random_table(seed: int = 0) -> "PrisonerDilemmaScoreTable": }) def score(self, player_movement: PrisonerDilemmaMovement, opponent_movement: PrisonerDilemmaMovement) -> float: - return self.score_table[player_movement][opponent_movement] + return self.score_table[player_movement.decision][opponent_movement.decision] class PrisonerDilemmaPosition(IPosition): @@ -148,7 +154,7 @@ class PrisonerDilemmaRules(IGameRules): def __init__( self, score_table: PrisonerDilemmaScoreTable = None, - seed: int = 0): + seed: int = None): """ Args: initial_position: The number of PrisonerDilemma at the beginning of the game. @@ -186,8 +192,8 @@ def possible_movements( self, position: PrisonerDilemmaPosition) -> Iterator[PrisonerDilemmaMovement]: return [ - PrisonerDilemmaMovement.Cooperate, - PrisonerDilemmaMovement.Defect + PrisonerDilemmaMovement(PrisonerDilemmaMovement.Cooperate), + PrisonerDilemmaMovement(PrisonerDilemmaMovement.Defect) ] @override