diff --git a/.gitignore b/.gitignore index 68bc17f..b6520d6 100644 --- a/.gitignore +++ b/.gitignore @@ -158,3 +158,7 @@ cython_debug/ # and can be added to the global gitignore or merged into this file. For a more nuclear # option (not recommended) you can uncomment the following to ignore the entire idea folder. #.idea/ + + +# .test for manual testing +.test/ diff --git a/README.md b/README.md index 7f89167..2878091 100644 --- a/README.md +++ b/README.md @@ -48,3 +48,11 @@ There are some resources under `resources` folder with util information and impl - `play_hanoi.ipynb`: Jupyter notebook to play Towers of Hanoi. It installs and plays all along. - `create_player_coins.ipynb`: Jupyter notebook with everything set up to create a player and play against a random player `Coins` game. + +--- + +## Installation + +### From source + +```bash diff --git a/docs/conf.py b/docs/conf.py index 9084b1b..a877bcb 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -3,3 +3,12 @@ extensions = ['sphinx.ext.autodoc'] html_theme = 'sphinx_rtd_theme' + + +# The suffix(es) of source filenames. +# You can specify multiple suffix as a list of string: +# +source_suffix = ['.rst', '.md'] + +# The master toctree document. +master_doc = 'index' diff --git a/docs/index.rst b/docs/index.rst index 45b5956..82a1a1f 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,18 +1,21 @@ +.. include:: src/titlepage.rst + .. toctree:: - :caption: Introduction + :caption: Project :maxdepth: 2 :hidden: - /src/titlepage + /src/getting_started .. toctree:: - :caption: Project + :caption: Players :maxdepth: 2 :hidden: - /src/project + /src/players/players + /src/players/random .. toctree:: @@ -20,4 +23,9 @@ :maxdepth: 2 :hidden: - /src/games + /src/games/games + /src/games/tutorials/hanoi + /src/games/tutorials/slicing + /src/games/tutorials/nqueens + /src/games/tutorials/coins + /src/games/tutorials/mastermind diff --git a/docs/resources/images/coins.png b/docs/resources/images/coins.png new file mode 100644 index 0000000..4772e0c Binary files /dev/null and b/docs/resources/images/coins.png differ diff --git a/docs/resources/images/hanoi.png b/docs/resources/images/hanoi.png new file mode 100644 index 0000000..f8a01dc Binary files /dev/null and b/docs/resources/images/hanoi.png differ diff --git a/docs/resources/images/mastermind.png b/docs/resources/images/mastermind.png new file mode 100644 index 0000000..ccf2e12 Binary files /dev/null and b/docs/resources/images/mastermind.png differ diff --git a/docs/resources/images/nqueens.svg b/docs/resources/images/nqueens.svg new file mode 100644 index 0000000..5861e39 --- /dev/null +++ b/docs/resources/images/nqueens.svg @@ -0,0 +1,3 @@ + + +
Valid queen positions
Valid queen positions
Invalid queen positions
Invalid queen positions
0
0
1
1
2
2
3
3
0
0
1
1
2
2
3
3
0
0
1
1
2
2
3
3
0
0
1
1
2
2
3
3
Viewer does not support full SVG 1.1
\ No newline at end of file diff --git a/docs/resources/images/slicing.jpg b/docs/resources/images/slicing.jpg new file mode 100644 index 0000000..c17be05 Binary files /dev/null and b/docs/resources/images/slicing.jpg differ diff --git a/docs/src/games.rst b/docs/src/games.rst deleted file mode 100644 index 2962e74..0000000 --- a/docs/src/games.rst +++ /dev/null @@ -1,7 +0,0 @@ -.. _games: - -##### -Games -##### - -In this page will be displayed the different games available in the platform along with their description, characteristics and tutorials. diff --git a/docs/src/games/games.rst b/docs/src/games/games.rst new file mode 100644 index 0000000..7e7a927 --- /dev/null +++ b/docs/src/games/games.rst @@ -0,0 +1,193 @@ +.. _games: + +##### +Games +##### + +In this module there is explained the structure of a **Game** or **Rule**. +It also enumerates the different games supported so far by the library. + +========== +Interfaces +========== + +There are 3 main interfaces that represent a **game**. +The *IGameRules* holds the logic of the game. +The *IPosition* represents a position or state of the game regarding a specific set of rules. +The *IMovement* represents a movement of the game that goes from one position to another. + +.. _rules: + +------ +IRules +------ + +Represent the rules of a game implementing the following methods: + +.. list-table:: + + * - **Method** + - **Description** + - **Arguments type** + - **Return type** + - **Must be implemented for each game** + + * - ``n_players()``. + - The number of players + - ``-`` + - ``int`` + - *Yes*. + + * - ``first_position()``. + - The initial position. + - ``-`` + - ``IPosition`` + - *Yes*. + + * - ``next_position()``. + - The next position giving a position and a move. + - ``movement: IMovement, position: IPosition`` + - ``IPosition`` + - *Yes*. + + * - ``possible_movements()``. + - The possible moves giving a position. + - ``IPosition`` + - ``List[IMovements]`` + - *Yes*. + + * - ``finished()``. + - Whether a position is terminal. + - ``IPosition`` + - ``bool`` + - *Yes*. + + * - ``score()``. + - The score of a terminal position. + - ``IPosition`` + - ``ScoreBoard`` + - *Yes*. + + * - ``is_movement_possible()``. + - Whether a move is possible giving a position. + - ``movement: IMovement, position: IPosition`` + - ``bool`` + - *No*. + + + +.. _movement: + +--------- +IMovement +--------- + +Represent a movement of a game. +It does not have general methods, as each movement depends on the rules of the specific game. + + +.. _position: + +--------- +IPosition +--------- + +Represent a position of a game. +This means, a state, a position of a board, a position in a map, etc. +Some variables referring to the position may be stored in the rules object that has generated this position. + +.. list-table:: + + * - **Method** + - **Description** + - **Arguments type** + - **Return type** + - **Must be implemented for each game** + + * - ``next_player()``. + - The next player to play. + - ``-`` + - ``PlayerIndex`` + - *Yes*. + + * - ``get_rules()``. + - Rules that has generated this position. + - ``-`` + - ``IGameRules`` + - *No*. + +========== +ScoreBoard +========== + +The Score measured in a ``float`` variable indicates for each player how good the final game went. +After finishing any game, you get a ``ScoreBoard`` that holds the score for each player. + +.. note:: + + Always remember that the lowest score is the best one. + + +.. _games_available: + +=============== +Games available +=============== + +This is a list of all the games supported and its characteristics: + +.. list-table:: Games + + * - **Game name** + - **Folder** + - **Tutorial** + - **Short description** + - **Number of players** + - **Deterministic / Random** + - **Perfect information / Hidden information** + - **Details** + + * - **Hanoi** + - ``IArena.games.Hanoi`` + - :ref:`hanoi_tutorial` + - The classic Hanoi's Tower game. + - 1 + - Deterministic + - Perfect information + - + + * - **SlicingPuzzle** + - ``IArena.games.SlicingPuzzle`` + - :ref:`slicing_tutorial` + - The classic Slicing Puzzle game. + - 1 + - Deterministic + - Perfect information + - + + * - **NQueens** + - ``IArena.games.NQueens`` + - :ref:`nqueens_tutorial` + - The classic N-Queens game. + - 1 + - Deterministic + - Perfect information + - *Min score*: 0 + + * - **Coins** + - ``IArena.games.Coins`` + - :ref:`coins_tutorial` + - Roman's coin game. + - 2 + - Deterministic + - Perfect information + - **0 sum game** + + * - **Mastermind** + - ``IArena.games.Mastermind`` + - :ref:`mastermind_tutorial` + - The classic Mastermind game. + - 1 + - Deterministic + - Hidden information + - diff --git a/docs/src/games/tutorials/coins.rst b/docs/src/games/tutorials/coins.rst new file mode 100644 index 0000000..570127c --- /dev/null +++ b/docs/src/games/tutorials/coins.rst @@ -0,0 +1,115 @@ +.. _coins_tutorial: + +##### +Coins +##### + +.. figure:: /resources/images/coins.svg + +This game is a classical 2 players game called *Roman Coins*. +By turns, each player takes between ``A`` and ``B`` coins from a pile of ``N`` coins. +The player that can no longer take a coins loses. + +This is a **0 sum** game with perfect information. + +==== +Goal +==== + +The goal of this game is to be the last player to take a coin. + +----- +Score +----- + +- The player that takes the last coin: + - ``0`` +- The player that cannot take a coin: + - ``1`` + +====== +Import +====== + +.. code-block:: python + + import IArena.games.Coins.CoinsPosition as CoinsPosition + import IArena.games.Coins.CoinsMovement as CoinsMovement + import IArena.games.Coins.CoinsRules as CoinsRules + + +======== +Movement +======== + +A movement is represented by an ``int`` representing the number of coins to take: + +- ``n`` + - ``int`` + - ``A <= tower_source < min(B, number of coins remaining)`` + - Number of coins to remove + + +.. code-block:: python + + movement = CoinsMovement(n=2) # Remove 2 coins from the stack + + +======== +Position +======== + +A position is represented by an ``int`` describing the size of the remaining stack. + +.. code-block:: python + + # position : CoinsPosition + len(position) # Size of the stack + position.n # Same as len + + +===== +Rules +===== + +This games has every methods of :ref:`IRules `. + +It counts with 2 methods to get the minimum and maximum number of coins that can be taken: + +- ``rules.min_play() -> int`` +- ``rules.max_play() -> int`` + + +----------- +Constructor +----------- + +Can receive 3 arguments: + +- ``initial_position`` + - ``int`` + - ``0 <= initial_position`` + - Initial size of the stack + - Default: ``15`` +- ``min_play`` + - ``int`` + - ``1 <= min_play`` + - Minimum number of coins that can be taken in a turn + - Default: ``1`` +- ``max_play`` + - ``int`` + - ``min_play <= max_play`` + - Maximum number of coins that can be taken in a turn + - Default: ``3`` + + +.. code-block:: python + + # Stack of 15 coins with min_play=1 and max_play=3 + rules = coinsRules() + + # Stack of 20 coins with min_play=2 and max_play=5 + rules = coinsRules( + initial_position=20, + min_play=2, + max_play=5) diff --git a/docs/src/games/tutorials/hanoi.rst b/docs/src/games/tutorials/hanoi.rst new file mode 100644 index 0000000..8560c7d --- /dev/null +++ b/docs/src/games/tutorials/hanoi.rst @@ -0,0 +1,99 @@ +.. _hanoi_tutorial: + +##### +Hanoi +##### + +.. figure:: /resources/images/hanoi.png + +This game is the classical Tower of Hanoi puzzle. +The objective of the game is to move the entire stack of disks from the leftmost to the rightmost rod. +Only one disk may be moved at a time from the top of one tower to the top of another, +and it is not possible to place a bigger disk on top of a smaller disk. + +The game has three rods and a number of disks ``N`` that can vary from one game to another. +The game starts with ``N`` disks in ascending order of size on the leftmost rod. + +==== +Goal +==== + +The final position of the game is when all the disks are in the rightmost rod (tower index ``2``). + +----- +Score +----- + +The score of the game is the number of movements to reach the final position. + + +====== +Import +====== + +.. code-block:: python + + import IArena.games.Hanoi.HanoiPosition as HanoiPosition + import IArena.games.Hanoi.HanoiMovement as HanoiMovement + import IArena.games.Hanoi.HanoiRules as HanoiRules + + +======== +Movement +======== + +A movement is represented by 2 ``int``: ``tower_source`` and ``tower_target``. + +- ``tower_source`` + - ``int`` + - ``0 <= tower_source < 3`` + - Indicates the index of the tower from which the top disk will be removed. + +- ``tower_target`` + - ``int`` + - ``0 <= tower_target < 3`` and ``tower_source != tower_target`` + - Indicates the index of the tower where the disk removed will be placed. + + +.. code-block:: python + + movement = HanoiMovement(tower_source=0, tower_target=1) + + +======== +Position +======== + +A position is represented by a ``List[List[int]]`` and a cost in ``int`` format. +Each element of the list represents a tower. +Each tower has a list of ``int`` that represents the disks in the tower. +The disks go from ``0`` to ``N-1`` in ascending order being ``0`` the highest disk. + + +.. code-block:: python + + # position : HanoiPosition + position.towers[0][0] # lowest disk of the leftmost tower + position.towers[2][-1] # highest disk of the rightmost tower + position.cost # number of movements to reach this position + + +===== +Rules +===== + +This games has every methods of :ref:`IRules `. + +----------- +Constructor +----------- + +Can receive an argument ``n : int`` that represents the number of disks. + +.. code-block:: python + + # Initial position with 4 disks + rules = HanoiRules() + + # Initial position with 5 disks + rules = HanoiRules(n=5) diff --git a/docs/src/games/tutorials/mastermind.rst b/docs/src/games/tutorials/mastermind.rst new file mode 100644 index 0000000..70859ad --- /dev/null +++ b/docs/src/games/tutorials/mastermind.rst @@ -0,0 +1,141 @@ +.. _mastermind_tutorial: + +########## +Mastermind +########## + +.. figure:: /resources/images/mastermind.png + +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)``. +Each turn the player has to guess the code. +After each guess, the game will tell the player which of the guesses appears in the code but are not correctly positioned, and which ones are correctly positioned. +The goal is to guess the code in the fewest number of turns. + +Some changes have been made to the original game: + +- There is only a one player game (guesser) while the other player (codemaker) is the game itself. +- Instead of colors we use numbers. +- The clues of each guess is not only the number of appearance and correctness, but are linked with a direct position in the guess. +- Numbers could be repeated in the secret code. + +==== +Goal +==== + +Guess the correct *secret code* in the fewest number of turns. + +----- +Score +----- + +The number of turns needed to guess the secret code. + + +====== +Import +====== + +.. code-block:: python + + import IArena.games.Mastermind.MastermindPosition as MastermindPosition + import IArena.games.Mastermind.MastermindMovement as MastermindMovement + import IArena.games.Mastermind.MastermindRules as MastermindRules + + +======== +Movement +======== + +A movement has a *guess* in the format of ``List[int]``. +It must have ``N`` integers in the range ``[0,M)``. + +- ``guess`` + - ``List[int]`` + - ``len == N`` + - ``0 <= l[i] < M`` + - Guess of the secret code + + +.. code-block:: python + + # A guess in a game with N=3 and M>2 + movement = MastermindMovement(guess=[0, 1, 2]) + + +======== +Position +======== + +A position is represented by a list of movements (guesses) and a list of correctness. + +----------- +Correctness +----------- + +A ``MastermindCorrectness`` is an enumeration with the following values: + +- ``Wrong``: 0 +- ``Misplaced``: 1 +- ``Correct``: 2 + +The correctness of a guess is a list of ``MastermindCorrectness`` indicating for each of the values in the guess, +if it is correctly placed (``2``), +if it is in the secret code, but misplaced (``1``), +or whether it is not present in the secret code (``0``). + +.. code-block:: python + + # position : MastermindPosition + guesses = position.guesses + correctness = position.correctness + + guesses[-1][0] # First position of the last guess + + if correctness[-1][0] == MastermindPosition.MastermindCorrectness.Correct: + # The first value of the last guess is correct + elif correctness[-1][1] == MastermindPosition.MastermindCorrectness.Misplaced: + # The second value of the last guess is in the code but in other position + else: + # The third value of the last guess is wrong + + +===== +Rules +===== + +This games has every methods of :ref:`IRules `. + +It counts with 2 methods to get the minimum and maximum number of coins that can be taken: + +- ``rules.get_size_code() -> int`` +- ``rules.get_number_colors() -> int`` + + +----------- +Constructor +----------- + +There are 2 ways to construct the rules: + +#. Using a secret code already defined. + + .. code-block:: python + + # Secret code with N=4 and M=6 + rules = mastermindRules(secret=[0, 1, 2, 3], m=6) + + # Secret code with N=8 and M=8 + 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. + Using argument ``seed: int`` the random generation can be reproduced. + + .. code-block:: python + + # Random secret code with N=4 and M=6 + rules = mastermindRules() + + # Random secret code with N=8 and M=8 reproducible + rules = mastermindRules(n=8, m=8, seed=0) diff --git a/docs/src/games/tutorials/nqueens.rst b/docs/src/games/tutorials/nqueens.rst new file mode 100644 index 0000000..936e7e5 --- /dev/null +++ b/docs/src/games/tutorials/nqueens.rst @@ -0,0 +1,108 @@ +.. _nqueens_tutorial: + +####### +NQueens +####### + +.. figure:: /resources/images/nqueens.svg + +This game is the classical N-Queens puzzle. +The objective of the game is to place ``N`` queens on a ``N x N`` chessboard so that no two queens attack each other. +The attack of the queens follows the rules of chess: a queen can attack horizontally, vertically and diagonally. + +The game has a chessboard (square grid) of ``N x N`` squares. +Each movement indicates the place of a new queen in the chessboard. +After ``N`` movements, the optimal score is reached when no queens attack each other. + +==== +Goal +==== + +After placing ``N`` queens on a ``N x N`` chessboard, the queens must attack as less other queens as possible. + +----- +Score +----- + +``+1`` for each queen in the range of attack of other queen. +*This will always give even scores.* + + +====== +Import +====== + +.. code-block:: python + + import IArena.games.NQueens.NQueensPosition as NQueensPosition + import IArena.games.NQueens.NQueensMovement as NQueensMovement + import IArena.games.NQueens.NQueensRules as NQueensRules + + +======== +Movement +======== + +A movement is represented by a tuple ``new_position`` of 2 ``int``: + +- ``new_position`` + - ``tuple(int, int)`` + - ``int`` + - ``0 <= tower_source < N`` + - Number of row to place the next queen. + - ``tuple(int, int)`` + - ``int`` + - ``0 <= tower_source < N`` + - Number of column to place the next queen. + + +.. code-block:: python + + movement = NQueensMovement(new_position=[0, 0]) + + +======== +Position +======== + +A position is represented by an ``int`` describing the size of the board, and a ``list`` of movements. +Each movement represents the position of a queen in the board. + +.. code-block:: python + + # position : NQueensPosition + position.n # The size of the board is n x n + len(position.positions) # Number of queens already in the board + position.positions[0][0] # Row of the first queen + position.positions[-1][1] # Column of the last queen + + +===== +Rules +===== + +This games has every methods of :ref:`IRules `. + +Remember that using ``score`` method can give the current result of the game: + +.. code-block:: python + + # rules : NQueensRules + # position : NQueensPosition + rules.score(position) # Returns how many queens are attacking each other + + +----------- +Constructor +----------- + +Can receive an argument ``n : int`` that represents the size of the board. + + +.. code-block:: python + + # Initial board of 8x8 + rules = nqueensRules() + + # Initial board of 5x5 + rules = nqueensRules(n=5) diff --git a/docs/src/games/tutorials/slicing.rst b/docs/src/games/tutorials/slicing.rst new file mode 100644 index 0000000..c8c7d83 --- /dev/null +++ b/docs/src/games/tutorials/slicing.rst @@ -0,0 +1,131 @@ +.. _slicing_tutorial: + +############## +Slicing Puzzle +############## + +.. figure:: /resources/images/slicing.jpg + +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``). +The objective is to sort all the squares in the board in the lowest number of movements. +A movement is moving one of the squares next to the empty square to the empty square, becoming its position as the new empty square. + +==== +Goal +==== + +Sort the board in the lowest number of movements. +A board is considered solved when all the squares are sorted in ascending order, sorting first rows and then columns. +In the solved board the empty square is in the bottom right corner. + +----- +Score +----- + +The number of movements needed to solve the board. + + +====== +Import +====== + +.. code-block:: python + + import IArena.games.SlicingPuzzle.SlicingPuzzlePosition as SlicingPuzzlePosition + import IArena.games.SlicingPuzzle.SlicingPuzzleMovement as SlicingPuzzleMovement + import IArena.games.SlicingPuzzle.SlicingPuzzleRules as SlicingPuzzleRules + + +======== +Movement +======== + +A movement is represented by an enumeration with 4 values: + +- ``Up`` + - ``0`` + - Move the square below the empty one to the empty square. + - Move the empty square down. +- ``Down`` + - ``1`` + - Move the square above the empty one to the empty square. + - Move the empty square up. +- ``Left`` + - ``2`` + - Move the square to the right of the empty one to the empty square. + - Move the empty square to the right. +- ``Right`` + - ``3`` + - Move the square to the left of the empty one to the empty square. + - Move the empty square to the left. + + +.. code-block:: python + + # Move Up + movement = SlicingPuzzleMovement.Values.Up + + # Move Left + movement = SlicingPuzzleMovement.Values.Left + + +======== +Position +======== + +The position is represented by a ``List[List[int]]``. +The size of the board is ``n x n``. +The value of each square is between ``[0, n-2]``. +The empty square is represented by ``-1``. + +It counts with the following methods: + +- ``empty_space`` + - Retrieve the position of the empty square. + - Returns a tuple ``(row, column)``. +- ``len`` + - Get the size of the board +- ``cost`` + - Get the number of movements made so far + +.. code-block:: python + + # position : SlicingPuzzlePosition + position.n # Size of the board: n x n + len(position.positions) # Size of the board: n x n + + x, y = positions.empty_space() # Row and Col of the empty square + + position.squares # The board + position.squares[0][0] # The number in the upper left corner + position.positions[-1][-1] # The number in the bottom right corner + + position.cost() # Number of movements so far + + +===== +Rules +===== + +This games has every methods of :ref:`IRules `. + + +----------- +Constructor +----------- + +Can receive the initial position of the numbers, +or the size of the board (``n``) and it will generate a random position. + + +.. code-block:: python + + # Random initial board of 3x3 + rules = SlicingPuzzleRulesRules() + + # Random initial board of 4x4 reproducible + rules = SlicingPuzzleRulesRules(n=4, seed=0) + + # Initial board of 3x3 predefined + rules = SlicingPuzzleRulesRules(initial_position=[[1, 2, 3], [4, 5, 6], [-1, 7, 8]]) diff --git a/docs/src/getting_started.rst b/docs/src/getting_started.rst new file mode 100644 index 0000000..b874a36 --- /dev/null +++ b/docs/src/getting_started.rst @@ -0,0 +1,73 @@ +.. _getting_started: + +############### +Getting Started +############### + +================ +Project Overview +================ + +In order to know more about the infrastructure and design of this software from a developer point of view, +check the :ref:`infrastructure` section. + + +============ +Installation +============ + +Check the :ref:`installation guide `. + + +===== +Games +===== + +To check the games available in the library, please check the :ref:`games_available` section. + + +=========== +Play a game +=========== + +There is an specific *arena* that allows to play any game in a terminal interface. +In order to do so follow this instructions with the game desired: + +.. code-block:: python + + # Change Hanoi for the name of the game to play + + from IArena.arena.PlayableGame import PlayableGame + from IArena.games.Hanoi import HanoiRules, HanoiMovement, HanoiPosition + + # Create the game and play it + rules = HanoiRules() + game = PlayableGame(rules) + score = game.play() + +.. note:: + + Always remember that the lowest score is the best one. + + +============== +Build a player +============== + +In order to build a new player, ``IPlayer`` must be implemented. +Check the :ref:`players` section for more information. + + +============== +Add a new game +============== + +.. warning:: + + Coming soon. + +===== +Games +===== + +To check the games available in the library, please check the :ref:`games_available` section. diff --git a/docs/src/infraestructure.rst b/docs/src/infraestructure.rst new file mode 100644 index 0000000..1b47971 --- /dev/null +++ b/docs/src/infraestructure.rst @@ -0,0 +1,9 @@ +.. _infrastructure: + +############## +Infrastructure +############## + +.. warning:: + + Coming soon. diff --git a/docs/src/installation.rst b/docs/src/installation.rst new file mode 100644 index 0000000..83e924d --- /dev/null +++ b/docs/src/installation.rst @@ -0,0 +1,47 @@ +.. _installation: + +############ +Installation +############ + +.. _installation_googlecolab: + +============ +Google Colab +============ + +Using :ref:`Google Colab ` is the easiest way to get started. +Just add in the first cell of your notebook: + +.. code-block:: python + + # Download and install latest version of the package + !pip install --upgrade git+https://github.com/jparisu/IArena.git + + # Or to download a specific version + !pip install --upgrade git+https://github.com/jparisu/IArena.git@v0.2 + +This will install the specific version of the package and make it available in the rest of the cells of your notebook. + +=========================== +Install in Windows Anaconda +=========================== + +In order to install the package in Windows Anaconda, the steps are the same as for :ref:`installation_googlecolab`. +The only detail is that ``git`` may not be installed by default in Anaconda. + +From a command prompt, you can install it in a conda environment inside Anaconda: + +.. code-block:: bash + + conda install git + pip install --upgrade git+https://github.com/jparisu/IArena.git@v0.2 + + +=========== +Coming soon +=========== + +- Installation from source +- Installation via ``pip`` +- Installation via ``conda`` diff --git a/docs/src/players/players.rst b/docs/src/players/players.rst new file mode 100644 index 0000000..40473b0 --- /dev/null +++ b/docs/src/players/players.rst @@ -0,0 +1,59 @@ +.. _players: + +####### +Players +####### + +The final goal of this project is to be able to create software that can play a specific game. +This software is called **player** and must implement the **IPlayer** interface. + +======= +IPlayer +======= + +The interface can be found in ``src/IArena/interfaces/IPlayer`` module. + +This interface is used by an *arena* to play a specific set of rules. +The only method that must be implemented is ``play``. +It receives an ``IPosition`` and must return an ``IMovement`` (specific movement depending on the rules playing). + +.. code-block:: python + + def play( + self, + position: IPosition) -> IMovement: + pass + +The constructor of an ``IPlayer`` is not defined. +Each implementation could have its own constructor, adding for example a name, the rules of the game, etc. + +----- +Rules +----- + +The rules of each game are available from each ``IPosition`` of the game by calling ``position.get_rules()``. + +--------- +Movements +--------- + +Choose the next movement is responsibility of the player. +Each game has different movements that require different parameters. +Depending on the game, the player must create a movement with the required parameters. + +However, in the rules of the game there is a method ``possible_movements`` that returns a list of possible movements. +The player can create its own movement or choose one from the list of possible movements. + +.. code-block:: python + + def play( + self, + position: IPosition) -> IMovement: + + # Create next movement from scratch + movement = IMovement(...) + + # Choose one from the list of possible movements + rules = position.get_rules() + possible_movements = rules.possible_movements(position) + movement = possible_movements[...] diff --git a/docs/src/players/random.rst b/docs/src/players/random.rst new file mode 100644 index 0000000..203e029 --- /dev/null +++ b/docs/src/players/random.rst @@ -0,0 +1,9 @@ +.. _random_player: + +############# +Random Player +############# + +.. warning:: + + Coming soon. diff --git a/docs/src/project.rst b/docs/src/project.rst deleted file mode 100644 index f366173..0000000 --- a/docs/src/project.rst +++ /dev/null @@ -1,7 +0,0 @@ -.. _project_overview: - -################ -Project Overview -################ - -Detailed information about the project. diff --git a/docs/src/titlepage.rst b/docs/src/titlepage.rst index 8008a1d..c571b10 100644 --- a/docs/src/titlepage.rst +++ b/docs/src/titlepage.rst @@ -1,15 +1,39 @@ .. _title: -.. raw:: html +#################### +IArena Documentation +#################### -

- IArena Documentation -

+**IArena** is an **open source** project that aims to provide a **simple** and **easy to use** framework for playing games in a manual or **automatic** way. +It is developed with educational and research purposes related with **computer science** and **artificial intelligence**. -*IArena* first documentation release. - -######## +======== Overview -######## +======== + +**IArena** is a *Python* library that provides a framework for developing logic games and agents that play them. +The main idea is to be able to test strategies, algorithms and heuristics in different games. + +---- +Goal +---- + +The main goal of this project is to provide a framework for developing algorithms that are able to play optimally in different games. +A *player* is a piece of software that implements a simple interface that allows it to play following a game's *rules*. + +------- +Library +------- + +The library is designed to be **extensible** and **easy to use**. +It is easy to add new games and agents, and to test them in different scenarios. +The games follow a series of interfaces for making scalable in order to test different agents in different games. + +This library is mainly written in *Python*. + + +=============== +Further reading +=============== -Some text. +To start using the library, please check the :ref:`getting_started` section. diff --git a/resources/play_hanoi.ipynb b/resources/play_hanoi.ipynb index 7dcd7dd..85a094d 100644 --- a/resources/play_hanoi.ipynb +++ b/resources/play_hanoi.ipynb @@ -9,7 +9,7 @@ "outputs": [], "source": [ "# Download and install package\n", - "!pip install git+https://github.com/jparisu/IArena.git" + "!pip install git+https://github.com/jparisu/IArena.git\n" ] }, { @@ -21,8 +21,8 @@ "outputs": [], "source": [ "# Imports\n", - "import IArena.tournaments.PlayableGame\n", - "import IArena.games.Hanoi" + "import IArena.arena.PlayableGame\n", + "import IArena.games.Hanoi\n" ] }, { @@ -35,8 +35,8 @@ "source": [ "# Create the game and play it\n", "rules = IArena.games.Hanoi.HanoiRules()\n", - "game = IArena.tournaments.PlayableGame.PlayableGame(rules)\n", - "print(f\"You got it in : {game.play()} movements!\")" + "game = IArena.arena.PlayableGame.PlayableGame(rules)\n", + "print(f\"You got it in : {game.play()} movements!\")\n" ] }, { diff --git a/src/IArena/arena/GenericGame.py b/src/IArena/arena/GenericGame.py index 0ce1d3f..2bc3a8b 100644 --- a/src/IArena/arena/GenericGame.py +++ b/src/IArena/arena/GenericGame.py @@ -4,6 +4,8 @@ from IArena.interfaces.IPosition import IPosition from IArena.interfaces.IGameRules import IGameRules from IArena.interfaces.PlayerIndex import PlayerIndex +from IArena.interfaces.Score import ScoreBoard +from IArena.interfaces.IMovement import IMovement from IArena.utils.decorators import override class GenericGame: @@ -22,18 +24,17 @@ def __init__( self.players = players - def play(self) -> PlayerIndex: + def play(self) -> ScoreBoard: current_position = self.rules.first_position() finished = self.rules.finished(current_position) while not finished: current_position = self.next_movement_(current_position) finished = self.rules.finished(current_position) - return self.rules.score(current_position) + return self.calculate_score_(current_position) def next_movement_(self, current_position: IPosition) -> IPosition: - next_player = current_position.next_player() - movement = self.players[next_player].play(current_position) + movement = self.next_player_move_(current_position) # Check if the movement is possible if not self.rules.is_movement_possible(movement, current_position): @@ -43,6 +44,13 @@ def next_movement_(self, current_position: IPosition) -> IPosition: movement, current_position) + def next_player_move_(self, current_position: IPosition) -> IMovement: + next_player = current_position.next_player() + return self.players[next_player].play(current_position) + + def calculate_score_(self, position: IPosition) -> PlayerIndex: + return self.rules.score(position) + class BroadcastGame(GenericGame): diff --git a/src/IArena/arena/PlayableGame.py b/src/IArena/arena/PlayableGame.py index 553d120..396a285 100644 --- a/src/IArena/arena/PlayableGame.py +++ b/src/IArena/arena/PlayableGame.py @@ -2,7 +2,8 @@ from IArena.interfaces.IGameRules import IGameRules from IArena.interfaces.PlayerIndex import PlayerIndex -from IArena.tournaments.GenericGame import GenericGame +from IArena.interfaces.Score import ScoreBoard +from IArena.arena.GenericGame import GenericGame from IArena.players.players import PlayablePlayer class PlayableGame(GenericGame): @@ -14,3 +15,9 @@ def __init__( rules=rules, players=[ PlayablePlayer(rules, i) for i in range(rules.n_players())]) + + def play(self) -> ScoreBoard: + score = super().play() + print(f'SCORE: {score}') + print(f'WINNER: Player <{score.winner()}>') + return score diff --git a/src/IArena/arena/TournamentGame.py b/src/IArena/arena/TournamentGame.py new file mode 100644 index 0000000..220ee64 --- /dev/null +++ b/src/IArena/arena/TournamentGame.py @@ -0,0 +1,66 @@ +from typing import List + +from IArena.interfaces.IPlayer import IPlayer +from IArena.interfaces.IPosition import IPosition +from IArena.interfaces.IGameRules import IGameRules +from IArena.interfaces.PlayerIndex import PlayerIndex +from IArena.arena.GenericGame import GenericGame +from IArena.utils.decorators import override +from IArena.utils.Timer import Timer + +class ClockGame(GenericGame): + + def __init__( + self, + rules: IGameRules, + players: List[IPlayer]): + + # If the number of players is not correct, throw exception + if rules.n_players() != len(players): + raise ValueError(f'This game requires {rules.n_players()} players.' + f'{len(players)} were given.') + + self.rules = rules + self.players = players + + + def play(self) -> PlayerIndex: + current_position = self.rules.first_position() + finished = self.rules.finished(current_position) + while not finished: + current_position = self.next_movement_(current_position) + finished = self.rules.finished(current_position) + return self.rules.score(current_position) + + + def next_movement_(self, current_position: IPosition) -> IPosition: + next_player = current_position.next_player() + movement = self.players[next_player].play(current_position) + + # Check if the movement is possible + if not self.rules.is_movement_possible(movement, current_position): + raise ValueError(f'Invalid movement: {movement}') + + return self.rules.next_position( + movement, + current_position) + + +class BroadcastGame(GenericGame): + + @override + def next_movement_(self, current_position: IPosition) -> IPosition: + next_player = current_position.next_player() + movement = self.players[next_player].play(current_position) + + # Check if the movement is possible + if not self.rules.is_movement_possible(movement, current_position): + raise ValueError(f'Invalid movement: {movement}') + + next_position = self.rules.next_position( + movement, + current_position) + + print(f'Player <{next_player}> move: <{movement}> ->\n {next_position}') + + return next_position diff --git a/src/IArena/games/BlindWalk.py b/src/IArena/games/BlindWalk.py index 3e556e8..1faf5b1 100644 --- a/src/IArena/games/BlindWalk.py +++ b/src/IArena/games/BlindWalk.py @@ -7,7 +7,7 @@ from IArena.interfaces.IPosition import IPosition from IArena.interfaces.IMovement import IMovement from IArena.interfaces.IGameRules import IGameRules -from IArena.interfaces.PlayerIndex import PlayerIndex, two_player_game_change_player +from IArena.interfaces.PlayerIndex import PlayerIndex from IArena.utils.decorators import override """ @@ -16,71 +16,43 @@ The player can move in 4 directions: up, down, left and right. Each movement has a cost equal to the weight of the square. The player must reach the end with the minimum cost. +The grid is not known a priori, and must be discovered by the player step by step. """ -class BlindWalkSquare: +class BlindWalkMovement(Enum, IMovement): """ - Represents a square in the grid. + Represents the movement of the player in the grid. - Attributes: - position: List[int] size(2): The position of the square in the grid. - weight: The cost to move to this square. - """ - def __init__( - self, - position: List[int], - weight: int): - self.position = position - self.weight = weight - - def __str__(self) -> str: - return "| {0:4d} |".format(self.weight) - - -class BlindWalkMovement(IMovement): + Values: + Up: 0 - Move up. + Down: 1 - Move down. + Left: 2 - Move left. + Right: 3 - Move right. """ - Represents the movement of the player in the game. - - Attributes: - direction: The direction of the movement. - """ - - class MovementDirection(Enum): - Up = 0 - Down = 1 - Left = 2 - Right = 3 - - def __init__( - self, - direction: MovementDirection): - self.direction = direction - - def __eq__( - self, - other: "BlindWalkMovement"): - return self.direction == other.direction - - def __str__(self): - return f'{{direction: {self.direction}}}' + Up = 0 + Down = 1 + Left = 2 + Right = 3 class BlindWalkPosition(IPosition): """ - Represents the position of the player in the game. + Represents the position of the player in the grid. Attributes: - square: The square where the player is. + x: The row of the position (goal N-1) + y: The column of the position (goal N-1) cost: The cost of the path to reach this position. - neighbors: The neighbors of the square. """ def __init__( self, - square: BlindWalkSquare, + x: int, + y: int, cost: int, - neighbors: Dict[BlindWalkMovement.MovementDirection, BlindWalkSquare]): - self.square = square + neighbors: Dict[BlindWalkMovement, int]): + self.x = x + self.y = y self.cost = cost self.neighbors = neighbors @@ -92,33 +64,38 @@ def next_player( def __eq__( self, other: "BlindWalkPosition"): - return self.square == other.square and self.cost == other.cost + return self.x == other.x and self.y == other.y and self.cost == other.cost and self.neighbors == other.neighbors def __str__(self): - st = "=============================================\n" - st += f"Square: {self.square.position} Cost: {self.cost}\n\n" - for direction, neighbor in self.neighbors.items(): - st += " " + str(direction) + " " + str(neighbor) + "\n" - st += "=============================================\n" - return st + return f'{{[x: {self.x}, y: {self.y}] accumulated cost: {self.cost} neighbors: {self.neighbors}}}' class BlindWalkMap: def __init__( self, - squares: List[List[BlindWalkSquare]]): - self.squares = squares + squares: List[List[int]]): + self.squares__ = squares def __str__(self): - st = "" - st += "-" * 8 * len(self.squares[0]) + "\n" - for row in self.squares: - for square in row: - st += str(square) - st += "\n" + "-" * 8 * len(self.squares[0]) + "\n" - return st + return '\n'.join([' '.join(["%0:4d".format(square) for square in row]) for row in self.squares]) + + def __len__(self): + return len(self.squares) + + def __getitem__(self, i, j): + return self.squares[i][j] + def goal(self): + return (len(self)-1, len(self)-1) + + def is_goal(self, position: BlindWalkPosition): + return position.x == len(self.squares) - 1 and position.y == len(self.squares[0]) - 1 + + 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) lambda_parameter = 0.5 # You can adjust this to your preference @@ -126,40 +103,30 @@ def generate_random_map(rows: int, cols: int, seed: int = 0): def exponential_random_number(): return max(1, int(-1/lambda_parameter * math.log(1 - random.random()))) - return BlindWalkMap([ - [ - BlindWalkSquare( - position=[i, j], - weight=exponential_random_number() - ) for j in range(cols) - ] for i in range(rows) - ]) - - def get_neigbhours(self, square: BlindWalkSquare) -> Dict[BlindWalkMovement.MovementDirection, BlindWalkSquare]: - row = square.position[0] - col = square.position[1] - result = {} - if row > 0: - result[BlindWalkMovement.MovementDirection.Up] = self.squares[row - 1][col] - if row < len(self.squares) - 1: - result[BlindWalkMovement.MovementDirection.Down] = self.squares[row + 1][col] - if col > 0: - result[BlindWalkMovement.MovementDirection.Left] = self.squares[row][col - 1] - if col < len(self.squares[row]) - 1: - result[BlindWalkMovement.MovementDirection.Right] = self.squares[row][col + 1] + return BlindWalkMap( + [[exponential_random_number() for j in range(cols)] for i in range(rows)]) + + def get_possible_movements(self, position: BlindWalkPosition) -> List[BlindWalkMovement]: + result = [] + if position.x > 0: + result[BlindWalkMovement.Up] = self.squares[position.x - 1][position.y] + if position.x < len(self.squares) - 1: + result[BlindWalkMovement.Down] = self.squares[position.x + 1][position.y] + if position.y > 0: + result[BlindWalkMovement.Left] = self.squares[position.x][position.y - 1] + if position.y < len(self.squares[position.x]) - 1: + result[BlindWalkMovement.Right] = self.squares[position.x][position.y + 1] return result - def get_next_position(self, square: BlindWalkSquare, movement: BlindWalkMovement): - if movement == BlindWalkMovement.MovementDirection.Up: - return self.squares[square.position[0]-1][square.position[1]] - elif movement == BlindWalkMovement.MovementDirection.Down: - return self.squares[square.position[0]+1][square.position[1]] - elif movement == BlindWalkMovement.MovementDirection.Right: - return self.squares[square.position[0]][square.position[1]+1] - elif movement == BlindWalkMovement.MovementDirection.Left: - return self.squares[square.position[0]][square.position[1]-1] - - + def get_next_position(self, position: BlindWalkPosition, movement: BlindWalkMovement) -> BlindWalkPosition: + if movement == BlindWalkMovement.Up: + return BlindWalkPosition(position.x - 1, position.y, position.cost + self.squares[position.x - 1][position.y]) + if movement == BlindWalkMovement.Down: + return BlindWalkPosition(position.x + 1, position.y, position.cost + self.squares[position.x + 1][position.y]) + if movement == BlindWalkMovement.Left: + return BlindWalkPosition(position.x, position.y - 1, position.cost + self.squares[position.x][position.y - 1]) + if movement == BlindWalkMovement.Right: + return BlindWalkPosition(position.x, position.y + 1, position.cost + self.squares[position.x][position.y + 1]) class BlindWalkRules(IGameRules): @@ -189,36 +156,31 @@ def n_players(self) -> int: @override def first_position(self) -> BlindWalkPosition: return BlindWalkPosition( - square=self.__map.squares[0][0], - cost=0, - neighbors=self.__map.get_neigbhours(self.__map.squares[0][0])) + x=0, + y=0, + cost=0) @override def next_position( self, movement: BlindWalkMovement, position: BlindWalkPosition) -> BlindWalkPosition: - new_square = self.__map.get_next_position(position.square, movement) - return BlindWalkPosition( - new_square, - cost=position.cost + new_square.weight, - neighbors=self.__map.get_neigbhours(new_square)) + return self.__map.get_next_position(position, movement) @override def possible_movements( self, position: BlindWalkPosition) -> Iterator[BlindWalkMovement]: - movements = self.__map.get_neigbhours(position.square) - return list(movements.keys()) + return self.__map.get_possible_movements(position) @override def finished( self, position: BlindWalkPosition) -> bool: - return position.square.position[0] == len(self.__map.squares) - 1 and position.square.position[1] == len(self.__map.squares[0]) - 1 + return position.map.is_goal(position) @override def score( self, position: BlindWalkPosition) -> dict[PlayerIndex, float]: - return position.cost + return {PlayerIndex.FirstPlayer : position.cost} diff --git a/src/IArena/games/Coins.py b/src/IArena/games/Coins.py index a0488ab..5838b24 100644 --- a/src/IArena/games/Coins.py +++ b/src/IArena/games/Coins.py @@ -6,6 +6,7 @@ from IArena.interfaces.IGameRules import IGameRules from IArena.interfaces.PlayerIndex import PlayerIndex, two_player_game_change_player from IArena.utils.decorators import override +from IArena.interfaces.Score import ScoreBoard """ This game represents the Roman coins game. @@ -25,13 +26,18 @@ class CoinsPosition(IPosition): def __init__( self, + rules: "CoinsRules", n: int, next_player: PlayerIndex): + super().__init__(rules) # Number of coins self.n = n # Last player that has played self.next_player_ = next_player + def __len__(self) -> int: + return self.n + @override def next_player( self) -> PlayerIndex: @@ -44,11 +50,9 @@ def __eq__( return self.n == other.n and self.next_player_ == other.next_player_ def __str__(self): - st = "----------------\n" - st += f"Player: {self.next_player_}\n" + st = f"Player: {self.next_player_}\n" for i in range(self.n): st += (" {0:3d} === \n".format(i)) - st += "----------------\n" return st @@ -91,6 +95,14 @@ def __init__( self.min_play = min_play self.max_play = max_play + def min_play(self) -> int: + """Minimum number of coins that can be removed in a turn.""" + return self.min_play + + def max_play(self) -> int: + """Maximum number of coins that can be removed in a turn.""" + return self.max_play + @override def n_players(self) -> int: return 2 @@ -98,6 +110,7 @@ def n_players(self) -> int: @override def first_position(self) -> CoinsPosition: return CoinsPosition( + self, self.initial_position, PlayerIndex.FirstPlayer) @@ -107,8 +120,9 @@ def next_position( movement: CoinsMovement, position: CoinsPosition) -> CoinsPosition: return CoinsPosition( + self, position.n - movement.n, - two_player_game_change_player(position.next_player)) + two_player_game_change_player(position.next_player())) @override def possible_movements( @@ -133,8 +147,8 @@ def finished( @override def score( self, - position: CoinsPosition) -> dict[PlayerIndex, float]: - return { - position.next_player() : 0.0, - two_player_game_change_player(position.next_player()) : 1.0 - } + position: CoinsPosition) -> ScoreBoard: + s = ScoreBoard() + s.add_score(position.next_player(), 1.0) + s.add_score(two_player_game_change_player(position.next_player()), 0.0) + return s diff --git a/src/IArena/games/FieldWalk.py b/src/IArena/games/FieldWalk.py index b81fb1a..2a2255f 100644 --- a/src/IArena/games/FieldWalk.py +++ b/src/IArena/games/FieldWalk.py @@ -1,5 +1,5 @@ -from typing import Dict, Iterator, List +from typing import Iterator, List from enum import Enum import random import math @@ -7,9 +7,8 @@ from IArena.interfaces.IPosition import IPosition from IArena.interfaces.IMovement import IMovement from IArena.interfaces.IGameRules import IGameRules -from IArena.interfaces.PlayerIndex import PlayerIndex, two_player_game_change_player +from IArena.interfaces.PlayerIndex import PlayerIndex from IArena.utils.decorators import override -import IArena.games.BlindWalk as BlindWalk """ This game represents a grid search where each square has a different weight. @@ -17,26 +16,173 @@ The player can move in 4 directions: up, down, left and right. Each movement has a cost equal to the weight of the square. The player must reach the end with the minimum cost. - -Similar to BlindWalk but the player has access to the whole map from the beginning. """ -FieldWalkSquare = BlindWalk.BlindWalkSquare -FieldWalkMovement = BlindWalk.BlindWalkMovement -MovementDirection = BlindWalk.MovementDirection -FieldWalkPosition = BlindWalk.BlindWalkPosition -FieldWalkMap = BlindWalk.BlindWalkMap -FieldWalkRules = BlindWalk.BlindWalkRules +class FieldWalkMovement(IMovement): + """ + Represents the movement of the player in the grid. + + Values: + Up: 0 - Move up. + Down: 1 - Move down. + Left: 2 - Move left. + Right: 3 - Move right. + """ + + class Values(Enum): + Up = 0 + Down = 1 + Left = 2 + Right = 3 + + +class FieldWalkPosition(IPosition): + """ + Represents the position of the player in the grid. + + Attributes: + x: The row of the position (goal N-1) + y: The column of the position (goal N-1) + cost: The cost of the path to reach this position. + """ + + def __init__( + self, + x: int, + y: int, + cost: int): + self.x = x + self.y = y + self.cost = cost + + @override + def next_player( + self) -> PlayerIndex: + return PlayerIndex.FirstPlayer + + def __eq__( + self, + other: "FieldWalkPosition"): + return self.x == other.x and self.y == other.y and self.cost == other.cost + + def __str__(self): + return f'{{[x: {self.x}, y: {self.y}] accumulated cost: {self.cost}}}' + + +class FieldWalkMap: + + def __init__( + self, + squares: List[List[int]]): + self.squares = squares + + def __str__(self): + return '\n'.join([' '.join(["%0:4d".format(square) for square in row]) for row in self.squares]) + + def __len__(self): + return len(self.squares) -class FieldWalkRules(BlindWalk.BlindWalkRules): + def __getitem__(self, i, j): + return self.squares[i][j] + + def goal(self): + return (len(self)-1, len(self)-1) + + def is_goal(self, position: FieldWalkPosition): + return position.x == len(self.squares) - 1 and position.y == len(self.squares[0]) - 1 + + 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) + lambda_parameter = 0.5 # You can adjust this to your preference + + def exponential_random_number(): + return max(1, int(-1/lambda_parameter * math.log(1 - random.random()))) + + return FieldWalkMap( + [[exponential_random_number() for j in range(cols)] for i in range(rows)]) + + def get_possible_movements(self, position: FieldWalkPosition) -> List[FieldWalkMovement]: + result = [] + if position.x > 0: + result[FieldWalkMovement.Up] = self.squares[position.x - 1][position.y] + if position.x < len(self.squares) - 1: + result[FieldWalkMovement.Down] = self.squares[position.x + 1][position.y] + if position.y > 0: + result[FieldWalkMovement.Left] = self.squares[position.x][position.y - 1] + if position.y < len(self.squares[position.x]) - 1: + result[FieldWalkMovement.Right] = self.squares[position.x][position.y + 1] + 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]) + + +class FieldWalkRules(IGameRules): def __init__( self, - initial_map: BlindWalk.BlindWalkMap = None, + initial_map: FieldWalkMap = None, rows: int = 10, cols: int = 10, seed: int = 0): - super().__init__(initial_map, rows, cols, seed) + """ + Args: + initial_map: The map of the game. If none, it is generated randomly. + rows: The number of rows of the map. Only has effect if initial_map is None. + cols: The number of columns of the map. Only has effect if initial_map is None. + seed: The seed for the random generator of the map. Only has effect if initial_map is None. + """ + if initial_map: + self.map = initial_map + else: + self.map = FieldWalkMap.generate_random_map(rows, cols, seed) - def get_map(self): - return self._BlindWalkRules__map + def get_map(self) -> FieldWalkMap: + return self.map + + @override + def n_players(self) -> int: + return 1 + + @override + def first_position(self) -> FieldWalkPosition: + return FieldWalkPosition( + x=0, + y=0, + cost=0) + + @override + def next_position( + self, + movement: FieldWalkMovement, + position: FieldWalkPosition) -> FieldWalkPosition: + return self.map.get_next_position(position, movement) + + @override + def possible_movements( + self, + position: FieldWalkPosition) -> Iterator[FieldWalkMovement]: + return self.map.get_possible_movements(position) + + @override + def finished( + self, + position: FieldWalkPosition) -> bool: + return position.map.is_goal(position) + + @override + def score( + self, + position: FieldWalkPosition) -> dict[PlayerIndex, float]: + return {PlayerIndex.FirstPlayer : position.cost} diff --git a/src/IArena/games/Hanoi.py b/src/IArena/games/Hanoi.py index 9f742fe..e45b900 100644 --- a/src/IArena/games/Hanoi.py +++ b/src/IArena/games/Hanoi.py @@ -2,10 +2,11 @@ from typing import Iterator, List from queue import LifoQueue -from IArena.interfaces.IPosition import IPosition +from IArena.interfaces.IPosition import CostPosition, CostType from IArena.interfaces.IMovement import IMovement from IArena.interfaces.IGameRules import IGameRules from IArena.interfaces.PlayerIndex import PlayerIndex, two_player_game_change_player +from IArena.interfaces.Score import ScoreBoard from IArena.utils.decorators import override """ @@ -18,7 +19,7 @@ NOTE: Bigger pieces are the one with lower index. """ -class HanoiPosition(IPosition): +class HanoiPosition(CostPosition): """ Represents the position of the game with the pieces in each tower. @@ -31,10 +32,11 @@ class HanoiPosition(IPosition): def __init__( self, + rules: "IGameRules", towers: List[List[int]], - movements: int): + cost: CostType): + super().__init__(rules, cost) self.towers = towers - self.movements = movements @override def next_player( @@ -44,22 +46,21 @@ def next_player( def __eq__( self, other: "HanoiPosition"): - return self.towers == other.towers and self.movements == other.movements + return self.towers == other.towers and self.cost() == other.cost() def __str__(self): max_height = max([len(tower) for tower in self.towers]) - max_piece = max([max(tower, default=0) for tower in self.towers]) + max_piece = max([max(tower, default=0) for tower in self.towers])+1 max_width = max_piece * 2 st = "" - st += "\n" + "=" * (max_width + 1) * len(self.towers) + "\n" - st += f"Movements: {self.movements}\n\n" + st += f"Cost: {self.cost()}\n\n" for level in reversed(range(max_height)): for tower in self.towers: if level < len(tower): - piece_width = (max_piece - tower[level] + 1) * 2 + piece_width = (max_piece - tower[level]) * 2 padding = (max_width - piece_width) // 2 st += " " * padding + "#" * piece_width + " " * padding + " " else: @@ -68,7 +69,7 @@ def __str__(self): for i, _ in enumerate(self.towers): st += "=" * max_width + " " - st += "\n" + "=" * (max_width + 1) * len(self.towers) + "\n" + st += "\n" return st @@ -101,14 +102,20 @@ def __str__(self): class HanoiRules(IGameRules): + DefaultPieces = 4 + + @staticmethod + def generate_initial_towers(n: int) -> List[List[int]]: + return [list(range(n)), [], []] + def __init__( self, - initial_towers: List[List[int]] = [[1, 2, 3, 4], [], []]): + n: int = DefaultPieces): """ Args: - initial_towers: The initial position of the pieces in the towers. + n: The number of pieces in the game. """ - self.initial_towers = initial_towers + self.n = n @override def n_players(self) -> int: @@ -117,8 +124,9 @@ def n_players(self) -> int: @override def first_position(self) -> HanoiPosition: return HanoiPosition( - towers=self.initial_towers, - movements=0) + rules=self, + towers=HanoiRules.generate_initial_towers(self.n), + cost=0) @override def next_position( @@ -127,8 +135,9 @@ def next_position( position: HanoiPosition) -> HanoiPosition: new_position = HanoiPosition( + rules=self, towers=position.towers.copy(), - movements=position.movements + 1) + cost=position.cost() + 1) x = new_position.towers[movement.tower_source].pop() new_position.towers[movement.tower_target].append(x) @@ -140,7 +149,7 @@ def possible_movements( self, position: HanoiPosition) -> Iterator[HanoiMovement]: movements_result = [] - top_towers = [tower[-1] if len(tower) > 0 else 0 for tower in position.towers] + top_towers = [tower[-1] if len(tower) > 0 else -1 for tower in position.towers] for i in range(len(position.towers)): for j in range(len(position.towers)): @@ -164,5 +173,7 @@ def finished( @override def score( self, - position: HanoiPosition) -> dict[PlayerIndex, float]: - return {PlayerIndex.FirstPlayer : position.movements} + position: HanoiPosition) -> 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 new file mode 100644 index 0000000..3b23f0d --- /dev/null +++ b/src/IArena/games/HighestCard.py @@ -0,0 +1,185 @@ + +from typing import Dict, Iterator, List +import random +from copy import deepcopy + +from IArena.interfaces.IPosition import IPosition +from IArena.interfaces.IMovement import IMovement +from IArena.interfaces.IGameRules import IGameRules +from IArena.interfaces.PlayerIndex import PlayerIndex +from IArena.utils.decorators import override + +""" +This game represents the HighestCard game. +In this game N players has M cards from 0 to N*M-1. +The players must bet how many rounds it will win. +Then in each round all players play its highest card, and wins the highest of the cards played. + +Players that has accurately bet the number of rounds won gets -5 points. +Players that bet less than the number of rounds won gets 1 point for each round less. +Players that bet more than the number of rounds won gets 2 point for each round more. +""" + +class HighestCardMovement(IMovement): + """ + Represents the bet of the player. + It is a number between 0 and M. + + Attributes: + bet: int: The guess of how many rounds the player will win. + """ + + def __init__( + self, + bet: int): + self.bet = bet + + def __eq__( + self, + other: "HighestCardMovement"): + return self.bet == other.bet + + def __str__(self): + return f'<{self.bet}>' + + +class HighestCardPosition(IPosition): + """ + Represents a position where some players has already bet. + The bets are taken secretly. + """ + + def __init__( + self, + cards: Dict[PlayerIndex, List[int]] = None, + next_bet: HighestCardMovement = None, + previous: "HighestCardPosition" = None): + if cards is not None: + self.__cards = cards + self.__bet = {} + + else: + self.__cards = deepcopy(previous.__cards) + self.__bet = deepcopy(previous.__bet) + self.__bet[self.next_player()](next_bet) + + @override + def next_player( + self) -> PlayerIndex: + # The next player is the one that has not bet yet + if self.__bet == {}: + return PlayerIndex.FirstPlayer + return max(self.__bet.keys()) + 1 + + def get_cards(self) -> List[int]: + return deepcopy(self.__cards[self.next_player()]) + + def number_players(self) -> int: + return len(self.__cards) + + def number_cards(self) -> int: + return len(self.__cards[0]) + + def __eq__( + self, + other: "HighestCardPosition"): + return self.__bet == other.__bet + + 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): + # Calculate how many rounds each player has won + round_win = [0 for _ in range(self.number_players())] + for i in range(self.number_cards()): + max_player = -1 + max_card = -1 + for j in range(self.number_players()): + if self.__cards[j][i] > max_card: + max_card = self.__cards[j][i] + max_player = j + round_win[max_player] += 1 + + # Calculate the score of each player depending on its bet + score = {} + 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]) + else: + score[i] = 2 * (round_win[i] - self.__bet[i]) + + +class HighestCardRules(IGameRules): + + def __init__( + self, + cards_distribution: Dict[PlayerIndex, List[int]] = None, + n_players: int = 3, + m_cards: int = 4, + seed: int = 0): + + if cards_distribution is None: + cards = [i for i in range(n_players*m_cards)] + random.seed(seed) + random.shuffle(cards) + + self.__cards = {} + + for i in range(n_players): + self.__cards[i] = [] + for _ in range(m_cards): + self.__cards[i].append(cards.pop()) + + else: + self.__cards = cards_distribution + + # Sort the cards of each player + for i in range(n_players): + self.__cards[i].sort() + + self.n = len(self.__cards) + self.m = len(self.__cards[0]) + + + @override + def n_players(self) -> int: + return self.n + + @override + def first_position(self) -> HighestCardPosition: + return HighestCardPosition( + cards=self.__cards + ) + + @override + def next_position( + self, + movement: HighestCardMovement, + position: HighestCardPosition) -> HighestCardPosition: + return HighestCardPosition( + next_bet=movement, + previous=position + ) + + @override + def possible_movements( + self, + position: HighestCardPosition) -> Iterator[HighestCardMovement]: + # The possible movements are the numbers between 0 and M + return [HighestCardMovement(i) for i in range(self.m + 1)] + + @override + def finished( + self, + position: HighestCardPosition) -> bool: + # Finished when all players has bet + return position.next_player() == self.n + + @override + def score( + self, + position: HighestCardPosition) -> dict[PlayerIndex, float]: + return position.calculate_score() diff --git a/src/IArena/games/Mastermind.py b/src/IArena/games/Mastermind.py new file mode 100644 index 0000000..6ba08aa --- /dev/null +++ b/src/IArena/games/Mastermind.py @@ -0,0 +1,197 @@ + +from copy import deepcopy +from typing import Iterator, List +import random +from enum import Enum +import itertools + +from IArena.interfaces.IPosition import IPosition +from IArena.interfaces.IMovement import IMovement +from IArena.interfaces.IGameRules import IGameRules +from IArena.interfaces.PlayerIndex import PlayerIndex, two_player_game_change_player +from IArena.utils.decorators import override +from IArena.interfaces.Score import ScoreBoard + +""" +This game represents the Mastermind game. +In this game there is a pattern hidden and the player must guess it. +The pattern is a list of N numbers from 0 to M-1. +The player makes guesses and the game tells how many numbers are correct and in the right position. +The game ends when the player guesses the pattern. + +NOTE: +1. In this implementation, the game is played by one player, and the pattern is not known. +2. The numbers (colors) could be repeated. +3. The game tells for each guess exactly which numbers are correct and in the right position. +""" + +class MastermindMovement(IMovement): + """ + Represents the movement of the player in the game by guessing the pattern. + It is a list of N numbers with numbers from 0 to M-1. + + Attributes: + guess: The guess of the player. + """ + + def __init__( + self, + guess: List[int]): + self.guess = guess + + def __eq__( + self, + other: "MastermindMovement"): + return self.guess == other.guess + + def __str__(self): + return f'{self.guess}' + + +class MastermindPosition(IPosition): + """ + TODO + """ + + class MastermindCorrectness (Enum): + """ + Represents the correctness one number in one guess. + """ + Wrong = 0 + Misplaced = 1 + Correct = 2 + + def __init__( + self, + rules: "MastermindRules", + guesses: List[MastermindMovement], + correctness: List[List[MastermindCorrectness]]): + super().__init__(rules) + self.guesses = guesses + self.correctness = correctness + + @override + def next_player( + self) -> PlayerIndex: + return PlayerIndex.FirstPlayer + + def __eq__( + self, + other: "MastermindPosition"): + return self.guesses == other.guesses and self.correctness == other.correctness + + def __str__(self): + # Print each guess in a line together with the correctness + return "\n".join([f'{self.guesses[i]} : {[x.name for x in self.correctness[i]]}' for i in range(len(self.guesses))]) + "\n" + + +class MastermindRules(IGameRules): + + DefaultSizeCode = 4 + DefaultNumberColors = 6 + + @staticmethod + def get_secret(n: int, m: int, seed: int = None) -> List[int]: + if seed is not None: + random.seed(seed) + return [random.randint(0, m-1) for _ in range(n)] + + def __init__( + self, + n: int = DefaultNumberColors, + m: int = DefaultSizeCode, + seed: int = None, + secret: List[int] = None): + """ + Construct a secret code of size n with numbers from 0 to m-1. + + If a secret is not provided, a new one is generated using the seed provided. + n and m must be provided. + + If a secret is provided, it is used instead of generating a new one. + m must be always provided. + + Args: + n: Size of the code. + m: Number of colors. + seed: Seed for the random generator. + secret: The secret code. + """ + self.m = m + if secret: + self.__secret = secret + self.n = len(secret) + else: + self.__secret = MastermindRules.get_secret(n, m, seed) + self.n = n + + def get_number_colors(self) -> int: + return self.m + + def get_size_code(self) -> int: + return self.n + + @override + def n_players(self) -> int: + return 1 + + @override + def first_position(self) -> MastermindPosition: + return MastermindPosition(self, [], []) + + @override + def next_position( + self, + movement: MastermindMovement, + position: MastermindPosition) -> MastermindPosition: + guesses = position.guesses + [movement] + + # Calculate the correctness of the new guess + correctness = [MastermindPosition.MastermindCorrectness.Wrong for _ in range(self.n)] + already_placed = [False for _ in range(self.n)] + possible_misplaced = [] + for i in range(self.n): + if movement.guess[i] == self.__secret[i]: + correctness[i] = MastermindPosition.MastermindCorrectness.Correct + already_placed[i] = True + elif movement.guess[i] in self.__secret: + possible_misplaced.append(i) + + for i in possible_misplaced: + # Check if such number is in secret in a position that has already not being checked + for j in range(self.n): + if not already_placed[j] and movement.guess[i] == self.__secret[j]: + already_placed[j] = True + correctness[i] = MastermindPosition.MastermindCorrectness.Misplaced + break + + new_correctness = deepcopy(position.correctness) + new_correctness.append(correctness) + + return MastermindPosition(self, guesses, new_correctness) + + @override + def possible_movements( + self, + position: MastermindPosition) -> Iterator[MastermindMovement]: + # Every combination of n numbers from 0 to m-1 using itertools + return [MastermindMovement(guess = list(x)) + for x + in itertools.product(range(self.m), repeat=self.n)] + + @override + def finished( + self, + position: MastermindPosition) -> bool: + # Game is finished if the last guess is equal the hidden secret + if len(position.guesses) == 0: + return False + return position.guesses[-1].guess == self.__secret + + @override + def score( + self, + position: MastermindPosition) -> ScoreBoard: + s = ScoreBoard() + s.add_score(PlayerIndex.FirstPlayer, len(position.guesses)) + return s diff --git a/src/IArena/games/NQueens.py b/src/IArena/games/NQueens.py new file mode 100644 index 0000000..8aa9094 --- /dev/null +++ b/src/IArena/games/NQueens.py @@ -0,0 +1,151 @@ + +from typing import Iterator, List + +from IArena.interfaces.IPosition import IPosition +from IArena.interfaces.IMovement import IMovement +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 NQueens game. +In a chessboard of NxN, N queens must be placed in a way that no queen can attack another. +""" + +class NQueensPosition(IPosition): + """ + List of positions of the queens along the board. + + Attributes: + n: Size of the board = nxn + positions: List of (x,y) positions of the queens + """ + def __init__( + self, + rules: "NQueensRules", + positions: List[tuple[int, int]] = []) -> None: + super().__init__(rules) + self.n = rules.get_size() + self.positions = positions + + @override + def next_player( + self) -> PlayerIndex: + return PlayerIndex.FirstPlayer + + def __str__(self) -> str: + # Print the board with the queens + board = [["." for _ in range(self.n)] for _ in range(self.n)] + for x, y in self.positions: + board[x][y] = "Q" + return "\n".join(["".join(row) for row in board]) + + def __len__(self) -> int: + return len(self.positions) + + +class NQueensMovement(IMovement): + """ + A movement in this game is the position of the 1 queen in the board. + + Attributes: + new_position: (x,y) position of the new queen + """ + + def __init__( + self, + new_position: tuple[int, int]): + self.new_position = new_position + + def __eq__( + self, + other: "NQueensMovement"): + return self.new_position == other.new_position + + def __str__(self): + return f'[{self.new_position[0]},{self.new_position[1]}]' + + +class NQueensRules(IGameRules): + """ + Rules of the NQueens game. + + Attributes: + n: Size of the board = nxn + + The score of the game is calculated as the number of queens that are attacking other queens. + 0 score is the best score. + """ + + DefaultBoardSize = 8 + + def __init__( + self, + n: int = DefaultBoardSize): + """ + Args: + n: Size of the board = nxn + """ + self.n = n + + def __len__(self): + return self.n + + def get_size(self): + return self.n + + @override + def n_players(self) -> int: + return 1 + + @override + def first_position(self) -> NQueensPosition: + return NQueensPosition(self) + + @override + def next_position( + self, + movement: NQueensMovement, + position: NQueensPosition) -> NQueensPosition: + return NQueensPosition(self, position.positions + [movement.new_position]) + + @override + def possible_movements( + self, + position: NQueensPosition) -> Iterator[NQueensMovement]: + return [NQueensMovement((x, y)) for x in range(self.n) for y in range(self.n)] + + @override + def finished( + self, + position: NQueensPosition) -> bool: + return len(position) == self.n + + + @override + def score( + self, + position: NQueensPosition) -> ScoreBoard: + # Sum 1 for each queen that is attacking other + attacks = 0 + + # For each queen + for x, y in position.positions: + # For each other + for x2, y2 in position.positions: + if not (x == x2 and y == y2): + + # Check if it not attacking other horizontally + if x == x2: + attacks += 1 + # Check if it not attacking other vertically + if y == y2: + attacks += 1 + # Check if it not attacking other diagonally + if abs(x - x2) == abs(y - y2): + attacks += 1 + + s = ScoreBoard() + s.add_score(PlayerIndex.FirstPlayer, attacks) + return s diff --git a/src/IArena/games/Nim.py b/src/IArena/games/Nim.py index 50a8c34..5b10be5 100644 --- a/src/IArena/games/Nim.py +++ b/src/IArena/games/Nim.py @@ -102,7 +102,7 @@ def next_position( next_player = two_player_game_change_player(position.next_player()) lines = list(position.lines) - lines[movement.line_index] -= movement.Nim + lines[movement.line_index] -= movement.remove return NimPosition( lines=lines, @@ -121,7 +121,7 @@ def possible_movements( new_movement = NimMovement( line_index=index, - Nim=i+1 + remove=i+1 ) movements_result.append(new_movement) @@ -138,6 +138,6 @@ def score( self, position: NimPosition) -> dict[PlayerIndex, float]: return { - position.next_player() : 0.0, - two_player_game_change_player(position.next_player()) : 1.0 + position.next_player() : 1.0, + two_player_game_change_player(position.next_player()) : 0.0 } diff --git a/src/IArena/games/PrisonerDilemma.py b/src/IArena/games/PrisonerDilemma.py new file mode 100644 index 0000000..cf59ac9 --- /dev/null +++ b/src/IArena/games/PrisonerDilemma.py @@ -0,0 +1,163 @@ + +from typing import Dict, Iterator +from enum import Enum +import random + +from IArena.interfaces.IPosition import IPosition +from IArena.interfaces.IMovement import IMovement +from IArena.interfaces.IGameRules import IGameRules +from IArena.interfaces.PlayerIndex import PlayerIndex, two_player_game_change_player +from IArena.utils.decorators import override + +""" +This game represents the Prisoner Dilemma game theory problem. +Given 2 players, can decide whether to cooperate or betray. +A score table will be given a priori with the score of each player depending on the decision of both. +The goal is to minimize the score of the player. +""" + + +class PrisonerDilemmaMovement(Enum, IMovement): + """ + Represents the movement of one of the players. + + Values: + Cooperate: 0 - Cooperate. + Betray: 1 - Betray. + """ + Cooperate = 0 + Betray = 1 + + +class PrisonerDilemmaScoreTable: + + def __init__(self, score_table: Dict[Dict[PrisonerDilemmaMovement, float]]): + self.score_table = score_table + + def __str__(self): + return str(self.score_table) + + def generate_random_table(self, seed: int = 0) -> "PrisonerDilemmaScoreTable": + random.seed(seed) + scores = sorted([random.random() for _ in range(4)]) + return { + PrisonerDilemmaMovement.Cooperate: { + PrisonerDilemmaMovement.Cooperate: scores[1], + PrisonerDilemmaMovement.Betray: scores[3] + }, + PrisonerDilemmaMovement.Betray: { + PrisonerDilemmaMovement.Cooperate: scores[0], + PrisonerDilemmaMovement.Betray: scores[2] + } + } + + def score(self, player_movement: PrisonerDilemmaMovement, opponent_movement: PrisonerDilemmaMovement) -> float: + return self.score_table[player_movement][opponent_movement] + +class PrisonerDilemmaPosition(IPosition): + """ + Represents the position of the game by counting the coins remaining. + This holds in secret the movement of the players until the score() method is called. + If such method is called before the game is over, it will not return the score. + """ + def __init__( + self, + first_player_position: "PrisonerDilemmaPosition" = None, + new_movement: PrisonerDilemmaMovement = None): + if first_player_position: + self.__first_player_movement = first_player_position.__new_movement + self.__new_movement = new_movement + else: + self.__first_player_movement = new_movement + self.__new_movement = None + + def first_player_already_played(self) -> bool: + return self.__first_player_movement is not None + + def second_player_already_played(self) -> bool: + return self.__new_movement is not None + + @override + def next_player( + self) -> PlayerIndex: + if self.__first_player_movement is None: + return PlayerIndex.FirstPlayer + else: + return two_player_game_change_player(PlayerIndex.FirstPlayer) + + def __eq__( + self, + other: "PrisonerDilemmaPosition"): + return self.__first_player_movement == other.__first_player_movement and self.__new_movement == other.__new_movement + + def __str__(self): + return f"Player: {self.next_player()}" + + def score(self, score_table: PrisonerDilemmaScoreTable) -> dict[PlayerIndex, float]: + if self.__first_player_movement is None or self.__new_movement is None: + return None + + return { + PlayerIndex.FirstPlayer: score_table.score_table(self.__first_player_movement, self.__new_movement), + two_player_game_change_player(PlayerIndex.FirstPlayer): score_table.score_table(self.__new_movement, self.__first_player_movement) + } + + +class PrisonerDilemmaRules(IGameRules): + """ + This rules contains the punctuation table of the Prisoner Dilemma game. + Such score table is a dictionary of dictionaries where the score of each player is calculated by: + score_table[player_movement][opponent_movement] + """ + + def __init__( + self, + score_table: PrisonerDilemmaScoreTable = None, + seed: int = 0): + """ + Args: + initial_position: The number of PrisonerDilemma at the beginning of the game. + min_play: The minimum number of PrisonerDilemma that can be removed. + max_play: The maximum number of PrisonerDilemma that can be removed. + """ + if score_table is None: + score_table = PrisonerDilemmaScoreTable.generate_random_table(seed) + + @override + def n_players(self) -> int: + return 2 + + @override + def first_position(self) -> PrisonerDilemmaPosition: + return PrisonerDilemmaPosition() + + @override + def next_position( + self, + movement: PrisonerDilemmaMovement, + position: PrisonerDilemmaPosition) -> PrisonerDilemmaPosition: + if position.first_player_already_played(): + return PrisonerDilemmaPosition( + first_player_position=position, + new_movement=movement) + + @override + def possible_movements( + self, + position: PrisonerDilemmaPosition) -> Iterator[PrisonerDilemmaMovement]: + return [ + PrisonerDilemmaMovement.Cooperate, + PrisonerDilemmaMovement.Betray + ] + + @override + def finished( + self, + position: PrisonerDilemmaPosition) -> bool: + return position.first_player_already_played() and position.second_player_already_played() + + @override + def score( + self, + position: PrisonerDilemmaPosition) -> dict[PlayerIndex, float]: + return position.score() diff --git a/src/IArena/games/SlicingPuzzle.py b/src/IArena/games/SlicingPuzzle.py new file mode 100644 index 0000000..1b282dd --- /dev/null +++ b/src/IArena/games/SlicingPuzzle.py @@ -0,0 +1,223 @@ + +from typing import Iterator, List +from enum import Enum +import random + +from IArena.interfaces.IPosition import CostPosition, CostType +from IArena.interfaces.IMovement import IMovement +from IArena.interfaces.IGameRules import IGameRules +from IArena.interfaces.PlayerIndex import PlayerIndex +from IArena.interfaces.Score import ScoreBoard +from IArena.utils.decorators import override + +""" +This game represents the SlicingPuzzle game. +This is a grid NxN with NxN-1 numerated squares. +One movement moves one of the squares next to the empty space to the empty space. +The goal is to order the squares from 1 to NxN-1. +""" + +class SlicingPuzzlePosition(CostPosition): + """ + This is the grid NxN with NxN-1 numerated squares. + The empty space is the -1. + The not empty numbers go from 1 to NxN-1. + """ + def __init__( + self, + rules: "SlicingPuzzleRules", + squares: List[List[int]], + cost: CostType) -> None: + super().__init__(rules, cost) + self.squares = squares + self.n = len(squares) + + @override + def next_player( + self) -> PlayerIndex: + return PlayerIndex.FirstPlayer + + def __str__(self) -> str: + # Print the board with the squares + board = f"Cost: {self.cost()}\n" + board += "+" + "----+" * self.n + "\n" + for row in self.squares: + for col in row: + if col == -1: + board += "| " + else: + board += f"|{col:3d} " + board += "|\n+" + "----+" * self.n + "\n" + return board + + def empty_space(self): + for i in range(self.n): + for j in range(self.n): + if self.squares[i][j] == -1: + return (i, j) + + +class SlicingPuzzleMovement(IMovement): + """ + Represents the movement of one of the neighbors of the empty space to the empty space. + + Values: + Up: 0 - Move up. + Down: 1 - Move down. + Left: 2 - Move left. + Right: 3 - Move right. + """ + + class Values(Enum): + Up = 0 + Down = 1 + Left = 2 + Right = 3 + + +class SlicingPuzzleRules(IGameRules): + """ + Rules for the SlicingPuzzle game. + """ + + DefaultSize = 3 + DefaultRandomShuffle = 1000 + + + def generate_correct_position(n: int) -> List[List[int]]: + """ + Generate a correct position of the game. + + Args: + n: Size of the board = nxn + """ + squares = [[i + j * n + 1 for i in range(n)] for j in range(n)] + squares[n - 1][n - 1] = -1 + return squares + + + def generate_random_position(self, seed: int = None, random_moves: int = DefaultRandomShuffle) -> List[List[int]]: + """ + Generate a random position of the game by moving random_moves times a correct one + + Args: + seed: Seed for the random generator + random_moves: Number of random movements + """ + self.initial_position = SlicingPuzzlePosition(self, SlicingPuzzleRules.generate_correct_position(self.n), 0) + + # Move the squares randomly to generate a random position + if seed is not None: + random.seed(seed) + + for _ in range(random_moves): + possible_movements = self.possible_movements(self.initial_position) + movement = random.choice(list(possible_movements)) + self.initial_position = self.next_position(movement, self.initial_position) + + return self.initial_position.squares + + + def __init__( + self, + initial_position: List[List[int]] = None, + n: int = DefaultSize, + seed: int = None): + """ + Args: + initial_position: Initial position of the game if given. + n: Size of the board = nxn + """ + if initial_position is None: + self.n = n + self.initial_position = self.generate_random_position(seed=seed) + else: + self.initial_position = initial_position + self.n = len(initial_position) + + @override + def n_players(self) -> int: + return 1 + + @override + def first_position(self) -> SlicingPuzzlePosition: + return SlicingPuzzlePosition( + rules=self, + squares=self.initial_position, + cost=0) + + @override + def next_position( + self, + movement: SlicingPuzzleMovement, + position: SlicingPuzzlePosition) -> SlicingPuzzlePosition: + # Find the empty space and move the square next to it to the empty space + empty_space = position.empty_space() + new_space = None + + if movement == SlicingPuzzleMovement.Values.Up: + new_space = (empty_space[0] + 1, empty_space[1]) + + elif movement == SlicingPuzzleMovement.Values.Down: + new_space = (empty_space[0] - 1, empty_space[1]) + + elif movement == SlicingPuzzleMovement.Values.Left: + new_space = (empty_space[0], empty_space[1] + 1) + + elif movement == SlicingPuzzleMovement.Values.Right: + new_space = (empty_space[0], empty_space[1] - 1) + + new_squares = [[position.squares[i][j] for j in range(self.n)] for i in range(self.n)] + new_squares[empty_space[0]][empty_space[1]] = new_squares[new_space[0]][new_space[1]] + new_squares[new_space[0]][new_space[1]] = -1 + + return SlicingPuzzlePosition( + rules=self, + squares=new_squares, + cost=position.cost() + 1) + + + @override + def possible_movements( + self, + position: SlicingPuzzlePosition) -> Iterator[SlicingPuzzleMovement]: + # Find the empty space and return the possible movements depending on the borders of the game + empty_space = position.empty_space() + + possible_movements = [] + if empty_space[0] > 0: + possible_movements.append(SlicingPuzzleMovement.Values.Down) + if empty_space[0] < self.n - 1: + possible_movements.append(SlicingPuzzleMovement.Values.Up) + if empty_space[1] > 0: + possible_movements.append(SlicingPuzzleMovement.Values.Right) + if empty_space[1] < self.n - 1: + possible_movements.append(SlicingPuzzleMovement.Values.Left) + + return possible_movements + + + @override + def finished( + self, + position: SlicingPuzzlePosition) -> bool: + # Check that every square is in the correct position + correct_n = 1 + for i in range(self.n): + for j in range(self.n): + if position.squares[i][j] != correct_n: + if i == self.n - 1 and j == self.n - 1: + continue + else: + return False + correct_n += 1 + return True + + + @override + def score( + self, + position: SlicingPuzzlePosition) -> ScoreBoard: + s = ScoreBoard() + s.add_score(PlayerIndex.FirstPlayer, position.cost()) + return s diff --git a/src/IArena/games/TicTacToe.py b/src/IArena/games/TicTacToe.py new file mode 100644 index 0000000..24beb4c --- /dev/null +++ b/src/IArena/games/TicTacToe.py @@ -0,0 +1,206 @@ + +from typing import Iterator, List +from enum import Enum +from copy import deepcopy + +from IArena.interfaces.IPosition import IPosition +from IArena.interfaces.IMovement import IMovement +from IArena.interfaces.IGameRules import IGameRules +from IArena.interfaces.PlayerIndex import PlayerIndex, two_player_game_change_player +from IArena.utils.decorators import override + +""" +This game represents the Tic Tac Toe or 3 in a row game. +The game is played on a 3x3 board, where each player has a symbol (X/1 or O). +The players alternate turns, and the first player to get 3 of their symbols in a row (horizontally, vertically or diagonally) wins. +If the board is full and neither player has 3 in a row, the game is a draw. +""" + + +class TicTacToeMovement(IMovement): + """ + Represents a new position of the piece in the board. + + Attributes: + row: [0, 1, 2] The row where the piece is moved. + column: [0, 1, 2] The column where the piece is moved. + """ + + def __init__( + self, + row: int, + column: int): + self.row = row + self.column = column + + def __eq__( + self, + other: "TicTacToeMovement"): + return self.row == other.row and self.column == other.column + + def __str__(self): + return f'[{self.row}, {self.column}]' + + +class TicTacToePosition(IPosition): + """ + Represents the position of the board game. + + Attributes: + board: List[List[int]] The board of the game. + """ + + class TicTacToePiece(Enum): + Empty = 0 + FirstPlayer = 1 + SecondPlayer = 2 + + def __init__( + self, + board: List[List[PlayerIndex]]): + self.board = board + + @override + def next_player( + self) -> PlayerIndex: + # The movement is first player if the count in the board is even, otherwise is second player. + return PlayerIndex.FirstPlayer if sum( + [sum([1 for x in row if x != TicTacToePosition.TicTacToePiece.Empty]) for row in self.board]) % 2 == 0 else PlayerIndex.SecondPlayer + + def __eq__( + self, + other: "TicTacToePosition"): + return self.board == other.board + + def __str__(self): + st = "----------------\n" + for row in self.board: + st += "|" + for column in row: + if column == TicTacToePosition.TicTacToePiece.Empty: + st += " " + elif column == TicTacToePosition.TicTacToePiece.FirstPlayer: + st += "X" + elif column == TicTacToePosition.TicTacToePiece.SecondPlayer: + st += "O" + st += "|" + st += "\n" + st += "----------------\n" + return st + + +class TicTacToeRules(IGameRules): + + def __init__( + self, + initial_position: TicTacToePosition = None): + """ + Args: + initial_position: The number of TicTacToe at the beginning of the game. + min_play: The minimum number of TicTacToe that can be removed. + max_play: The maximum number of TicTacToe that can be removed. + """ + if initial_position is None: + initial_position = TicTacToePosition( + [ + [TicTacToePosition.TicTacToePiece.Empty for _ in range(3)] for _ in range(3) + ] + ) + self.initial_position = initial_position + + @override + def n_players(self) -> int: + return 2 + + @override + def first_position(self) -> TicTacToePosition: + return self.initial_position + + @override + def next_position( + self, + movement: TicTacToeMovement, + position: TicTacToePosition) -> TicTacToePosition: + board = deepcopy(position.board) + + if position.next_player() == PlayerIndex.FirstPlayer: + board[movement.row][movement.column] = TicTacToePosition.TicTacToePiece.FirstPlayer + else: + board[movement.row][movement.column] = TicTacToePosition.TicTacToePiece.SecondPlayer + + return TicTacToePosition(board=board) + + @override + def possible_movements( + self, + position: TicTacToePosition) -> Iterator[TicTacToeMovement]: + movements = [] + for row in range(3): + for column in range(3): + if position.board[row][column] == TicTacToePosition.TicTacToePiece.Empty: + movements.append(TicTacToeMovement(row, column)) + return movements + + @staticmethod + def check_winner(position: TicTacToePosition) -> TicTacToePosition.TicTacToePiece: + # Check if there is a winner in the rows + for row in range(3): + if (position.board[row][0] != TicTacToePosition.TicTacToePiece.Empty + and position.board[row][0] == position.board[row][1] + and position.board[row][1] == position.board[row][2]): + return position.board[row][0] + + # Check if there is a winner in the columns + for column in range(3): + if (position.board[0][column] != TicTacToePosition.TicTacToePiece.Empty + and position.board[0][column] == position.board[1][column] + and position.board[1][column] == position.board[2][column]): + return position.board[0][column] + + # Check if there is a winner in the diagonals + if (position.board[0][0] != TicTacToePosition.TicTacToePiece.Empty + and position.board[0][0] == position.board[1][1] + and position.board[1][1] == position.board[2][2]): + return position.board[0][0] + + if (position.board[0][2] != TicTacToePosition.TicTacToePiece.Empty + and position.board[0][2] == position.board[1][1] + and position.board[1][1] == position.board[2][0]): + return position.board[0][2] + + return TicTacToePosition.TicTacToePiece.Empty + + + @override + def finished( + self, + position: TicTacToePosition) -> bool: + winner = TicTacToeRules.check_winner(position) + + # It have finished if there is a winner or the board is full + return winner != TicTacToePosition.TicTacToePiece.Empty or all( + [all([x != TicTacToePosition.TicTacToePiece.Empty for x in row]) for row in position.board]) + + @override + def score( + self, + position: TicTacToePosition) -> dict[PlayerIndex, float]: + winner = TicTacToeRules.check_winner(position) + + if winner == TicTacToePosition.TicTacToePiece.FirstPlayer: + return { + PlayerIndex.FirstPlayer: 0.0, + PlayerIndex.SecondPlayer: 3.0 + } + + if winner == TicTacToePosition.TicTacToePiece.SecondPlayer: + return { + PlayerIndex.FirstPlayer: 3.0, + PlayerIndex.SecondPlayer: 0.0 + } + + # If there is no winner, it is a draw + return { + PlayerIndex.FirstPlayer: 1.0, + PlayerIndex.SecondPlayer: 1.0 + } diff --git a/src/IArena/interfaces/IGameRules.py b/src/IArena/interfaces/IGameRules.py index 8812fbe..e869383 100644 --- a/src/IArena/interfaces/IGameRules.py +++ b/src/IArena/interfaces/IGameRules.py @@ -3,7 +3,7 @@ from IArena.interfaces.IPosition import IPosition from IArena.interfaces.IMovement import IMovement -from IArena.interfaces.PlayerIndex import PlayerIndex +from IArena.interfaces.Score import ScoreBoard from IArena.utils.decorators import pure_virtual @@ -42,7 +42,7 @@ def finished( @pure_virtual def score( self, - position: IPosition) -> dict[PlayerIndex, float]: + position: IPosition) -> ScoreBoard: pass def is_movement_possible( diff --git a/src/IArena/interfaces/IPosition.py b/src/IArena/interfaces/IPosition.py index 2ada791..4712186 100644 --- a/src/IArena/interfaces/IPosition.py +++ b/src/IArena/interfaces/IPosition.py @@ -7,7 +7,36 @@ class IPosition: Abstract class that represents a position of a game. """ + def __init__(self, rules: "IGameRules"): + self.rules = rules + @pure_virtual def next_player( self) -> PlayerIndex: pass + + def get_rules(self) -> "IGameRules": + """Get the rules of the game.""" + return self.rules + + +CostType = float + +class CostPosition(IPosition): + """ + Abstract class that represents a position of a game that has a cost. + This is very useful for one player games. + """ + + def __init__(self, rules: "IGameRules", cost: CostType): + super().__init__(rules) + self._cost = cost + + def cost(self) -> CostType: + return self._cost + + def __eq__(self, other: "CostPosition"): + return self._cost == other._cost + + def __str__(self): + return f"Cost: {self._cost}" diff --git a/src/IArena/interfaces/Score.py b/src/IArena/interfaces/Score.py new file mode 100644 index 0000000..a7c71e7 --- /dev/null +++ b/src/IArena/interfaces/Score.py @@ -0,0 +1,43 @@ + +from IArena.interfaces.PlayerIndex import PlayerIndex + +Score = float + +class ScoreBoard: + + def __init__(self): + self.score = {} + + def define_score(self, player: PlayerIndex, score: Score): + """Define the score of a player.""" + self.score[player] = score + + def get_score(self, player: PlayerIndex) -> Score: + """Get the score of a player.""" + return self.score[player] + + def __str__(self) -> str: + st = "" + for player in sorted(self.score, key=self.score.get, reverse=True): + st += "Player: <" + str(player) + "> : <" + str(self.score[player]) + ">\n" + return st + + def add_score(self, player: PlayerIndex, score: Score): + """Add score to a player.""" + if player in self.score: + self.score[player] += score + else: + self.score[player] = score + + def winner(self) -> Score: + """Get the winner of the game.""" + # Sort the players by score and return the higher + return sorted(self.score, key=self.score.get)[0] + + def join(self, score_board: 'ScoreBoard'): + """Join two score boards.""" + for player in score_board.score: + if player in self.score: + self.score[player] += score_board.score[player] + else: + self.score[player] = score_board.score[player] diff --git a/src/IArena/players/players.py b/src/IArena/players/players.py index b836a91..aad324c 100644 --- a/src/IArena/players/players.py +++ b/src/IArena/players/players.py @@ -48,15 +48,29 @@ def play( class PlayablePlayer(DummyPlayer): + SeparatorN = 40 + @override def play( self, position: IPosition) -> IMovement: - possibilities = self.rules_.possible_movements(position) - print (f'{position}') + + possibilities = list(self.rules_.possible_movements(position)) + + print ("=" * PlayablePlayer.SeparatorN) + print (f"Next player: {position.next_player()}") + print ("-" * PlayablePlayer.SeparatorN) + print (position) + print ("-" * PlayablePlayer.SeparatorN) + print ("Movements:") + for i, p in enumerate(possibilities): print(f' {i}: {p}') + + print ("=" * PlayablePlayer.SeparatorN) + move = int(input()) + return possibilities[move] diff --git a/src/IArena/tools/__init__.py b/src/IArena/tools/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/IArena/tools/play_along.py b/src/IArena/tools/play_along.py deleted file mode 100644 index b467faf..0000000 --- a/src/IArena/tools/play_along.py +++ /dev/null @@ -1,11 +0,0 @@ - -import IArena.tournaments.PlayableGame -import IArena.tournaments.GenericGame -import IArena.games.Hanoi -import IArena.players.players - -# Choose the game to play -rules = IArena.games.Hanoi.HanoiRules() - -game = IArena.tournaments.PlayableGame.PlayableGame(rules) -print(f"You got it in : {game.play()} movements")