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 @@
+
+
+
\ 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")