24 | {% endif %}
25 | {%- endblock %}
26 |
--------------------------------------------------------------------------------
/docs/source/examples/integrate.rst:
--------------------------------------------------------------------------------
1 | .. _ExamplesOfGameIntegration:
2 |
3 | Integrating easyAI with other frameworks
4 | ========================================
5 |
6 | The primary means of executing easyAI is with the ``play`` method
7 | of TwoPlayerGame. That method handles getting human input and executing
8 | AI functions from start to finish.
9 |
10 | But, when using easyAI with other frameworks, one must often break down the
11 | steps execution. For that, use the ``get_move`` method to get an AI players
12 | decision, and the ``play_move`` to properly execute a turn.
13 |
14 | Here are some games implementations using other frameworks provided in the
15 | ``examples`` folder of easyAI.
16 |
17 | Tic-Tac-Toe Using Flask
18 | -----------------------
19 |
20 | .. literalinclude:: ../../../easyAI/games/TicTacToe-Flask.py
21 |
22 | Game of Knights using Kivy
23 | --------------------------
24 |
25 | .. literalinclude:: ../../../easyAI/games/Knights-Kivy.py
26 |
27 |
--------------------------------------------------------------------------------
/docs/source/ref.rst:
--------------------------------------------------------------------------------
1 | Reference Manual
2 | ================
3 |
4 | Games
5 | -----
6 |
7 | .. autoclass:: easyAI.TwoPlayerGame
8 | :members:
9 | :show-inheritance:
10 |
11 |
12 | Players
13 | -------
14 |
15 | .. autoclass:: easyAI.Human_Player
16 | :show-inheritance:
17 |
18 | .. autoclass:: easyAI.AI_Player
19 | :show-inheritance:
20 |
21 | AI algorithms
22 | -------------
23 |
24 | .. autoclass:: easyAI.AI.Negamax
25 | :members:
26 | :show-inheritance:
27 |
28 | .. autoclass:: easyAI.AI.NonRecursiveNegamax
29 | :members:
30 | :show-inheritance:
31 |
32 | .. autoclass:: easyAI.AI.DUAL
33 | :members:
34 | :show-inheritance:
35 |
36 | .. autoclass:: easyAI.AI.SSS
37 | :members:
38 | :show-inheritance:
39 |
40 |
41 | Transposition tables
42 | --------------------
43 |
44 | .. autoclass:: easyAI.AI.TranspositionTable
45 | :members:
46 | :show-inheritance:
47 |
48 | Solving Games
49 | -------------
50 |
51 | .. autofunction:: easyAI.AI.solving.solve_with_iterative_deepening
52 |
53 | .. autofunction:: easyAI.AI.solving.solve_with_depth_first_search
54 |
--------------------------------------------------------------------------------
/.github/workflows/tests.yml:
--------------------------------------------------------------------------------
1 | name: Run tests
2 |
3 | on: [push]
4 |
5 | jobs:
6 | build:
7 | runs-on: ubuntu-latest
8 | strategy:
9 | matrix:
10 | python-version: [3.6, 3.7, 3.8, 3.9]
11 |
12 | steps:
13 | - uses: actions/checkout@v2
14 | - name: Set up Python ${{ matrix.python-version }}
15 | uses: actions/setup-python@v2
16 | with:
17 | python-version: ${{ matrix.python-version }}
18 | - name: Install dependencies
19 | run: |
20 | python -m pip install --upgrade pip
21 | pip install .
22 | pip install flake8 pytest
23 | if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
24 | - name: Lint with flake8
25 | run: |
26 | # stop the build if there are Python syntax errors or undefined names
27 | flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
28 | # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
29 | flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
30 | - name: Test with pytest
31 | run: |
32 | pytest
33 |
--------------------------------------------------------------------------------
/LICENCE.txt:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 | [OSI Approved License]
3 |
4 | The MIT License (MIT)
5 |
6 | Copyright (c) 2013 Zulko
7 |
8 | Permission is hereby granted, free of charge, to any person obtaining a copy
9 | of this software and associated documentation files (the "Software"), to deal
10 | in the Software without restriction, including without limitation the rights
11 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12 | copies of the Software, and to permit persons to whom the Software is
13 | furnished to do so, subject to the following conditions:
14 |
15 | The above copyright notice and this permission notice shall be included in
16 | all copies or substantial portions of the Software.
17 |
18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
24 | THE SOFTWARE.
25 |
26 |
--------------------------------------------------------------------------------
/docs/source/_themes/README:
--------------------------------------------------------------------------------
1 | Flask Sphinx Styles
2 | ===================
3 |
4 | This repository contains sphinx styles for Flask and Flask related
5 | projects. To use this style in your Sphinx documentation, follow
6 | this guide:
7 |
8 | 1. put this folder as _themes into your docs folder. Alternatively
9 | you can also use git submodules to check out the contents there.
10 | 2. add this to your conf.py:
11 |
12 | sys.path.append(os.path.abspath('_themes'))
13 | html_theme_path = ['_themes']
14 | html_theme = 'flask'
15 |
16 | The following themes exist:
17 |
18 | - 'flask' - the standard flask documentation theme for large
19 | projects
20 | - 'flask_small' - small one-page theme. Intended to be used by
21 | very small addon libraries for flask.
22 |
23 | The following options exist for the flask_small theme:
24 |
25 | [options]
26 | index_logo = '' filename of a picture in _static
27 | to be used as replacement for the
28 | h1 in the index.rst file.
29 | index_logo_height = 120px height of the index logo
30 | github_fork = '' repository name on github for the
31 | "fork me" badge
32 |
--------------------------------------------------------------------------------
/tests/test_negamax.py:
--------------------------------------------------------------------------------
1 | from easyAI import AI_Player, Negamax
2 | from easyAI.games import ConnectFour, Nim
3 | import numpy as np
4 |
5 |
6 | def test_negamax_saves_the_next_turn_even_in_a_desperate_situation():
7 | """In this game of Connect4, the AI ("circles") will lose whatever it plays:
8 |
9 | . . . . . . .
10 | O . . . . . .
11 | X . . . . . .
12 | O . O . . . .
13 | O X X X . . .
14 | O O X X . . .
15 |
16 | However the AI is expected to go for the furthest-possible-away defeat and
17 | therefore play on the second column to block a 1-move win of crosses.
18 | """
19 | ai_algo = Negamax(6)
20 | ai_player = AI_Player(ai_algo)
21 | game = ConnectFour(players=[ai_player, ai_player])
22 | game.board = np.array(
23 | [
24 | [1, 1, 2, 2, 0, 0, 0],
25 | [1, 2, 2, 2, 0, 0, 0],
26 | [1, 0, 1, 0, 0, 0, 0],
27 | [2, 0, 0, 0, 0, 0, 0],
28 | [1, 0, 0, 0, 0, 0, 0],
29 | [0, 0, 0, 0, 0, 0, 0],
30 | ]
31 | )
32 | assert ai_algo(game) == 1
33 |
34 |
35 | def test_nim_strategy_is_good():
36 | ai_algo = Negamax(6)
37 | game = Nim(piles=(4, 4))
38 | assert ai_algo(game) == "1,1"
39 |
--------------------------------------------------------------------------------
/docs/source/_themes/flask/static/small_flask.css:
--------------------------------------------------------------------------------
1 | /*
2 | * small_flask.css_t
3 | * ~~~~~~~~~~~~~~~~~
4 | *
5 | * :copyright: Copyright 2010 by Armin Ronacher.
6 | * :license: Flask Design License, see LICENSE for details.
7 | */
8 |
9 | body {
10 | margin: 0;
11 | padding: 20px 30px;
12 | }
13 |
14 | div.documentwrapper {
15 | float: none;
16 | background: white;
17 | }
18 |
19 | div.sphinxsidebar {
20 | display: block;
21 | float: none;
22 | width: 102.5%;
23 | margin: 50px -30px -20px -30px;
24 | padding: 10px 20px;
25 | background: #333;
26 | color: white;
27 | }
28 |
29 | div.sphinxsidebar h3, div.sphinxsidebar h4, div.sphinxsidebar p,
30 | div.sphinxsidebar h3 a {
31 | color: white;
32 | }
33 |
34 | div.sphinxsidebar a {
35 | color: #aaa;
36 | }
37 |
38 | div.sphinxsidebar p.logo {
39 | display: none;
40 | }
41 |
42 | div.document {
43 | width: 100%;
44 | margin: 0;
45 | }
46 |
47 | div.related {
48 | display: block;
49 | margin: 0;
50 | padding: 10px 0 20px 0;
51 | }
52 |
53 | div.related ul,
54 | div.related ul li {
55 | margin: 0;
56 | padding: 0;
57 | }
58 |
59 | div.footer {
60 | display: none;
61 | }
62 |
63 | div.bodywrapper {
64 | margin: 0;
65 | }
66 |
67 | div.body {
68 | min-height: 0;
69 | padding: 0;
70 | }
71 |
--------------------------------------------------------------------------------
/docs/source/crash_course.rst:
--------------------------------------------------------------------------------
1 | An easyAI crash course
2 | ======================
3 |
4 |
5 | Defining a game
6 | ---------------
7 |
8 | Defining a new game with easyAI looks like this: ::
9 |
10 | from easyAI import TwoPlayerGame
11 |
12 | class MyNewGame( TwoPlayerGame ):
13 |
14 | def __init__(self, players) :
15 | self.players = players
16 | self.current_player= 1 # initialization. Player #1 starts.
17 |
18 | def possible_moves(self) :
19 | return # all moves allowed to the current player
20 |
21 | def make_move(self, move) : # play the move !
22 | self.player.pos = move
23 |
24 | def is_over() :
25 | return # whether the game has ended.
26 |
27 | def show() :
28 | print # or display the current game
29 |
30 | Then you set the AI algorithm as follows: ::
31 |
32 | from easyAI import Negamax
33 |
34 | def scoring(game):
35 | """ give a (heuristic) score to the game """
36 | return 100 if lose(game) else 0 # very basic example
37 |
38 | ai_algo = Negamax(8,scoring) # AI will think 8 moves in advance
39 |
40 | Now you can start a game, for instance human vs. AI: ::
41 |
42 | from easyAI import Human_Player, AI_Player
43 |
44 | human = Human_Player( "Roger" ) # The name is optional :)
45 | ai = AI_Player( ai_algo )
46 | game = MyNewGame( [ human, ai ] )
47 | history = game.play() # Starts the game. Returns the 'history' at the end
48 |
--------------------------------------------------------------------------------
/easyAI/AI/HashTranspositionTable.py:
--------------------------------------------------------------------------------
1 | # contributed by mrfesol (Tomasz Wesolowski)
2 |
3 |
4 | class HashTranspositionTable:
5 | """
6 | Base Class for various types of hashes
7 | """
8 |
9 | def __init__(self):
10 | self.modulo = 1024 # default value
11 |
12 | def before(self, key):
13 | """
14 | Returns initial value of hash.
15 | It's also the place where you can initialize some auxiliary variables
16 | """
17 | return 0
18 |
19 | def after(self, key, hash):
20 | """
21 | Returns final value of hash
22 | """
23 | return hash
24 |
25 | def get_hash(self, key, depth=0):
26 | """
27 | Recursively computes a hash
28 | """
29 | ret_hash = self.before(key)
30 | if type(key) is int:
31 | return self.hash_int(key)
32 | if type(key) is str and len(key) <= 1:
33 | return self.hash_char(key)
34 | for v in list(key):
35 | ret_hash = self.join(ret_hash, self.get_hash(v, depth + 1)) % self.modulo
36 | if depth == 0:
37 | ret_hash = self.after(key, ret_hash)
38 | return ret_hash
39 |
40 | def hash_int(self, number):
41 | """
42 | Returns hash for a number
43 | """
44 | return number
45 |
46 | def hash_char(self, string):
47 | """
48 | Returns hash for an one-letter string
49 | """
50 | return ord(string)
51 |
52 | def join(self, one, two):
53 | """
54 | Returns combined hash from two hashes
55 | one - existing (combined) hash so far
56 | two - hash of new element
57 | one = join(one, two)
58 | """
59 | return (one * two) % self.modulo
--------------------------------------------------------------------------------
/easyAI/games/GameOfBones.py:
--------------------------------------------------------------------------------
1 | """ This is the example featured in section 'A quick example' of the docs """
2 |
3 | from easyAI import TwoPlayerGame
4 |
5 |
6 | class GameOfBones(TwoPlayerGame):
7 | """In turn, the players remove one, two or three bones from a
8 | pile of bones. The player who removes the last bone loses."""
9 |
10 | def __init__(self, players=None):
11 | self.players = players
12 | self.pile = 20 # start with 20 bones in the pile
13 | self.current_player = 1 # player 1 starts
14 |
15 | def possible_moves(self):
16 | return ["1", "2", "3"]
17 |
18 | def make_move(self, move):
19 | self.pile -= int(move) # remove bones.
20 |
21 | def win(self):
22 | return self.pile <= 0 # opponent took the last bone ?
23 |
24 | def is_over(self):
25 | return self.win() # game stops when someone wins.
26 |
27 | def scoring(self):
28 | return 100 if self.win() else 0
29 |
30 | def show(self):
31 | print("%d bones left in the pile" % (self.pile))
32 |
33 |
34 | if __name__ == "__main__":
35 | """
36 | Start a match (and store the history of moves when it ends)
37 | ai = Negamax(10) # The AI will think 10 moves in advance
38 | game = GameOfBones( [ AI_Player(ai), Human_Player() ] )
39 | history = game.play()
40 | """
41 |
42 | # Let's solve the game
43 |
44 | from easyAI import solve_with_iterative_deepening, Human_Player, AI_Player
45 | from easyAI.AI import TranspositionTable
46 |
47 | tt = TranspositionTable()
48 | GameOfBones.ttentry = lambda self: self.pile
49 | r, d, m = solve_with_iterative_deepening(
50 | GameOfBones(), range(2, 20), win_score=100, tt=tt
51 | )
52 | print(r, d, m) # see the docs.
53 |
54 | # Unbeatable AI !
55 |
56 | game = GameOfBones([AI_Player(tt), Human_Player()])
57 | game.play() # you will always lose this game :)
58 |
--------------------------------------------------------------------------------
/docs/source/_themes/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2010 by Armin Ronacher.
2 |
3 | Some rights reserved.
4 |
5 | Redistribution and use in source and binary forms of the theme, with or
6 | without modification, are permitted provided that the following conditions
7 | are met:
8 |
9 | * Redistributions of source code must retain the above copyright
10 | notice, this list of conditions and the following disclaimer.
11 |
12 | * Redistributions in binary form must reproduce the above
13 | copyright notice, this list of conditions and the following
14 | disclaimer in the documentation and/or other materials provided
15 | with the distribution.
16 |
17 | * The names of the contributors may not be used to endorse or
18 | promote products derived from this software without specific
19 | prior written permission.
20 |
21 | We kindly ask you to only use these themes in an unmodified manner just
22 | for Flask and Flask-related products, not for unrelated projects. If you
23 | like the visual style and want to use it for your own projects, please
24 | consider making some larger changes to the themes (such as changing
25 | font faces, sizes, colors or margins).
26 |
27 | THIS THEME IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
28 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
29 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
30 | ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
31 | LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
32 | CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
33 | SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
34 | INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
35 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
36 | ARISING IN ANY WAY OUT OF THE USE OF THIS THEME, EVEN IF ADVISED OF THE
37 | POSSIBILITY OF SUCH DAMAGE.
38 |
--------------------------------------------------------------------------------
/easyAI/games/TicTacToe.py:
--------------------------------------------------------------------------------
1 | from easyAI import TwoPlayerGame
2 | from easyAI.Player import Human_Player
3 |
4 |
5 | class TicTacToe(TwoPlayerGame):
6 | """The board positions are numbered as follows:
7 | 1 2 3
8 | 4 5 6
9 | 7 8 9
10 | """
11 |
12 | def __init__(self, players):
13 | self.players = players
14 | self.board = [0 for i in range(9)]
15 | self.current_player = 1 # player 1 starts.
16 |
17 | def possible_moves(self):
18 | return [i + 1 for i, e in enumerate(self.board) if e == 0]
19 |
20 | def make_move(self, move):
21 | self.board[int(move) - 1] = self.current_player
22 |
23 | def unmake_move(self, move): # optional method (speeds up the AI)
24 | self.board[int(move) - 1] = 0
25 |
26 | def lose(self):
27 | """ Has the opponent "three in line ?" """
28 | return any(
29 | [
30 | all([(self.board[c - 1] == self.opponent_index) for c in line])
31 | for line in [
32 | [1, 2, 3],
33 | [4, 5, 6],
34 | [7, 8, 9], # horiz.
35 | [1, 4, 7],
36 | [2, 5, 8],
37 | [3, 6, 9], # vertical
38 | [1, 5, 9],
39 | [3, 5, 7],
40 | ]
41 | ]
42 | ) # diagonal
43 |
44 | def is_over(self):
45 | return (self.possible_moves() == []) or self.lose()
46 |
47 | def show(self):
48 | print(
49 | "\n"
50 | + "\n".join(
51 | [
52 | " ".join([[".", "O", "X"][self.board[3 * j + i]] for i in range(3)])
53 | for j in range(3)
54 | ]
55 | )
56 | )
57 |
58 | def scoring(self):
59 | return -100 if self.lose() else 0
60 |
61 |
62 | if __name__ == "__main__":
63 |
64 | from easyAI import AI_Player, Negamax
65 |
66 | ai_algo = Negamax(6)
67 | TicTacToe([Human_Player(), AI_Player(ai_algo)]).play()
68 |
--------------------------------------------------------------------------------
/docs/source/examples/speedup_cython.pyx:
--------------------------------------------------------------------------------
1 | """
2 |
3 | The Cython code for `find_four`.
4 | Here is the procedure to integrate this into the IPython Notebook
5 | in a first iPython cell you type
6 |
7 | %load_ext cythonmagic
8 |
9 | Then in another cell you write on the first line
10 |
11 | %%cython
12 |
13 | then the actual code
14 |
15 | """
16 |
17 |
18 | import numpy as np
19 | cimport numpy as np
20 |
21 | """
22 | The next array represents starting tiles and directions in which to
23 | search for four connected pieces. It has been obtained with
24 |
25 | >>> print np.array(
26 | [[[i,0],[0,1]] for i in range(6)]+
27 | [ [[0,i],[1,0]] for i in range(7)]+
28 | [ [[i,0],[1,1]] for i in range(1,3)]+
29 | [ [[0,i],[1,1]] for i in range(4)]+
30 | [ [[i,6],[1,-1]] for i in range(1,3)]+
31 | [ [[0,i],[1,-1]] for i in range(3,7)]).flatten()
32 | """
33 |
34 | cdef int *POS_DIR = [ 0, 0, 0, 1, 1, 0, 0, 1, 2, 0,
35 | 0, 1, 3, 0, 0, 1, 4, 0, 0, 1, 5, 0, 0, 1,
36 | 0, 0, 1, 0, 0, 1, 1, 0, 0, 2, 1, 0, 0, 3,
37 | 1, 0, 0, 4, 1, 0, 0, 5, 1, 0, 0, 6, 1, 0,
38 | 1, 0, 1, 1, 2, 0, 1, 1, 0, 0, 1, 1, 0, 1,
39 | 1, 1, 0, 2, 1, 1, 0, 3, 1, 1, 1, 6, 1, -1,
40 | 2, 6, 1, -1, 0, 3, 1, -1, 0, 4, 1, -1, 0, 5,
41 | 1, -1, 0, 6, 1, -1]
42 |
43 | cpdef int find_four(np.ndarray[int, ndim=2] board, int current_player):
44 |
45 | cdef int i, streak, pos_i, pos_j , dir_i, dir_j
46 |
47 | for i in range(25):
48 |
49 | pos_i = POS_DIR[4*i+0]
50 | pos_j = POS_DIR[4*i+1]
51 | dir_i = POS_DIR[4*i+2]
52 | dir_j = POS_DIR[4*i+3]
53 |
54 | streak = 0
55 |
56 | while (0 <= pos_i <= 5) and (0 <= pos_j <= 6):
57 | if board[pos_i][pos_j] == current_player:
58 | streak += 1
59 | if streak == 4:
60 | return 1
61 | else:
62 | streak = 0
63 | pos_i = pos_i + dir_i
64 | pos_j = pos_j + dir_j
65 |
66 | return 0
67 |
68 |
69 |
--------------------------------------------------------------------------------
/easyAI/Player.py:
--------------------------------------------------------------------------------
1 | """
2 | This module implements the Player (Human or AI), which is basically an
3 | object with an ``ask_move(game)`` method
4 | """
5 | try:
6 | input = raw_input
7 | except NameError:
8 | pass
9 |
10 |
11 | class Human_Player:
12 | """
13 | Class for a human player, which gets asked by text what moves
14 | she wants to play. She can type ``show moves`` to display a list of
15 | moves, or ``quit`` to quit the game.
16 | """
17 |
18 | def __init__(self, name="Human"):
19 | self.name = name
20 |
21 | def ask_move(self, game):
22 | possible_moves = game.possible_moves()
23 | # The str version of every move for comparison with the user input:
24 | possible_moves_str = list(map(str, game.possible_moves()))
25 | move = "NO_MOVE_DECIDED_YET"
26 | while True:
27 | move = input("\nPlayer %s what do you play ? " % (game.current_player))
28 | if move == "show moves":
29 | print(
30 | "Possible moves:\n"
31 | + "\n".join(
32 | ["#%d: %s" % (i + 1, m) for i, m in enumerate(possible_moves)]
33 | )
34 | + "\nType a move or type 'move #move_number' to play."
35 | )
36 |
37 | elif move == "quit":
38 | raise KeyboardInterrupt
39 |
40 | elif move.startswith("move #"):
41 | # Fetch the corresponding move and return.
42 | move = possible_moves[int(move[6:]) - 1]
43 | return move
44 |
45 | elif str(move) in possible_moves_str:
46 | # Transform the move into its real type (integer, etc. and return).
47 | move = possible_moves[possible_moves_str.index(str(move))]
48 | return move
49 |
50 |
51 | class AI_Player:
52 | """
53 | Class for an AI player. This class must be initialized with an
54 | AI algortihm, like ``AI_Player( Negamax(9) )``
55 | """
56 |
57 | def __init__(self, AI_algo, name="AI"):
58 | self.AI_algo = AI_algo
59 | self.name = name
60 | self.move = {}
61 |
62 | def ask_move(self, game):
63 | return self.AI_algo(game)
64 |
--------------------------------------------------------------------------------
/docs/source/ai_descriptions.rst:
--------------------------------------------------------------------------------
1 | AI Class Descriptions
2 | =====================
3 |
4 | EasyAI has four AI classes available; each with their own characteristics.
5 |
6 | Negamax with Alpha/Beta Pruning
7 | -------------------------------
8 |
9 | Negamax is a variation of the MiniMax algorithm. Minimax works by always considering the worst-case score for all possible moves for a set number of turns (plies) into the future. See https://en.wikipedia.org/wiki/Minimax
10 |
11 | Negamax is a more efficient version of Minimax for games where the scoring is zero-sum. That is, when the score of the game board is exactly the opposite for each player. For example, if player A sees the game currently having a "score" of 7, then player B sees the score as -7.
12 |
13 | This algorithmm also supports alpha/beta pruning. Instead of considering ALL branches, the algorithm ignores branches that cannot possibly be better given what it has seen so far.
14 |
15 | For more information, see https://en.wikipedia.org/wiki/Negamax
16 |
17 | Non-Recursive Negamax
18 | ---------------------
19 |
20 | This variation of Negamax has the same features as the regular Negamax described above. The difference
21 | is that the algorithm has been redesigned to not use recursion.
22 |
23 | Recursion is where a process calls itself. For example, if function A calls function A which in turn calls function A, then it is behaving recursively. The negamax algorithm is naturally recursive. One of the problems with resursion is that it is not possible to predict the amount of memory and processing needed to finish the algorithm.
24 |
25 | This variation instead pre-allocates a "list of states" to avoid recursion. For some games, this can dramatically improve performance.
26 |
27 | DUAL
28 | ----
29 |
30 | A variation of the Monte-Carlo Tree search algorithm described by L. W. Zhang and S. X. He, *The convergence of a dual algorithm for nonlinear programming*, Korean J. Comput. & Appl. Math.7 (2000), 487–506.
31 |
32 | SSS*
33 | ----
34 |
35 | A minimax algorithm similar to Negamax but where the pruning is much more extreme. It prunes all but one branch at each node in the decision tree.
36 |
37 | As such, the SSS* algorithm does not always provide the ideal/right answer. But it can dramatically increase the performance of some games without sacrificing quality too much. It depends the nature of the game.
38 |
39 | See https://en.wikipedia.org/wiki/SSS*
40 |
--------------------------------------------------------------------------------
/easyAI/AI/SSS.py:
--------------------------------------------------------------------------------
1 | # contributed by mrfesol (Tomasz Wesolowski)
2 |
3 | from .MTdriver import mtd
4 |
5 |
6 | class SSS:
7 | """
8 | This implements the SSS* algorithm. The following example shows
9 | how to setup the AI and play a Connect Four game:
10 |
11 | >>> from easyAI import Human_Player, AI_Player, SSS
12 | >>> AI = SSS(7)
13 | >>> game = ConnectFour([AI_Player(AI),Human_Player()])
14 | >>> game.play()
15 |
16 | Parameters
17 | -----------
18 |
19 | depth:
20 | How many moves in advance should the AI think ?
21 | (2 moves = 1 complete turn)
22 |
23 | scoring:
24 | A function f(game)-> score. If no scoring is provided
25 | and the game object has a ``scoring`` method it ill be used.
26 |
27 | win_score:
28 | Score LARGER than the largest score of game, but smaller than inf.
29 | It's required to run algorithm.
30 |
31 | tt:
32 | A transposition table (a table storing game states and moves)
33 | scoring: can be none if the game that the AI will be given has a
34 | ``scoring`` method.
35 |
36 | Notes
37 | -----
38 |
39 | The score of a given game is given by
40 |
41 | >>> scoring(current_game) - 0.01*sign*current_depth
42 |
43 | for instance if a lose is -100 points, then losing after 4 moves
44 | will score -99.96 points but losing after 8 moves will be -99.92
45 | points. Thus, the AI will chose the move that leads to defeat in
46 | 8 turns, which makes it more difficult for the (human) opponent.
47 | This will not always work if a ``win_score`` argument is provided.
48 |
49 | """
50 |
51 | def __init__(self, depth, scoring=None, win_score=100000, tt=None):
52 | self.scoring = scoring
53 | self.depth = depth
54 | self.tt = tt
55 | self.win_score = win_score
56 |
57 | def __call__(self, game):
58 | """
59 | Returns the AI's best move given the current state of the game.
60 | """
61 |
62 | scoring = (
63 | self.scoring if self.scoring else (lambda g: g.scoring())
64 | ) # horrible hack
65 |
66 | first = self.win_score # essence of SSS algorithm
67 |
68 | def next(lowerbound, upperbound, best_value):
69 | return best_value
70 |
71 | self.alpha = mtd(game, first, next, self.depth, scoring, self.tt)
72 |
73 | return game.ai_move
74 |
--------------------------------------------------------------------------------
/docs/source/index.rst:
--------------------------------------------------------------------------------
1 | easyAI
2 | ======
3 |
4 |
5 | EasyAI is an artificial intelligence framework for two-players abstract games such as Tic Tac Toe, Connect 4, Reversi, etc.
6 |
7 | It is written in Python and makes it easy to define the mechanisms of a game and play against the computer or solve the game (see :ref:`a-quick-example`).
8 |
9 | Under the hood, the AI is a Negamax algorithm with alpha-beta pruning and transposition tables as described on Wikipedia_. It has been written with clarity/simplicity in mind, rather than speed, so it can be slow, but there are fixes (see :ref:`speedup`).
10 |
11 | .. raw:: html
12 |
13 |
14 | Tweet
16 |
17 |
21 |
22 |
24 |
25 |
26 | User's Guide
27 | --------------
28 |
29 | .. toctree::
30 | :maxdepth: 1
31 |
32 | installation
33 | get_started
34 | examples/examples
35 | speedup
36 | ai_descriptions
37 | ref
38 |
39 | Contribute !
40 | -------------
41 |
42 | EasyAI is an open source software originally written by Zulko_ and released under the MIT licence.
43 | It is hosted on Github_, where you can submit improvements, get support, etc.
44 |
45 | Some ideas of improvements are: AI algos for incomplete information games, better game solving strategies, (efficient) use of databases to store moves, AI algorithms using parallelisation. Want to make one of these happen ?
46 |
47 | .. raw:: html
48 |
49 |
50 |
53 |
54 | .. _Wikipedia: http://en.wikipedia.org/wiki/Negamax
55 | .. _`game design`:
56 | .. _`AI design/optimization`:
57 | .. _Zulko : https://github.com/Zulko
58 | .. _JohnAD : https://github.com/JohnAD
59 | .. _Github : https://github.com/Zulko/easyAI
60 |
61 | Maintainers
62 | -----------
63 |
64 | - Zulko_ (owner)
65 | - JohnAD_
66 |
--------------------------------------------------------------------------------
/easyAI/AI/DUAL.py:
--------------------------------------------------------------------------------
1 | #contributed by mrfesol (Tomasz Wesolowski)
2 |
3 | from easyAI.AI.MTdriver import mtd
4 |
5 | class DUAL:
6 | """
7 | This implements DUAL algorithm. The following example shows
8 | how to setup the AI and play a Connect Four game:
9 |
10 | >>> from easyAI import Human_Player, AI_Player, DUAL
11 | >>> AI = DUAL(7)
12 | >>> game = ConnectFour([AI_Player(AI),Human_Player()])
13 | >>> game.play()
14 |
15 | Parameters
16 | -----------
17 |
18 | depth:
19 | How many moves in advance should the AI think ?
20 | (2 moves = 1 complete turn)
21 |
22 | scoring:
23 | A function f(game)-> score. If no scoring is provided
24 | and the game object has a ``scoring`` method it ill be used.
25 |
26 | win_score:
27 | Score LARGER than the largest score of game, but smaller than inf.
28 | It's required to run algorithm.
29 |
30 | tt:
31 | A transposition table (a table storing game states and moves)
32 | scoring: can be none if the game that the AI will be given has a
33 | ``scoring`` method.
34 |
35 | Notes
36 | -----
37 |
38 | The score of a given game is given by
39 |
40 | >>> scoring(current_game) - 0.01*sign*current_depth
41 |
42 | for instance if a lose is -100 points, then losing after 4 moves
43 | will score -99.96 points but losing after 8 moves will be -99.92
44 | points. Thus, the AI will chose the move that leads to defeat in
45 | 8 turns, which makes it more difficult for the (human) opponent.
46 | This will not always work if a ``win_score`` argument is provided.
47 |
48 | """
49 |
50 | def __init__(self, depth, scoring=None, win_score=100000, tt=None):
51 | self.scoring = scoring
52 | self.depth = depth
53 | self.tt = tt
54 | self.win_score= win_score
55 |
56 | def __call__(self,game):
57 | """
58 | Returns the AI's best move given the current state of the game.
59 | """
60 |
61 | scoring = self.scoring if self.scoring else (
62 | lambda g: g.scoring() ) # horrible hack
63 |
64 | first = -self.win_score #essence of DUAL algorithm
65 | next = (lambda lowerbound, upperbound, bestValue: bestValue + 1)
66 |
67 | self.alpha = mtd(game,
68 | first, next,
69 | self.depth,
70 | scoring,
71 | self.tt)
72 |
73 | return game.ai_move
74 |
--------------------------------------------------------------------------------
/easyAI/games/Hexapawn.py:
--------------------------------------------------------------------------------
1 | from easyAI import TwoPlayerGame
2 |
3 | # Convert D7 to (3,6) and back...
4 | to_string = lambda move: " ".join(
5 | ["ABCDEFGHIJ"[move[i][0]] + str(move[i][1] + 1) for i in (0, 1)]
6 | )
7 | to_tuple = lambda s: ("ABCDEFGHIJ".index(s[0]), int(s[1:]) - 1)
8 |
9 |
10 | class Hexapawn(TwoPlayerGame):
11 | """
12 | A nice game whose rules are explained here:
13 | http://fr.wikipedia.org/wiki/Hexapawn
14 | """
15 |
16 | def __init__(self, players, size=(4, 4)):
17 | self.size = M, N = size
18 | p = [[(i, j) for j in range(N)] for i in [0, M - 1]]
19 |
20 | for i, d, goal, pawns in [(0, 1, M - 1, p[0]), (1, -1, 0, p[1])]:
21 | players[i].direction = d
22 | players[i].goal_line = goal
23 | players[i].pawns = pawns
24 |
25 | self.players = players
26 | self.current_player = 1
27 |
28 | def possible_moves(self):
29 | moves = []
30 | opponent_pawns = self.opponent.pawns
31 | d = self.player.direction
32 | for i, j in self.player.pawns:
33 | if (i + d, j) not in opponent_pawns:
34 | moves.append(((i, j), (i + d, j)))
35 | if (i + d, j + 1) in opponent_pawns:
36 | moves.append(((i, j), (i + d, j + 1)))
37 | if (i + d, j - 1) in opponent_pawns:
38 | moves.append(((i, j), (i + d, j - 1)))
39 |
40 | return list(map(to_string, [(i, j) for i, j in moves]))
41 |
42 | def make_move(self, move):
43 | move = list(map(to_tuple, move.split(" ")))
44 | ind = self.player.pawns.index(move[0])
45 | self.player.pawns[ind] = move[1]
46 |
47 | if move[1] in self.opponent.pawns:
48 | self.opponent.pawns.remove(move[1])
49 |
50 | def lose(self):
51 | return any([i == self.opponent.goal_line for i, j in self.opponent.pawns]) or (
52 | self.possible_moves() == []
53 | )
54 |
55 | def is_over(self):
56 | return self.lose()
57 |
58 | def show(self):
59 | f = (
60 | lambda x: "1"
61 | if x in self.players[0].pawns
62 | else ("2" if x in self.players[1].pawns else ".")
63 | )
64 | print(
65 | "\n".join(
66 | [
67 | " ".join([f((i, j)) for j in range(self.size[1])])
68 | for i in range(self.size[0])
69 | ]
70 | )
71 | )
72 |
73 |
74 | if __name__ == "__main__":
75 | from easyAI import AI_Player, Human_Player, Negamax
76 |
77 | scoring = lambda game: -100 if game.lose() else 0
78 | ai = Negamax(10, scoring)
79 | game = Hexapawn([AI_Player(ai), AI_Player(ai)])
80 | game.play()
81 | print("player %d wins after %d turns " % (game.opponent_index, game.nmove))
82 |
--------------------------------------------------------------------------------
/easyAI/games/ConnectFour.py:
--------------------------------------------------------------------------------
1 | try:
2 | import numpy as np
3 | except ImportError:
4 | print("Sorry, this example requires Numpy installed !")
5 | raise
6 |
7 | from easyAI import TwoPlayerGame
8 |
9 |
10 | class ConnectFour(TwoPlayerGame):
11 | """
12 | The game of Connect Four, as described here:
13 | http://en.wikipedia.org/wiki/Connect_Four
14 | """
15 |
16 | def __init__(self, players, board=None):
17 | self.players = players
18 | self.board = (
19 | board
20 | if (board is not None)
21 | else (np.array([[0 for i in range(7)] for j in range(6)]))
22 | )
23 | self.current_player = 1 # player 1 starts.
24 |
25 | def possible_moves(self):
26 | return [i for i in range(7) if (self.board[:, i].min() == 0)]
27 |
28 | def make_move(self, column):
29 | line = np.argmin(self.board[:, column] != 0)
30 | self.board[line, column] = self.current_player
31 |
32 | def show(self):
33 | print(
34 | "\n"
35 | + "\n".join(
36 | ["0 1 2 3 4 5 6", 13 * "-"]
37 | + [
38 | " ".join([[".", "O", "X"][self.board[5 - j][i]] for i in range(7)])
39 | for j in range(6)
40 | ]
41 | )
42 | )
43 |
44 | def lose(self):
45 | return find_four(self.board, self.opponent_index)
46 |
47 | def is_over(self):
48 | return (self.board.min() > 0) or self.lose()
49 |
50 | def scoring(self):
51 | return -100 if self.lose() else 0
52 |
53 |
54 | def find_four(board, current_player):
55 | """
56 | Returns True iff the player has connected 4 (or more)
57 | This is much faster if written in C or Cython
58 | """
59 | for pos, direction in POS_DIR:
60 | streak = 0
61 | while (0 <= pos[0] <= 5) and (0 <= pos[1] <= 6):
62 | if board[pos[0], pos[1]] == current_player:
63 | streak += 1
64 | if streak == 4:
65 | return True
66 | else:
67 | streak = 0
68 | pos = pos + direction
69 | return False
70 |
71 |
72 | POS_DIR = np.array(
73 | [[[i, 0], [0, 1]] for i in range(6)]
74 | + [[[0, i], [1, 0]] for i in range(7)]
75 | + [[[i, 0], [1, 1]] for i in range(1, 3)]
76 | + [[[0, i], [1, 1]] for i in range(4)]
77 | + [[[i, 6], [1, -1]] for i in range(1, 3)]
78 | + [[[0, i], [1, -1]] for i in range(3, 7)]
79 | )
80 |
81 | if __name__ == "__main__":
82 | # LET'S PLAY !
83 |
84 | from easyAI import AI_Player, Negamax, SSS
85 |
86 | ai_algo_neg = Negamax(5)
87 | ai_algo_sss = SSS(5)
88 | game = ConnectFour([AI_Player(ai_algo_neg), AI_Player(ai_algo_sss)])
89 | game.play()
90 | if game.lose():
91 | print("Player %d wins." % (game.opponent_index))
92 | else:
93 | print("Looks like we have a draw.")
94 |
--------------------------------------------------------------------------------
/easyAI/games/Cram.py:
--------------------------------------------------------------------------------
1 | import numpy as np
2 | from easyAI import TwoPlayerGame
3 |
4 |
5 | # directions in which a knight can move
6 | DIRECTIONS = list(
7 | map(
8 | np.array,
9 | [[1, 2], [-1, 2], [1, -2], [-1, -2], [2, 1], [2, -1], [-2, 1], [-2, -1]],
10 | )
11 | )
12 |
13 |
14 | # functions to convert "D8" into (3,7) and back...
15 | pos2string = lambda a: "ABCDEFGH"[a[0]] + str(a[1] + 1)
16 | string2pos = lambda s: ["ABCDEFGH".index(s[0]), int(s[1]) - 1]
17 |
18 | mov2string = lambda m: pos2string((m[0], m[1])) + " " + pos2string((m[2], m[3]))
19 |
20 |
21 | def string2mov(s):
22 | poss = [string2pos(p) for p in s.split(" ")]
23 | return poss[0] + poss[1]
24 |
25 |
26 | class Cram(TwoPlayerGame):
27 | """
28 | Players place a domino on the grid (provide x1,y1,x2,y2)
29 | """
30 |
31 | def __init__(self, players, board_size=(6, 6)):
32 | self.players = players
33 | self.board_size = board_size
34 | self.board = np.zeros(board_size, dtype=int)
35 | self.current_player = 1 # player 1 starts.
36 |
37 | def possible_moves(self):
38 | moves = []
39 | for i in range(self.board_size[0]):
40 | for j in range(self.board_size[1]):
41 | if self.board[i, j] == 0:
42 | if (i + 1) < self.board_size[0] and self.board[i + 1, j] == 0:
43 | moves.append([i, j, i + 1, j])
44 | if (j + 1) < self.board_size[1] and self.board[i, j + 1] == 0:
45 | moves.append([i, j, i, j + 1])
46 | return list(map(mov2string, moves))
47 |
48 | def make_move(self, move):
49 | move = string2mov(move)
50 | self.board[move[0], move[1]] = 1
51 | self.board[move[2], move[3]] = 1
52 |
53 | def unmake_move(self, move):
54 | move = string2mov(move)
55 | self.board[move[0], move[1]] = 0
56 | self.board[move[2], move[3]] = 0
57 |
58 | def show(self):
59 | print(
60 | "\n"
61 | + "\n".join(
62 | [" 1 2 3 4 5 6 7 8"]
63 | + [
64 | "ABCDEFGH"[k]
65 | + " "
66 | + " ".join(
67 | [".*"[self.board[k, i]] for i in range(self.board_size[0])]
68 | )
69 | for k in range(self.board_size[1])
70 | ]
71 | + [""]
72 | )
73 | )
74 |
75 | def lose(self):
76 | return self.possible_moves() == []
77 |
78 | def scoring(self):
79 | return -100 if (self.possible_moves() == []) else 0
80 |
81 | def is_over(self):
82 | return self.lose()
83 |
84 |
85 | if __name__ == "__main__":
86 | from easyAI import AI_Player, Negamax
87 |
88 | ai_algo = Negamax(6)
89 | game = Cram([AI_Player(ai_algo), AI_Player(ai_algo)], (5, 5))
90 | game.play()
91 | print("player %d loses" % game.current_player)
--------------------------------------------------------------------------------
/docs/source/examples/quick_example.rst:
--------------------------------------------------------------------------------
1 | .. _a-quick-example:
2 |
3 | A quick example
4 | ================
5 |
6 | Let us define the rules of a game and start a match against the AI: ::
7 |
8 | from easyAI import TwoPlayerGame, Human_Player, AI_Player, Negamax
9 |
10 | class GameOfBones( TwoPlayerGame ):
11 | """ In turn, the players remove one, two or three bones from a
12 | pile of bones. The player who removes the last bone loses. """
13 |
14 | def __init__(self, players=None):
15 | self.players = players
16 | self.pile = 20 # start with 20 bones in the pile
17 | self.current_player = 1 # player 1 starts
18 |
19 | def possible_moves(self): return ['1','2','3']
20 | def make_move(self,move): self.pile -= int(move) # remove bones.
21 | def win(self): return self.pile<=0 # opponent took the last bone ?
22 | def is_over(self): return self.win() # Game stops when someone wins.
23 | def show(self): print "%d bones left in the pile"%self.pile
24 | def scoring(self): return 100 if self.win() else 0 # For the AI
25 |
26 | # Start a match (and store the history of moves when it ends)
27 | ai = Negamax(13) # The AI will think 13 moves in advance
28 | game = GameOfBones( [ Human_Player(), AI_Player(ai) ] )
29 | history = game.play()
30 |
31 | Result: ::
32 |
33 | 20 bones left in the pile
34 |
35 | Player 1 what do you play ? 3
36 |
37 | Move #1: player 1 plays 3 :
38 | 17 bones left in the pile
39 |
40 | Move #2: player 2 plays 1 :
41 | 16 bones left in the pile
42 |
43 | Player 1 what do you play ?
44 |
45 | Solving the game
46 | -----------------
47 |
48 | Let us now solve the game: ::
49 |
50 | from easyAI import solve_with_iterative_deepening
51 | r,d,m = solve_with_iterative_deepening(GameOfBones(), ai_depths=range(2,20), win_score=100)
52 |
53 | We obtain ``r=1``, meaning that if both players play perfectly, the first player to play can always win (-1 would have meant always lose), ``d=10``, which means that the wins will be in ten moves (i.e. 5 moves per player) or less, and ``m='3'``, which indicates that the first player's first move should be ``'3'``.
54 |
55 | These computations can be sped up using a transposition table which will store the situations encountered and the best moves for each: ::
56 |
57 | tt = TranspositionTable()
58 | GameOfBones.ttentry = lambda game : game.pile # key for the table
59 | r,d,m = solve_with_iterative_deepening(GameOfBones(), range(2,20), win_score=100, tt=tt)
60 |
61 | After these lines are run the variable ``tt`` contains a transposition table storing the possible situations (here, the possible sizes of the pile) and the optimal moves to perform. With ``tt`` you can play perfectly without *thinking*: ::
62 |
63 | game = GameOfBones( [ AI_Player( tt ), Human_Player() ] )
64 | game.play() # you will always lose this game :)
65 |
--------------------------------------------------------------------------------
/.github/workflows/version-bump-and-release.yml:
--------------------------------------------------------------------------------
1 | # taken from:
2 | # https://github.com/joaomcteixeira/python-project-skeleton/blob/master/.github/workflows/version-bump-and-package.yml
3 | # Where it was in turn taken from:
4 | # https://github.com/haddocking/pdb-tools/blob/f019d163d8f8cc5a0cba288e02f5a63a969719f6/.github/workflows/bump-version-on-push.yml
5 |
6 | name: Version Bump & Release
7 |
8 | on:
9 | push:
10 | branches:
11 | - master
12 |
13 | jobs:
14 | bump-version:
15 | runs-on: ubuntu-latest
16 | if: "!startsWith(github.event.head_commit.message, '[SKIP]')"
17 |
18 | steps:
19 | - uses: actions/checkout@v2
20 | with:
21 | # I setup a new token for my GitHub user and added that token
22 | # to the secrets in the repository
23 | # When I tried
24 | # https://docs.github.com/en/actions/reference/authentication-in-a-workflow
25 | # I had some problems, they could be my fault, but yet, I felt using a
26 | # dedicated token would be better and suffice
27 | token: ${{ secrets.github_token }}
28 |
29 | - name: Set up Python
30 | uses: actions/setup-python@v2
31 | with:
32 | python-version: "3.x"
33 |
34 | - name: Setup Git
35 | run: |
36 | git config user.name "zulko"
37 | git config user.email 'zulko@users.noreply.github.com'
38 | git remote set-url origin https://x-access-token:${{ secrets.github_token }}@github.com/$GITHUB_REPOSITORY
39 | git checkout "${GITHUB_REF:11}"
40 | - name: Setup env variables
41 | run: |
42 | echo "SKIPBUMP=FALSE" >> $GITHUB_ENV
43 | - name: Install dependencies
44 | run: |
45 | python -m pip install --upgrade pip
46 | pip install bump2version setuptools wheel twine
47 | # If a commit starts with [MAJOR] a new major verion upgrade will be
48 | # triggered. Use with caution as Major upgrades denote backwards
49 | # incompatibility. Yet I like it to be integrated in the CI
50 | - name: Bump Major Version
51 | env:
52 | COMMIT_MSG: ${{ github.event.head_commit.message }}
53 | run: |
54 | bump2version minor
55 | echo "SKIPBUMP=TRUE" >> $GITHUB_ENV
56 | if: "startsWith(github.event.head_commit.message, '[MAJOR]')"
57 |
58 | - name: Bump Minor Version
59 | env:
60 | COMMIT_MSG: ${{ github.event.head_commit.message }}
61 | run: |
62 | bump2version minor
63 | echo "SKIPBUMP=TRUE" >> $GITHUB_ENV
64 | if: "startsWith(github.event.head_commit.message, '[FEATURE]')"
65 |
66 | # Default action
67 | - name: Bump Patch Version
68 | env:
69 | COMMIT_MSG: ${{ github.event.head_commit.message }}
70 | run: |
71 | bump2version patch
72 | if: env.SKIPBUMP == 'FALSE'
73 |
74 | - name: Commit version change to master
75 | run: |
76 | git push --follow-tags
77 |
78 | - name: Build the distribution
79 | run: |
80 | python setup.py sdist bdist_wheel
81 |
82 | - name: Publish distribution 📦 to PyPI
83 | uses: pypa/gh-action-pypi-publish@master
84 | with:
85 | password: ${{ secrets.PYPI_API_TOKEN }}
86 |
--------------------------------------------------------------------------------
/easyAI/games/Nim.py:
--------------------------------------------------------------------------------
1 | from easyAI import TwoPlayerGame
2 |
3 |
4 | class Nim(TwoPlayerGame):
5 | """
6 | The game starts with 4 piles of 5 pieces. In turn the players
7 | remove as much pieces as they want, but from one pile only. The
8 | player that removes the last piece loses.
9 |
10 | Parameters
11 | ----------
12 |
13 | players
14 | List of the two players e.g. [HumanPlayer(), HumanPlayer()]
15 |
16 | piles:
17 | The piles the game starts with. With piles=[2,3,4,4] the
18 | game will start with 1 pile of 2 pieces, 1 pile of 3 pieces, and 2
19 | piles of 4 pieces.
20 |
21 | max_removals_per_turn
22 | Max number of pieces you can remove in a turn. Default is no limit.
23 |
24 | """
25 |
26 | def __init__(self, players=None, max_removals_per_turn=None, piles=(5, 5, 5, 5)):
27 | """ Default for `piles` is 5 piles of 5 pieces. """
28 | self.players = players
29 | self.piles = list(piles)
30 | self.max_removals_per_turn = max_removals_per_turn
31 | self.current_player = 1 # player 1 starts.
32 |
33 | def possible_moves(self):
34 | return [
35 | "%d,%d" % (i + 1, j)
36 | for i in range(len(self.piles))
37 | for j in range(
38 | 1,
39 | self.piles[i] + 1
40 | if self.max_removals_per_turn is None
41 | else min(self.piles[i] + 1, self.max_removals_per_turn),
42 | )
43 | ]
44 |
45 | def make_move(self, move):
46 | move = list(map(int, move.split(",")))
47 | self.piles[move[0] - 1] -= move[1]
48 |
49 | def unmake_move(self, move): # optional, speeds up the AI
50 | move = list(map(int, move.split(",")))
51 | self.piles[move[0] - 1] += move[1]
52 |
53 | def show(self):
54 | print(" ".join(map(str, self.piles)))
55 |
56 | def win(self):
57 | return max(self.piles) == 0
58 |
59 | def is_over(self):
60 | return self.win()
61 |
62 | def scoring(self):
63 | return 100 if self.win() else 0
64 |
65 | def ttentry(self):
66 | return tuple(self.piles) # optional, speeds up AI
67 |
68 |
69 | if __name__ == "__main__":
70 | # IN WHAT FOLLOWS WE SOLVE THE GAME AND START A MATCH AGAINST THE AI
71 |
72 | from easyAI import AI_Player, Human_Player, Negamax, solve_with_iterative_deepening
73 | from easyAI.AI import TranspositionTable
74 |
75 | # we first solve the game
76 | w, d, m, tt = solve_with_iterative_deepening(Nim(), range(5, 20), win_score=80)
77 | w, d, len(tt.d)
78 | # the previous line prints -1, 16 which shows that if the
79 | # computer plays second with an AI depth of 16 (or 15) it will
80 | # always win in 16 (total) moves or less.
81 |
82 | # Now let's play (and lose !) against the AI
83 | ai = Negamax(16, tt=TranspositionTable())
84 | game = Nim([Human_Player(), AI_Player(tt)])
85 | game.play() # You will always lose this game !
86 | print("player %d wins" % game.current_player)
87 |
88 | # Note that with the transposition table tt generated by
89 | # solve_with_iterative_deepening
90 | # we can setup a perfect AI which doesn't have to think:
91 | # >>> game = Nim( [ Human_Player(), AI_Player( tt )])
92 | # >>> game.play() # You will always lose this game too!
93 |
--------------------------------------------------------------------------------
/easyAI/AI/DictTranspositionTable.py:
--------------------------------------------------------------------------------
1 | # contributed by mrfesol (Tomasz Wesolowski)
2 | from easyAI.AI.HashTranspositionTable import HashTranspositionTable
3 |
4 |
5 | class DictTranspositionTable:
6 | """
7 | A DictTranspositionTable implements custom dictionary,
8 | which can be used with transposition tables.
9 | """
10 |
11 | def __init__(self, num_buckets=1024, own_hash=None):
12 | """
13 | Initializes a dictionary with the given number of buckets.
14 | """
15 | self.dict = []
16 | for i in range(num_buckets):
17 | self.dict.append((None, None))
18 | self.keys = dict()
19 | self.hash = hash
20 | if own_hash is not None:
21 | own_hash.modulo = len(self.dict)
22 | self.hash = own_hash.get_hash
23 | self.num_collisions = 0
24 | self.num_calls = 0
25 |
26 | def hash_key(self, key):
27 | """
28 | Given a key this will create a number and then convert it to
29 | an index for the dict.
30 | """
31 | self.num_calls += 1
32 | return self.hash(key) % len(self.dict)
33 |
34 | def get_slot(self, key, default=None):
35 | """
36 | Returns the index, key, and value of a slot found in the dict.
37 | Returns -1, key, and default (None if not set) when not found.
38 | """
39 | slot = self.hash_key(key)
40 |
41 | if key == self.dict[slot][0]:
42 | return slot, self.dict[slot][0], self.dict[slot][1]
43 |
44 | return -1, key, default
45 |
46 | def get(self, key, default=None):
47 | """
48 | Gets the value for the given key, or the default.
49 | """
50 | i, k, v = self.get_slot(key, default=default)
51 | return v
52 |
53 | def set(self, key, value):
54 | """
55 | Sets the key to the value, replacing any existing value.
56 | """
57 | slot = self.hash_key(key)
58 |
59 | if self.dict[slot] != (None, None):
60 | self.num_collisions += 1 # collision occured
61 |
62 | self.dict[slot] = (key, value)
63 |
64 | if self.keys.__contains__(key):
65 | self.keys[key] = self.keys[key] + 1
66 | else:
67 | self.keys[key] = 1
68 |
69 | def delete(self, key):
70 | """
71 | Deletes the given key from the dictionary.
72 | """
73 |
74 | slot = self.hash_key(key)
75 | self.dict[slot] = (None, None)
76 |
77 | if self.keys.__contains__(key):
78 | self.keys[key] = self.keys[key] - 1
79 | if self.keys[key] <= 0:
80 | del self.keys[key]
81 |
82 | def collisions(self):
83 | return self.num_collisions
84 |
85 | def __getitem__(self, key):
86 | return self.get(key)
87 |
88 | def __missing__(self, key):
89 | return None
90 |
91 | def __setitem__(self, key, value):
92 | self.set(key, value)
93 |
94 | def __delitem__(self, key):
95 | self.delete(key)
96 |
97 | def __iter__(self):
98 | return iter(self.keys)
99 |
100 | def __contains__(self, key):
101 | return self.keys.__contains__(key)
102 |
--------------------------------------------------------------------------------
/easyAI/games/Knights.py:
--------------------------------------------------------------------------------
1 | import numpy as np
2 | from easyAI import TwoPlayerGame
3 |
4 |
5 | # directions in which a knight can move
6 | DIRECTIONS = list(
7 | map(
8 | np.array,
9 | [[1, 2], [-1, 2], [1, -2], [-1, -2], [2, 1], [2, -1], [-2, 1], [-2, -1]],
10 | )
11 | )
12 |
13 | # functions to convert "D8" into (3,7) and back...
14 | pos2string = lambda ab: "ABCDEFGH"[ab[0]] + str(ab[1] + 1)
15 | string2pos = lambda s: np.array(["ABCDEFGH".index(s[0]), int(s[1]) - 1])
16 |
17 |
18 | class Knights(TwoPlayerGame):
19 | """
20 | Each player has a chess knight (that moves in "L") on a chessboard.
21 | Each turn the player moves the knight to any tile that hasn't been
22 | occupied by a knight before. The first player that cannot move loses.
23 | """
24 |
25 | def __init__(self, players, board_size=(8, 8)):
26 | self.players = players
27 | self.board_size = board_size
28 | self.board = np.zeros(board_size, dtype=int)
29 | self.board[0, 0] = 1
30 | self.board[board_size[0] - 1, board_size[1] - 1] = 2
31 | players[0].pos = np.array([0, 0])
32 | players[1].pos = np.array([board_size[0] - 1, board_size[1] - 1])
33 | self.current_player = 1 # player 1 starts.
34 |
35 | def possible_moves(self):
36 | endings = [self.player.pos + d for d in DIRECTIONS]
37 | return [
38 | pos2string(e)
39 | for e in endings # all positions
40 | if (e[0] >= 0)
41 | and (e[1] >= 0)
42 | and (e[0] < self.board_size[0])
43 | and (e[1] < self.board_size[1])
44 | and self.board[e[0], e[1]] == 0 # inside the board
45 | ] # and not blocked
46 |
47 | def make_move(self, pos):
48 | pi, pj = self.player.pos
49 | self.board[pi, pj] = 3 # 3 means blocked
50 | self.player.pos = string2pos(pos)
51 | pi, pj = self.player.pos
52 | self.board[pi, pj] = self.current_player # place player on board
53 |
54 | def ttentry(self):
55 | e = [tuple(row) for row in self.board]
56 | e.append(pos2string(self.players[0].pos))
57 | e.append(pos2string(self.players[1].pos))
58 | return tuple(e)
59 |
60 | def ttrestore(self, entry):
61 | for x, row in enumerate(entry[: self.board_size[0]]):
62 | for y, n in enumerate(row):
63 | self.board[x, y] = n
64 | self.players[0].pos = string2pos(entry[-2])
65 | self.players[1].pos = string2pos(entry[-1])
66 |
67 | def show(self):
68 | print(
69 | "\n"
70 | + "\n".join(
71 | [" 1 2 3 4 5 6 7 8"]
72 | + [
73 | "ABCDEFGH"[k]
74 | + " "
75 | + " ".join(
76 | [
77 | [".", "1", "2", "X"][self.board[k, i]]
78 | for i in range(self.board_size[0])
79 | ]
80 | )
81 | for k in range(self.board_size[1])
82 | ]
83 | + [""]
84 | )
85 | )
86 |
87 | def lose(self):
88 | return self.possible_moves() == []
89 |
90 | def scoring(self):
91 | return -100 if (self.possible_moves() == []) else 0
92 |
93 | def is_over(self):
94 | return self.lose()
95 |
96 |
97 | if __name__ == "__main__":
98 | from easyAI import AI_Player, Negamax
99 |
100 | ai_algo = Negamax(11)
101 | game = Knights([AI_Player(ai_algo), AI_Player(ai_algo)], (5, 5))
102 | game.play()
103 | print("player %d loses" % (game.current_player))
104 |
--------------------------------------------------------------------------------
/easyAI/AI/MTdriver.py:
--------------------------------------------------------------------------------
1 | # contributed by mrfesol (Tomasz Wesolowski)
2 |
3 | inf = 1000000
4 | eps = 0.001
5 |
6 |
7 | def mt(game, gamma, depth, origDepth, scoring, tt=None):
8 | """
9 | This implements Memory-Enhanced Test with transposition tables.
10 | This method is not meant to be used directly.
11 | This implementation is inspired by paper:
12 | http://arxiv.org/ftp/arxiv/papers/1404/1404.1515.pdf
13 | """
14 |
15 | # Is there a transposition table and is this game in it ?
16 | lookup = None if (tt is None) else tt.lookup(game)
17 | possible_moves = None
18 | lowerbound, upperbound = -inf, inf
19 | best_move = None
20 |
21 | if (lookup is not None) and lookup["depth"] >= depth:
22 | # The game has been visited in the past
23 | lowerbound, upperbound = lookup["lowerbound"], lookup["upperbound"]
24 | if lowerbound > gamma:
25 | if depth == origDepth:
26 | game.ai_move = lookup["move"]
27 | return lowerbound
28 | if upperbound < gamma:
29 | if depth == origDepth:
30 | game.ai_move = lookup["move"]
31 | return upperbound
32 |
33 | best_value = -inf
34 |
35 | if (depth == 0) or game.is_over():
36 | score = game.scoring()
37 |
38 | if score != 0:
39 | score = score - 0.99 * depth * abs(score) / score
40 |
41 | lowerbound = upperbound = best_value = score
42 | else:
43 | ngame = game
44 | unmake_move = hasattr(game, "unmake_move")
45 | possible_moves = game.possible_moves()
46 | best_move = possible_moves[0]
47 |
48 | if not hasattr(game, "ai_move"):
49 | game.ai_move = best_move
50 |
51 | for move in possible_moves:
52 | if best_value >= gamma:
53 | break
54 |
55 | if not unmake_move:
56 | ngame = game.copy()
57 |
58 | ngame.make_move(move)
59 | ngame.switch_player()
60 |
61 | move_value = -mt(ngame, -gamma, depth - 1, origDepth, scoring, tt)
62 | if best_value < move_value:
63 | best_value = move_value
64 | best_move = move
65 |
66 | if unmake_move:
67 | ngame.switch_player()
68 | ngame.unmake_move(move)
69 |
70 | if best_value < gamma:
71 | upperbound = best_value
72 | else:
73 | if depth == origDepth:
74 | game.ai_move = best_move
75 | lowerbound = best_value
76 |
77 | if tt is not None:
78 |
79 | if depth > 0 and not game.is_over():
80 | assert best_move in possible_moves
81 | tt.store(
82 | game=game,
83 | lowerbound=lowerbound,
84 | upperbound=upperbound,
85 | depth=depth,
86 | move=best_move,
87 | )
88 |
89 | return best_value
90 |
91 |
92 | def mtd(game, first, next, depth, scoring, tt=None):
93 | """
94 | This implements Memory-Enhanced Test Driver.
95 | This method is not meant to be used directly.
96 | It's used by several algorithms from MT family, i.e see ``easyAI.SSS``
97 | For more details read following paper:
98 | http://arxiv.org/ftp/arxiv/papers/1404/1404.1515.pdf
99 | """
100 | bound, best_value = first, first
101 | lowerbound, upperbound = -inf, inf
102 | while True:
103 | bound = next(lowerbound, upperbound, best_value)
104 | best_value = mt(game, bound - eps, depth, depth, scoring, tt)
105 | if best_value < bound:
106 | upperbound = best_value
107 | else:
108 | lowerbound = best_value
109 | if lowerbound == upperbound:
110 | break
111 | return best_value
--------------------------------------------------------------------------------
/easyAI/games/Awele.py:
--------------------------------------------------------------------------------
1 | try:
2 | import numpy as np
3 | except ImportError:
4 | print("Sorry, this example requires Numpy installed !")
5 | raise
6 |
7 | from easyAI import TwoPlayerGame
8 |
9 |
10 | class Awele(TwoPlayerGame):
11 | """
12 | Rules are as defined as in http://en.wikipedia.org/wiki/Oware
13 | with the additional rule that the game ends when then are 6 seeds
14 | left in the game.
15 | """
16 |
17 | def __init__(self, players):
18 | for i, player in enumerate(players):
19 | player.score = 0
20 | player.isstarved = False
21 | player.camp = i
22 | self.players = players
23 |
24 | # Initial configuration of the board.
25 | # holes are indexed by a,b,c,d...
26 | self.board = [4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4]
27 |
28 | self.current_player = 1 # player 1 starts.
29 |
30 | def make_move(self, move):
31 | if move == "None":
32 | self.player.isstarved = True
33 | s = 6 * self.opponent.camp
34 | self.player.score += sum(self.board[s : s + 6])
35 | return
36 |
37 | move = "abcdefghijkl".index(move)
38 |
39 | pos = move
40 | for i in range(self.board[move]): # DEAL
41 | pos = (pos + 1) % 12
42 | if pos == move:
43 | pos = (pos + 1) % 12
44 | self.board[pos] += 1
45 |
46 | self.board[move] = 0
47 |
48 | while (pos / 6) == self.opponent.camp and (self.board[pos] in [2, 3]): # TAKE
49 | self.player.score += self.board[pos]
50 | self.board[pos] = 0
51 | pos = (pos - 1) % 12
52 |
53 | def possible_moves(self):
54 | """
55 | A player must play any hole that contains enough seeds to
56 | 'feed' the opponent. This no hole has this many seeds, any
57 | non-empty hole can be played.
58 | """
59 |
60 | if self.current_player == 1:
61 | if max(self.board[:6]) == 0:
62 | return ["None"]
63 | moves = [i for i in range(6) if (self.board[i] >= 6 - i)]
64 | if moves == []:
65 | moves = [i for i in range(6) if self.board[i] != 0]
66 | else:
67 | if max(self.board[6:]) == 0:
68 | return ["None"]
69 | moves = [i for i in range(6, 12) if (self.board[i] >= 12 - i)]
70 | if moves == []:
71 | moves = [i for i in range(6, 12) if self.board[i] != 0]
72 |
73 | return ["abcdefghijkl"[u] for u in moves]
74 |
75 | def show(self):
76 | """ Prints the board, with the hole's respective letters """
77 |
78 | print("Score: %d / %d" % tuple(p.score for p in self.players))
79 | print(" ".join("lkjihg"))
80 | print(" ".join(["%02d" % i for i in self.board[-1:-7:-1]]))
81 | print(" ".join(["%02d" % i for i in self.board[:6]]))
82 | print(" ".join("abcdef"))
83 |
84 | def lose(self):
85 | return self.opponent.score > 24
86 |
87 | def is_over(self):
88 | return self.lose() or sum(self.board) < 7 or self.opponent.isstarved
89 |
90 |
91 | if __name__ == "__main__":
92 | # In what follows we setup the AI and launch a AI-vs-AI match.
93 |
94 | from easyAI import Human_Player, AI_Player, Negamax
95 |
96 | # this shows that the scoring can be defined in the AI algo,
97 | # which enables 2 AIs with different scorings to play a match.
98 | scoring = lambda game: game.player.score - game.opponent.score
99 | ai = Negamax(6, scoring)
100 | game = Awele([AI_Player(ai), AI_Player(ai)])
101 |
102 | game.play()
103 |
104 | if game.player.score > game.opponent.score:
105 | print("Player %d wins." % game.current_player)
106 | elif game.player.score < game.opponent.score:
107 | print("Player %d wins." % game.opponent_index)
108 | else:
109 | print("Looks like we have a draw.")
110 |
--------------------------------------------------------------------------------
/easyAI/games/ThreeMusketeers.py:
--------------------------------------------------------------------------------
1 | import numpy as np
2 | from easyAI import TwoPlayerGame
3 |
4 | MOVES = np.zeros((30, 4), dtype=int)
5 |
6 |
7 | class ThreeMusketeers(TwoPlayerGame):
8 | """
9 | rules: http://en.wikipedia.org/wiki/Three_Musketeers_%28game%29
10 | """
11 |
12 | def __init__(self, players):
13 | self.players = players
14 | self.board = np.array(
15 | [
16 | [2, 2, 2, 2, 1],
17 | [2, 2, 2, 2, 2],
18 | [2, 2, 1, 2, 2],
19 | [2, 2, 2, 2, 2],
20 | [1, 2, 2, 2, 2],
21 | ]
22 | )
23 | self.musketeers = [(0, 4), (2, 2), (4, 0)]
24 | self.current_player = 1
25 |
26 | def possible_moves(self):
27 | moves = []
28 | if self.current_player == 2:
29 | for i in range(5):
30 | for j in range(5):
31 | if self.board[i, j] == 0:
32 | moves += [
33 | [k, l, i, j]
34 | for k, l in [(i + 1, j), (i, j + 1), (i - 1, j), (i, j - 1)]
35 | if 0 <= k < 5 and 0 <= l < 5 and self.board[k, l] == 2
36 | ]
37 | else:
38 | for i, j in self.musketeers:
39 | moves += [
40 | [i, j, k, l]
41 | for k, l in [(i + 1, j), (i, j + 1), (i - 1, j), (i, j - 1)]
42 | if (0 <= k < 5) and (0 <= l < 5) and self.board[k, l] == 2
43 | ]
44 |
45 | if moves == []:
46 | moves = ["None"]
47 |
48 | return moves
49 |
50 | def make_move(self, move):
51 | """ move = [y1, x1, y2, x2] """
52 |
53 | if move == "None":
54 | return
55 |
56 | self.board[move[0], move[1]] = 0
57 | self.board[move[2], move[3]] = self.current_player
58 | if self.current_player == 1:
59 | self.musketeers.remove((move[0], move[1]))
60 | self.musketeers.append((move[2], move[3]))
61 |
62 | def unmake_move(self, move):
63 |
64 | if move == "None":
65 | return
66 |
67 | self.board[move[0], move[1]] = self.current_player
68 | self.board[move[2], move[3]] = 0
69 | if self.current_player == 1:
70 | self.board[move[2], move[3]] = 2
71 | self.musketeers.remove((move[2], move[3]))
72 | self.musketeers.append((move[0], move[1]))
73 |
74 | def win(self):
75 | a, b, c = self.musketeers
76 | aligned = (a[0] == b[0] and b[0] == c[0]) or (a[1] == b[1] and b[1] == c[1])
77 | if self.current_player == 1:
78 | return not (aligned) and (self.possible_moves() == ["None"])
79 | else:
80 | return aligned
81 |
82 | def is_over(self):
83 | self.haswon = self.win()
84 | return self.haswon
85 |
86 | def scoring(self):
87 | if self.haswon is not None:
88 | haswon = self.haswon
89 | self.haswon = None
90 | return 100 if haswon else 0
91 | return 100 if self.win() else 0
92 |
93 | def show(self):
94 | print(
95 | "\n"
96 | + "\n".join(
97 | ["--1-2-3-4-5"]
98 | + [
99 | "ABCDE"[j]
100 | + " "
101 | + " ".join([".12"[self.board[j, i]] for i in range(5)])
102 | for j in range(5)
103 | ]
104 | )
105 | )
106 |
107 | def ttentry(self):
108 | return "".join(map(str, (self.current_player,) + tuple(self.board.flatten())))
109 |
110 |
111 | if __name__ == "__main__":
112 |
113 | # In what follows we setup the AI and launch a AI-vs-AI match.
114 |
115 | from easyAI import Human_Player, AI_Player, Negamax
116 | from easyAI.AI import TranspositionTable
117 |
118 | tt = TranspositionTable()
119 | ai = Negamax(5, tt=tt)
120 | players = [AI_Player(ai) for i in [0, 1]]
121 | game = ThreeMusketeers(players)
122 | game.play()
123 | print("player %d wins after %d turns " % (game.opponent_index, game.nmove))
124 |
--------------------------------------------------------------------------------
/easyAI/games/TicTacToe-Flask.py:
--------------------------------------------------------------------------------
1 | from easyAI import TwoPlayerGame, Human_Player, AI_Player, Negamax
2 | from flask import Flask, render_template_string, request, make_response
3 |
4 |
5 | class TicTacToe(TwoPlayerGame):
6 | """The board positions are numbered as follows:
7 | 1 2 3
8 | 4 5 6
9 | 7 8 9
10 | """
11 |
12 | def __init__(self, players):
13 | self.players = players
14 | self.board = [0 for i in range(9)]
15 | self.current_player = 1 # player 1 starts.
16 |
17 | def possible_moves(self):
18 | return [i + 1 for i, e in enumerate(self.board) if e == 0]
19 |
20 | def make_move(self, move):
21 | self.board[int(move) - 1] = self.current_player
22 |
23 | def unmake_move(self, move): # optional method (speeds up the AI)
24 | self.board[int(move) - 1] = 0
25 |
26 | WIN_LINES = [
27 | [1, 2, 3],
28 | [4, 5, 6],
29 | [7, 8, 9], # horiz.
30 | [1, 4, 7],
31 | [2, 5, 8],
32 | [3, 6, 9], # vertical
33 | [1, 5, 9],
34 | [3, 5, 7], # diagonal
35 | ]
36 |
37 | def lose(self, who=None):
38 | """ Has the opponent "three in line ?" """
39 | if who is None:
40 | who = self.opponent_index
41 | wins = [
42 | all([(self.board[c - 1] == who) for c in line]) for line in self.WIN_LINES
43 | ]
44 | return any(wins)
45 |
46 | def is_over(self):
47 | return (
48 | (self.possible_moves() == [])
49 | or self.lose()
50 | or self.lose(who=self.current_player)
51 | )
52 |
53 | def show(self):
54 | print(
55 | "\n"
56 | + "\n".join(
57 | [
58 | " ".join([[".", "O", "X"][self.board[3 * j + i]] for i in range(3)])
59 | for j in range(3)
60 | ]
61 | )
62 | )
63 |
64 | def spot_string(self, i, j):
65 | return ["_", "O", "X"][self.board[3 * j + i]]
66 |
67 | def scoring(self):
68 | opp_won = self.lose()
69 | i_won = self.lose(who=self.current_player)
70 | if opp_won and not i_won:
71 | return -100
72 | if i_won and not opp_won:
73 | return 100
74 | return 0
75 |
76 | def winner(self):
77 | if self.lose(who=2):
78 | return "AI Wins"
79 | return "Tie"
80 |
81 |
82 | TEXT = """
83 |
84 |
85 | Tic Tac Toe
86 |
87 |
Tic Tac Toe
88 |
{{msg}}
89 |
106 |
107 |
108 | """
109 |
110 | app = Flask(__name__)
111 | ai_algo = Negamax(6)
112 |
113 |
114 | @app.route("/", methods=["GET", "POST"])
115 | def play_game():
116 | ttt = TicTacToe([Human_Player(), AI_Player(ai_algo)])
117 | game_cookie = request.cookies.get("game_board")
118 | if game_cookie:
119 | ttt.board = [int(x) for x in game_cookie.split(",")]
120 | if "choice" in request.form:
121 | ttt.play_move(request.form["choice"])
122 | if not ttt.is_over():
123 | ai_move = ttt.get_move()
124 | ttt.play_move(ai_move)
125 | if "reset" in request.form:
126 | ttt.board = [0 for i in range(9)]
127 | if ttt.is_over():
128 | msg = ttt.winner()
129 | else:
130 | msg = "play move"
131 | resp = make_response(render_template_string(TEXT, ttt=ttt, msg=msg))
132 | c = ",".join(map(str, ttt.board))
133 | resp.set_cookie("game_board", c)
134 | return resp
135 |
136 |
137 | if __name__ == "__main__":
138 | app.run()
139 |
--------------------------------------------------------------------------------
/easyAI/AI/TranspositionTable.py:
--------------------------------------------------------------------------------
1 | """
2 | This module implements transposition tables, which store positions
3 | and moves to speed up the AI.
4 | """
5 |
6 | import pickle
7 | import json
8 | from ast import literal_eval as make_tuple
9 |
10 |
11 | class TranspositionTable:
12 | """
13 | A tranposition table made out of a Python dictionnary.
14 |
15 | It creates a "cache" of already resolved moves that can, under
16 | some circumstances, let the algorithm run faster.
17 |
18 | This table can be stored to file, allowing games to be stopped
19 | and restarted at a later time. Or, if the game is fully solved,
20 | the cache can return the correct moves nearly instantly because
21 | the AI alogorithm no longer has to compute correct moves.
22 |
23 | Transposition tables can only be used on games which have a method
24 | game.ttentry() -> string or tuple
25 |
26 | To save the table as a `pickle` file, use the **to_file** and **from_file**
27 | methods. A pickle file is binary and usually faster. A pickle file
28 | can also be appended to with new cached data. See python's pickle
29 | documentation for secuirty issues.
30 |
31 | To save the table as a universal JSON file, use the **to_json_file**
32 | and **from_json_file** methods. For these methods, you must explicity
33 | pass **use_tuples=True** if game.ttentry() returns tuples rather than
34 | strings.
35 |
36 | Usage:
37 |
38 | >>> table = TranspositionTable()
39 | >>> ai = Negamax(8, scoring, tt = table)
40 | >>> ai(some_game) # computes a move, fills the table
41 | >>> table.to_file('saved_tt.data') # maybe save for later ?
42 |
43 | >>> # later (or in a different program)...
44 | >>> table = TranspositionTable().from_file('saved_tt.data')
45 | >>> ai = Negamax(8, scoring, tt = table)
46 |
47 | Transposition tables can also be used as an AI (``AI_player(tt)``)
48 | but they must be exhaustive in this case: if they are asked for
49 | a position that isn't stored in the table, it will lead to an error.
50 |
51 | """
52 |
53 | def __init__(self, own_dict=None):
54 | self.d = own_dict if own_dict is not None else dict()
55 |
56 | def lookup(self, game):
57 | """Requests the entry in the table. Returns None if the
58 | entry has not been previously stored in the table."""
59 | return self.d.get(game.ttentry(), None)
60 |
61 | def __call__(self, game):
62 | """
63 | This method enables the transposition table to be used
64 | like an AI algorithm. However it will just break if it falls
65 | on some game state that is not in the table. Therefore it is a
66 | better option to use a mixed algorithm like
67 |
68 | >>> # negamax boosted with a transposition table !
69 | >>> Negamax(10, tt= my_dictTranspositionTable)
70 | """
71 | return self.d[game.ttentry()]["move"]
72 |
73 | def store(self, **data):
74 | """ Stores an entry into the table """
75 | entry = data.pop("game").ttentry()
76 | self.d[entry] = data
77 |
78 | def to_file(self, filename):
79 | """Saves the transposition table to a file. Warning: the file
80 | can be big (~100Mo)."""
81 | with open(filename, "wb") as f:
82 | pickle.dump(self, f)
83 |
84 | def from_file(self, filename):
85 | """Loads a transposition table previously saved with
86 | ``TranspositionTable.to_file``"""
87 | with open(filename, "rb") as h:
88 | self.__dict__.update(pickle.load(h).__dict__)
89 |
90 | def to_json_file(self, filename, use_tuples=False):
91 | """Saves the transposition table to a serial JSON file. Warning: the file
92 | can be big (~100Mo)."""
93 | if use_tuples:
94 | with open(filename, "w") as f:
95 | k = self.d.keys()
96 | v = self.d.values()
97 | k1 = [str(i) for i in k]
98 | json.dump(dict(zip(*[k1, v])), f, ensure_ascii=False)
99 | else:
100 | with open(filename, "w") as f:
101 | json.dump(self.d, f, ensure_ascii=False)
102 |
103 | def from_json_file(self, filename, use_tuples=False):
104 | """Loads a transposition table previously saved with
105 | ``TranspositionTable.to_json_file``"""
106 | with open(filename, "r") as f:
107 | data = json.load(f)
108 | if use_tuples:
109 | k = data.keys()
110 | v = data.values()
111 | k1 = [make_tuple(i) for i in k]
112 | self.d = dict(zip(*[k1, v]))
113 | else:
114 | self.d = data
115 |
--------------------------------------------------------------------------------
/docs/source/_themes/flask_theme_support.py:
--------------------------------------------------------------------------------
1 | # flasky extensions. flasky pygments style based on tango style
2 | from pygments.style import Style
3 | from pygments.token import Keyword, Name, Comment, String, Error, \
4 | Number, Operator, Generic, Whitespace, Punctuation, Other, Literal
5 |
6 |
7 | class FlaskyStyle(Style):
8 | background_color = "#f8f8f8"
9 | default_style = ""
10 |
11 | styles = {
12 | # No corresponding class for the following:
13 | #Text: "", # class: ''
14 | Whitespace: "underline #f8f8f8", # class: 'w'
15 | Error: "#a40000 border:#ef2929", # class: 'err'
16 | Other: "#000000", # class 'x'
17 |
18 | Comment: "italic #8f5902", # class: 'c'
19 | Comment.Preproc: "noitalic", # class: 'cp'
20 |
21 | Keyword: "bold #004461", # class: 'k'
22 | Keyword.Constant: "bold #004461", # class: 'kc'
23 | Keyword.Declaration: "bold #004461", # class: 'kd'
24 | Keyword.Namespace: "bold #004461", # class: 'kn'
25 | Keyword.Pseudo: "bold #004461", # class: 'kp'
26 | Keyword.Reserved: "bold #004461", # class: 'kr'
27 | Keyword.Type: "bold #004461", # class: 'kt'
28 |
29 | Operator: "#582800", # class: 'o'
30 | Operator.Word: "bold #004461", # class: 'ow' - like keywords
31 |
32 | Punctuation: "bold #000000", # class: 'p'
33 |
34 | # because special names such as Name.Class, Name.Function, etc.
35 | # are not recognized as such later in the parsing, we choose them
36 | # to look the same as ordinary variables.
37 | Name: "#000000", # class: 'n'
38 | Name.Attribute: "#c4a000", # class: 'na' - to be revised
39 | Name.Builtin: "#004461", # class: 'nb'
40 | Name.Builtin.Pseudo: "#3465a4", # class: 'bp'
41 | Name.Class: "#000000", # class: 'nc' - to be revised
42 | Name.Constant: "#000000", # class: 'no' - to be revised
43 | Name.Decorator: "#888", # class: 'nd' - to be revised
44 | Name.Entity: "#ce5c00", # class: 'ni'
45 | Name.Exception: "bold #cc0000", # class: 'ne'
46 | Name.Function: "#000000", # class: 'nf'
47 | Name.Property: "#000000", # class: 'py'
48 | Name.Label: "#f57900", # class: 'nl'
49 | Name.Namespace: "#000000", # class: 'nn' - to be revised
50 | Name.Other: "#000000", # class: 'nx'
51 | Name.Tag: "bold #004461", # class: 'nt' - like a keyword
52 | Name.Variable: "#000000", # class: 'nv' - to be revised
53 | Name.Variable.Class: "#000000", # class: 'vc' - to be revised
54 | Name.Variable.Global: "#000000", # class: 'vg' - to be revised
55 | Name.Variable.Instance: "#000000", # class: 'vi' - to be revised
56 |
57 | Number: "#990000", # class: 'm'
58 |
59 | Literal: "#000000", # class: 'l'
60 | Literal.Date: "#000000", # class: 'ld'
61 |
62 | String: "#4e9a06", # class: 's'
63 | String.Backtick: "#4e9a06", # class: 'sb'
64 | String.Char: "#4e9a06", # class: 'sc'
65 | String.Doc: "italic #8f5902", # class: 'sd' - like a comment
66 | String.Double: "#4e9a06", # class: 's2'
67 | String.Escape: "#4e9a06", # class: 'se'
68 | String.Heredoc: "#4e9a06", # class: 'sh'
69 | String.Interpol: "#4e9a06", # class: 'si'
70 | String.Other: "#4e9a06", # class: 'sx'
71 | String.Regex: "#4e9a06", # class: 'sr'
72 | String.Single: "#4e9a06", # class: 's1'
73 | String.Symbol: "#4e9a06", # class: 'ss'
74 |
75 | Generic: "#000000", # class: 'g'
76 | Generic.Deleted: "#a40000", # class: 'gd'
77 | Generic.Emph: "italic #000000", # class: 'ge'
78 | Generic.Error: "#ef2929", # class: 'gr'
79 | Generic.Heading: "bold #000080", # class: 'gh'
80 | Generic.Inserted: "#00A000", # class: 'gi'
81 | Generic.Output: "#888", # class: 'go'
82 | Generic.Prompt: "#745334", # class: 'gp'
83 | Generic.Strong: "bold #000000", # class: 'gs'
84 | Generic.Subheading: "bold #800080", # class: 'gu'
85 | Generic.Traceback: "bold #a40000", # class: 'gt'
86 | }
87 |
--------------------------------------------------------------------------------
/README.rst:
--------------------------------------------------------------------------------
1 | easyAI
2 | ======
3 |
4 | EasyAI (full documentation here_) is a pure-Python artificial intelligence framework for two-players abstract games such as Tic Tac Toe, Connect 4, Reversi, etc.
5 | It makes it easy to define the mechanisms of a game, and play against the computer or solve the game.
6 | Under the hood, the AI is a Negamax algorithm with alpha-beta pruning and transposition tables as described on Wikipedia_.
7 |
8 |
9 | Installation
10 | ------------
11 |
12 | If you have ``pip`` installed, type this in a terminal ::
13 |
14 | sudo pip install easyAI
15 |
16 | Otherwise, download the source code (for instance on Github_), unzip everything into one folder and in this folder, in a terminal, type ::
17 |
18 | sudo python setup.py install
19 |
20 | Additionally you will need to install Numpy to be able to run some of the examples.
21 |
22 |
23 | A quick example
24 | ----------------
25 |
26 | Let us define the rules of a game and start a match against the AI:
27 |
28 | .. code:: python
29 |
30 | from easyAI import TwoPlayerGame, Human_Player, AI_Player, Negamax
31 |
32 | class GameOfBones( TwoPlayerGame ):
33 | """ In turn, the players remove one, two or three bones from a
34 | pile of bones. The player who removes the last bone loses. """
35 |
36 | def __init__(self, players=None):
37 | self.players = players
38 | self.pile = 20 # start with 20 bones in the pile
39 | self.current_player = 1 # player 1 starts
40 |
41 | def possible_moves(self): return ['1','2','3']
42 | def make_move(self,move): self.pile -= int(move) # remove bones.
43 | def win(self): return self.pile<=0 # opponent took the last bone ?
44 | def is_over(self): return self.win() # Game stops when someone wins.
45 | def show(self): print ("%d bones left in the pile" % self.pile)
46 | def scoring(self): return 100 if game.win() else 0 # For the AI
47 |
48 | # Start a match (and store the history of moves when it ends)
49 | ai = Negamax(13) # The AI will think 13 moves in advance
50 | game = GameOfBones( [ Human_Player(), AI_Player(ai) ] )
51 | history = game.play()
52 |
53 | Result: ::
54 |
55 | 20 bones left in the pile
56 |
57 | Player 1 what do you play ? 3
58 |
59 | Move #1: player 1 plays 3 :
60 | 17 bones left in the pile
61 |
62 | Move #2: player 2 plays 1 :
63 | 16 bones left in the pile
64 |
65 | Player 1 what do you play ?
66 |
67 | Solving the game
68 | *****************
69 |
70 | Let us now solve the game:
71 |
72 | .. code:: python
73 |
74 | from easyAI import solve_with_iterative_deepening
75 | r,d,m = solve_with_iterative_deepening(
76 | game=GameOfBones(),
77 | ai_depths=range(2,20),
78 | win_score=100
79 | )
80 |
81 | We obtain ``r=1``, meaning that if both players play perfectly, the first player to play can always win (-1 would have meant always lose), ``d=10``, which means that the wins will be in ten moves (i.e. 5 moves per player) or less, and ``m='3'``, which indicates that the first player's first move should be ``'3'``.
82 |
83 | These computations can be speed up using a transposition table which will store the situations encountered and the best moves for each:
84 |
85 | .. code:: python
86 |
87 | tt = TranspositionTable()
88 | GameOfBones.ttentry = lambda game : game.pile # key for the table
89 | r,d,m = solve_with_iterative_deepening(
90 | game=GameOfBones(),
91 | ai_depths=range(2,20),
92 | win_score=100,
93 | tt=tt
94 | )
95 |
96 | After these lines are run the variable ``tt`` contains a transposition table storing the possible situations (here, the possible sizes of the pile) and the optimal moves to perform. With ``tt`` you can play perfectly without *thinking*:
97 |
98 | .. code:: python
99 |
100 | game = GameOfBones( [ AI_Player( tt ), Human_Player() ] )
101 | game.play() # you will always lose this game :)
102 |
103 |
104 | Contribute !
105 | ------------
106 |
107 | EasyAI is an open source software originally written by Zulko_ and released under the MIT licence. Contributions welcome! Some ideas: AI algos for incomplete information games, better game solving strategies, (efficient) use of databases to store moves, AI algorithms using parallelisation.
108 |
109 | For troubleshooting and bug reports, the best for now is to ask on Github_.
110 |
111 | How releases work
112 | *****************
113 |
114 | Every time a MR gets merged into master, an automatic release happens:
115 |
116 | - If the last commit's message starts with `[FEATURE]`, a feature release happens (`1.3.3 -> 1.4.0`)
117 | - If the last commit's message starts with `[MAJOR]`, a major release happens (`1.3.3 -> 2.0.0`)
118 | - If the last commit's message starts with `[SKIP]`, no release happens.
119 | - Otherwise, a patch release happens (`1.3.3 -> 1.3.4`)
120 |
121 |
122 |
123 | Maintainers
124 | -----------
125 |
126 | - Zulko_ (owner)
127 | - JohnAD_
128 |
129 |
130 | .. _here: http://zulko.github.io/easyAI
131 | .. _Wikipedia: http://en.wikipedia.org/wiki/Negamax
132 | .. _Zulko : https://github.com/Zulko
133 | .. _JohnAD : https://github.com/JohnAD
134 | .. _Github : https://github.com/Zulko/easyAI
135 |
--------------------------------------------------------------------------------
/easyAI/games/AweleTactical.py:
--------------------------------------------------------------------------------
1 | try:
2 | import numpy as np
3 | except ImportError:
4 | print("Sorry, this example requires Numpy installed !")
5 | raise
6 |
7 | from easyAI import TwoPlayerGame
8 |
9 | PLAYER1 = 1
10 | PLAYER2 = 2
11 |
12 | HOLES = {PLAYER1: [0, 1, 2, 3, 4, 5], PLAYER2: [6, 7, 8, 9, 10, 11]}
13 | POS_FACTOR = [4, 5, 6, 7, 8, 9, 4, 5, 6, 7, 8, 9]
14 |
15 |
16 | class AweleTactical(TwoPlayerGame):
17 | """
18 | Rules are as defined as in http://en.wikipedia.org/wiki/Oware
19 | with the additional rule that the game ends when then are 6 seeds
20 | left in the game.
21 | """
22 |
23 | def __init__(self, players):
24 | for i, player in enumerate(players):
25 | player.score = 0
26 | player.isstarved = False
27 | player.camp = i
28 | self.players = players
29 |
30 | # Initial configuration of the board.
31 | # holes are indexed by a,b,c,d...
32 | self.board = [4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4]
33 |
34 | self.current_player = 1 # player 1 starts.
35 |
36 | def make_move(self, move):
37 | if move == "None":
38 | self.player.isstarved = True
39 | s = 6 * self.opponent.camp
40 | self.player.score += sum(self.board[s : s + 6])
41 | return
42 |
43 | move = "abcdefghijkl".index(move)
44 |
45 | pos = move
46 | for i in range(self.board[move]): # DEAL
47 | pos = (pos + 1) % 12
48 | if pos == move:
49 | pos = (pos + 1) % 12
50 | self.board[pos] += 1
51 |
52 | self.board[move] = 0
53 |
54 | while (pos / 6) == self.opponent.camp and (self.board[pos] in [2, 3]): # TAKE
55 | self.player.score += self.board[pos]
56 | self.board[pos] = 0
57 | pos = (pos - 1) % 12
58 |
59 | def possible_moves(self):
60 | """
61 | A player must play any hole that contains enough seeds to
62 | 'feed' the opponent. This no hole has this many seeds, any
63 | non-empty hole can be played.
64 | """
65 |
66 | if self.current_player == 1:
67 | if max(self.board[:6]) == 0:
68 | return ["None"]
69 | moves = [i for i in range(6) if (self.board[i] >= 6 - i)]
70 | if moves == []:
71 | moves = [i for i in range(6) if self.board[i] != 0]
72 | else:
73 | if max(self.board[6:]) == 0:
74 | return ["None"]
75 | moves = [i for i in range(6, 12) if (self.board[i] >= 12 - i)]
76 | if moves == []:
77 | moves = [i for i in range(6, 12) if self.board[i] != 0]
78 |
79 | return ["abcdefghijkl"[u] for u in moves]
80 |
81 | def show(self):
82 | """ Prints the board, with the hole's respective letters """
83 |
84 | print("Score: %d / %d" % tuple(p.score for p in self.players))
85 | print(" ".join("lkjihg"))
86 | print(" ".join(["%02d" % i for i in self.board[-1:-7:-1]]))
87 | print(" ".join(["%02d" % i for i in self.board[:6]]))
88 | print(" ".join("abcdef"))
89 |
90 | def ttentry(self):
91 | return tuple(self.board + [self.players[0].score] + [self.players[1].score])
92 |
93 | def ttrestore(self, entry):
94 | for i in range(len(self.board)):
95 | self.board[i] = entry[i]
96 | self.players[0].score = entry[-2]
97 | self.players[1].score = entry[-1]
98 |
99 | def scoring(self):
100 | strategic_score = (self.player.score - self.opponent.score) * 100
101 | tactical_score = 0
102 | for hole in HOLES[self.current_player]:
103 | qty = self.board[hole]
104 | if qty == 0:
105 | tactical_score -= 7 + POS_FACTOR[hole]
106 | elif qty == 1:
107 | tactical_score -= 11 + POS_FACTOR[hole]
108 | elif qty == 2:
109 | tactical_score -= 13 + POS_FACTOR[hole]
110 | for hole in HOLES[self.opponent_index]:
111 | qty = self.board[hole]
112 | if qty == 0:
113 | tactical_score += 7 + POS_FACTOR[hole]
114 | elif qty == 1:
115 | tactical_score += 11 + POS_FACTOR[hole]
116 | elif qty == 2:
117 | tactical_score += 13 + POS_FACTOR[hole]
118 | return strategic_score + tactical_score
119 |
120 | def lose(self):
121 | return self.opponent.score > 24
122 |
123 | def is_over(self):
124 | return self.lose() or sum(self.board) < 7 or self.opponent.isstarved
125 |
126 |
127 | if __name__ == "__main__":
128 | # In what follows we setup the AI and launch a AI-vs-AI match.
129 |
130 | from easyAI import Human_Player, AI_Player, Negamax
131 |
132 | # this shows that the scoring can be defined in the AI algo,
133 | # which enables 2 AIs with different scorings to play a match.
134 | scoring = lambda game: game.player.score - game.opponent.score
135 | ai = Negamax(6, scoring)
136 | game = AweleTactical([AI_Player(ai), AI_Player(ai)])
137 |
138 | game.play()
139 |
140 | if game.player.score > game.opponent.score:
141 | print("Player %d wins." % game.current_player)
142 | elif game.player.score < game.opponent.score:
143 | print("Player %d wins." % game.opponent_index)
144 | else:
145 | print("Looks like we have a draw.")
146 |
--------------------------------------------------------------------------------
/easyAI/games/Reversi.py:
--------------------------------------------------------------------------------
1 | """
2 | The game of Reversi. Warning: this game is not coded in an optimal
3 | way, the AI will be slow.
4 | """
5 |
6 | import numpy as np
7 | from easyAI import TwoPlayerGame
8 |
9 | to_string = lambda a: "ABCDEFGH"[a[0]] + str(a[1] + 1)
10 | to_array = lambda s: np.array(["ABCDEFGH".index(s[0]), int(s[1]) - 1])
11 |
12 |
13 | class Reversi(TwoPlayerGame):
14 | """
15 | See the rules on http://en.wikipedia.org/wiki/Reversi
16 | Here for simplicity we suppose that the game ends when a
17 | player cannot play, but it would take just a few more lines to
18 | implement the real ending rules, by which the game ends when both
19 | players can't play.
20 |
21 | This implementation will make a slow and dumbe AI and could be sped
22 | up by adding a way of unmaking moves (method unmake_moves) and
23 | coding some parts in C (this is left as an exercise :) )
24 | """
25 |
26 | def __init__(self, players, board=None):
27 | self.players = players
28 | self.board = np.zeros((8, 8), dtype=int)
29 | self.board[3, [3, 4]] = [1, 2]
30 | self.board[4, [3, 4]] = [2, 1]
31 | self.current_player = 1
32 |
33 | def possible_moves(self):
34 | """ Only moves that lead to flipped pieces are allowed """
35 | return [
36 | to_string((i, j))
37 | for i in range(8)
38 | for j in range(8)
39 | if (self.board[i, j] == 0)
40 | and (pieces_flipped(self.board, (i, j), self.current_player) != [])
41 | ]
42 |
43 | def make_move(self, pos):
44 | """Put the piece at position ``pos`` and flip the pieces that
45 | much be flipped"""
46 | pos = to_array(pos)
47 | flipped = pieces_flipped(self.board, pos, self.current_player)
48 | for i, j in flipped:
49 | self.board[i, j] = self.current_player
50 | self.board[pos[0], pos[1]] = self.current_player
51 |
52 | def show(self):
53 | """ Prints the board in a fancy (?) way """
54 | print(
55 | "\n"
56 | + "\n".join(
57 | [" 1 2 3 4 5 6 7 8"]
58 | + [
59 | "ABCDEFGH"[k]
60 | + " "
61 | + " ".join(
62 | [[".", "1", "2", "X"][self.board[k][i]] for i in range(8)]
63 | )
64 | for k in range(8)
65 | ]
66 | + [""]
67 | )
68 | )
69 |
70 | def is_over(self):
71 | """The game is considered over when someone cannot play. That
72 | may not be the actual rule but it is simpler to code :). Of
73 | course it would be possible to implement that a player can pass
74 | if it cannot play (by adding the move 'pass')"""
75 | return self.possible_moves() == []
76 |
77 | def scoring(self):
78 | """
79 | In the beginning of the game (less than 32 pieces) much
80 | importance is given to placing pieces on the border. After this
81 | point, only the number of pieces of each player counts
82 | """
83 |
84 | if np.sum(self.board == 0) > 32: # less than half the board is full
85 | player = (self.board == self.current_player).astype(int)
86 | opponent = (self.board == self.opponent_index).astype(int)
87 | return ((player - opponent) * BOARD_SCORE).sum()
88 | else:
89 | npieces_player = np.sum(self.board == self.current_player)
90 | npieces_opponent = np.sum(self.board == self.opponent_index)
91 | return npieces_player - npieces_opponent
92 |
93 |
94 | # This board is used by the AI to give more importance to the border
95 | BOARD_SCORE = np.array(
96 | [
97 | [9, 3, 3, 3, 3, 3, 3, 9],
98 | [3, 1, 1, 1, 1, 1, 1, 3],
99 | [3, 1, 1, 1, 1, 1, 1, 3],
100 | [3, 1, 1, 1, 1, 1, 1, 3],
101 | [3, 1, 1, 1, 1, 1, 1, 3],
102 | [3, 1, 1, 1, 1, 1, 1, 3],
103 | [3, 1, 1, 1, 1, 1, 1, 3],
104 | [9, 3, 3, 3, 3, 3, 3, 9],
105 | ]
106 | )
107 |
108 | DIRECTIONS = [
109 | np.array([i, j]) for i in [-1, 0, 1] for j in [-1, 0, 1] if (i != 0 or j != 0)
110 | ]
111 |
112 |
113 | def pieces_flipped(board, pos, current_player):
114 | """
115 | Returns a list of the positions of the pieces to be flipped if
116 | player `nplayer` places a piece on the `board` at position `pos`.
117 | This is slow and could be coded in C or Cython.
118 | """
119 |
120 | flipped = []
121 |
122 | for d in DIRECTIONS:
123 | ppos = pos + d
124 | streak = []
125 | while (0 <= ppos[0] <= 7) and (0 <= ppos[1] <= 7):
126 | if board[ppos[0], ppos[1]] == 3 - current_player:
127 | streak.append(+ppos)
128 | elif board[ppos[0], ppos[1]] == current_player:
129 | flipped += streak
130 | break
131 | else:
132 | break
133 | ppos += d
134 |
135 | return flipped
136 |
137 |
138 | if __name__ == "__main__":
139 | from easyAI import Human_Player, AI_Player, Negamax
140 |
141 | # An example: Computer vs Computer:
142 | game = Reversi([AI_Player(Negamax(4)), AI_Player(Negamax(4))])
143 | game.play()
144 | if game.scoring() > 0:
145 | print("player %d wins." % game.current_player)
146 | elif game.scoring() < 0:
147 | print("player %d wins." % game.opponent_index)
148 | else:
149 | print("Draw.")
150 |
--------------------------------------------------------------------------------
/easyAI/games/Knights-Kivy.py:
--------------------------------------------------------------------------------
1 | import numpy as np
2 | from easyAI import TwoPlayerGame, Human_Player, AI_Player, Negamax
3 | from kivy.app import App
4 | from kivy.uix.button import Button
5 | from kivy.uix.boxlayout import BoxLayout
6 |
7 |
8 | # directions in which a knight can move
9 | DIRECTIONS = list(
10 | map(
11 | np.array,
12 | [[1, 2], [-1, 2], [1, -2], [-1, -2], [2, 1], [2, -1], [-2, 1], [-2, -1]],
13 | )
14 | )
15 |
16 | BOARD_SIZE = (5, 5)
17 |
18 | # functions to convert "D8" into (3,7) and back...
19 | pos2string = lambda ab: "ABCDEFGH"[ab[0]] + str(ab[1] + 1)
20 | string2pos = lambda s: np.array(["ABCDEFGH".index(s[0]), int(s[1]) - 1])
21 |
22 | SQUARE_COLORS = [
23 | (0.8, 0.8, 0.8, 1), # empty
24 | (0.5, 0.5, 1.0, 1), # player 1
25 | (1.0, 1.0, 0.8, 1), # player 2
26 | (0.8, 0.0, 0.0, 1), # occupied
27 | ]
28 | SQUARE_TEXT = [" ", "K1", "K2", "X"]
29 |
30 | AI = Negamax(11)
31 |
32 |
33 | class Knights(TwoPlayerGame):
34 | """
35 | Each player has a chess knight (that moves in "L") on a chessboard.
36 | Each turn the player moves the knight to any tile that hasn't been
37 | occupied by a knight before. The first player that cannot move loses.
38 | """
39 |
40 | def __init__(self, players, board_size=(8, 8)):
41 | self.players = players
42 | self.board_size = board_size
43 | self.board = np.zeros(board_size, dtype=int)
44 | self.board[0, 0] = 1
45 | self.board[board_size[0] - 1, board_size[1] - 1] = 2
46 | players[0].pos = np.array([0, 0])
47 | players[1].pos = np.array([board_size[0] - 1, board_size[1] - 1])
48 | self.current_player = 1 # player 1 starts.
49 |
50 | def possible_moves(self):
51 | endings = [self.player.pos + d for d in DIRECTIONS]
52 | return [
53 | pos2string(e)
54 | for e in endings
55 | if (e[0] >= 0)
56 | and (e[1] >= 0)
57 | and (e[0] < self.board_size[0])
58 | and (e[1] < self.board_size[1])
59 | and (self.board[e[0], e[1]] == 0) # inside the board # and not blocked
60 | ]
61 |
62 | def make_move(self, pos):
63 | pi, pj = self.player.pos
64 | self.board[pi, pj] = 3 # 3 means blocked
65 | self.player.pos = string2pos(pos)
66 | pi, pj = self.player.pos
67 | self.board[pi, pj] = self.current_player # place player on board
68 |
69 | def show(self):
70 | print(
71 | "\n"
72 | + "\n".join(
73 | [" 1 2 3 4 5 6 7 8"]
74 | + [
75 | "ABCDEFGH"[k]
76 | + " "
77 | + " ".join(
78 | [
79 | [".", "1", "2", "X"][self.board[k, i]]
80 | for i in range(self.board_size[0])
81 | ]
82 | )
83 | for k in range(self.board_size[1])
84 | ]
85 | + [""]
86 | )
87 | )
88 |
89 | def lose(self):
90 | return self.possible_moves() == []
91 |
92 | def scoring(self):
93 | return -100 if (self.possible_moves() == []) else 0
94 |
95 | def is_over(self):
96 | return self.lose()
97 |
98 |
99 | class KnightsKivyApp(App):
100 | def build(self):
101 | layout = BoxLayout(padding=10, orientation="vertical")
102 |
103 | self.msg_button = Button(text="K1, it is your turn.")
104 | layout.add_widget(self.msg_button)
105 |
106 | self.squares = [[] for _ in range(BOARD_SIZE[1])]
107 | for i in range(BOARD_SIZE[1]):
108 | h_layout = BoxLayout(padding=1)
109 | for j in range(BOARD_SIZE[0]):
110 | new_button = Button(on_press=self.do_move)
111 | new_button.location = (i, j)
112 | self.squares[i].append(new_button)
113 | h_layout.add_widget(new_button)
114 | layout.add_widget(h_layout)
115 |
116 | self.reset_button = Button(text="[start over]", on_press=self.reset_board)
117 | layout.add_widget(self.reset_button)
118 |
119 | self.refresh_board()
120 |
121 | return layout
122 |
123 | def do_move(self, btn):
124 | move = pos2string(btn.location)
125 | if move in self.game.possible_moves():
126 | self.game.play_move(move)
127 | self.refresh_board()
128 | if not self.game.is_over():
129 | self.msg_button.text = "AI is thinking. Please wait."
130 | move = self.game.get_move()
131 | self.game.play_move(move)
132 | self.msg_button.text = "K1, it is your turn."
133 | else:
134 | self.msg_button.text = "Invalid move. Try again."
135 | self.refresh_board()
136 |
137 | def refresh_board(self):
138 | for i in range(BOARD_SIZE[1]):
139 | for j in range(BOARD_SIZE[0]):
140 | self.squares[i][j].text = SQUARE_TEXT[self.game.board[i, j]]
141 | self.squares[i][j].background_color = SQUARE_COLORS[
142 | self.game.board[i, j]
143 | ]
144 | if self.game.is_over():
145 | self.msg_button.text = "Game over. {} wins.".format(
146 | SQUARE_TEXT[self.game.opponent_index]
147 | )
148 |
149 | def reset_board(self, btn):
150 | self.game = Knights([Human_Player(), AI_Player(AI)], BOARD_SIZE)
151 | self.refresh_board()
152 |
153 |
154 | if __name__ == "__main__":
155 | board_size = (5, 5)
156 | game = Knights([Human_Player(), AI_Player(AI)], BOARD_SIZE)
157 |
158 | app = KnightsKivyApp()
159 | app.game = game
160 | app.run()
161 |
--------------------------------------------------------------------------------
/easyAI/games/Chopsticks.py:
--------------------------------------------------------------------------------
1 | # contributed by mrfesol (Tomasz Wesolowski)
2 |
3 | from easyAI import TwoPlayerGame
4 | from easyAI.Player import Human_Player
5 | from copy import deepcopy
6 | from easyAI.AI.DictTranspositionTable import DictTranspositionTable
7 | from easyAI.AI.Hashes import JSWHashTranspositionTable
8 |
9 |
10 | class Chopsticks(TwoPlayerGame):
11 | """
12 | Simple game you can play with your fingers.
13 | See the rules on http://en.wikipedia.org/wiki/Chopsticks_(hand_game)
14 | Here, for simplicity, you can do only taps and splits.
15 |
16 | A move consists of:
17 | (type - split or tap, touching hand, touched hand, number of sticks 'transferred')
18 | for instance:
19 | ('split', 0, 1, 2)
20 | means that we transfer 2 sticks from hand 0 to hand 1
21 | and...
22 | ('tap', 1, 0, 3)
23 | indicates that we tap opponent's hand 0 with our hand 1 holding 3 sticks
24 |
25 | Type 'show moves' before any move and do a move by "move #XX"
26 | """
27 |
28 | def __init__(self, players, numhands=2):
29 | self.players = players
30 | self.numplayers = len(self.players)
31 | self.numhands = numhands
32 | self.current_player = 1 # player 1 starts.
33 |
34 | hand = [1 for hand in range(self.numhands)]
35 | self.hands = [hand[:] for player in range(self.numplayers)]
36 |
37 | def possible_moves(self):
38 | moves = []
39 | # splits
40 | for h1 in range(self.numhands):
41 | for h2 in range(self.numhands):
42 | if h1 == h2:
43 | continue
44 | hand1 = self.hands[self.current_player - 1][h1]
45 | hand2 = self.hands[self.current_player - 1][h2]
46 | for i in range(1, 1 + min(hand1, 5 - hand2)):
47 | move = ("split", h1, h2, i)
48 | if hand1 != hand2 + i and self.back_to_startstate(move) == False:
49 | moves.append(move)
50 |
51 | # taps
52 | for i in range(self.numhands):
53 | for j in range(self.numhands):
54 | hand_player = self.hands[self.current_player - 1][i]
55 | hand_opp = self.hands[self.opponent_index - 1][j]
56 | if hand_player != 0 and hand_opp != 0:
57 | moves.append(("tap", i, j, self.hands[self.current_player - 1][i]))
58 | return moves
59 |
60 | def make_move(self, move):
61 | type, one, two, value = move
62 | if type == "split":
63 | self.hands[self.current_player - 1][one] -= value
64 | self.hands[self.current_player - 1][two] += value
65 | else:
66 | self.hands[self.opponent_index - 1][two] += value
67 |
68 | for player in range(self.numplayers):
69 | for hand in range(self.numhands):
70 | if self.hands[player][hand] >= 5:
71 | self.hands[player][hand] = 0
72 |
73 | def lose(self):
74 | return max(self.hands[self.current_player - 1]) == 0
75 |
76 | def win(self):
77 | return max(self.hands[self.opponent_index - 1]) == 0
78 |
79 | def is_over(self):
80 | return self.lose() or self.win()
81 |
82 | def show(self):
83 | for i in range(self.numplayers):
84 | print("Player %d: " % (i + 1)),
85 | for j in range(self.numhands):
86 | if self.hands[i][j] > 0:
87 | print("|" * self.hands[i][j] + "\t"),
88 | else:
89 | print("x\t"),
90 | print("")
91 |
92 | def scoring(self):
93 | """
94 | Very simple heuristic counting 'alive' hands
95 | """
96 | if self.lose():
97 | return -100
98 | if self.win():
99 | return 100
100 | alive = [0] * 2
101 | for player in range(self.numplayers):
102 | for hand in range(len(self.hands[player])):
103 | alive[player] += self.hands[player][hand] > 0
104 | return alive[self.current_player - 1] - alive[self.opponent_index - 1]
105 |
106 | def ttentry(self):
107 | """
108 | Returns game entry
109 | """
110 | entry = [
111 | self.hands[i][j]
112 | for i in range(self.numplayers)
113 | for j in range(self.numhands)
114 | ]
115 | entry = entry + [self.current_player]
116 | return tuple(entry)
117 |
118 | def back_to_startstate(self, move):
119 | """
120 | Checking if move will cause returning to start state - never-ending loop protection
121 | """
122 | nextstate = self.copy()
123 | nextstate.make_move(move)
124 | hands_min = min([min(nextstate.hands[i]) for i in range(self.numplayers)])
125 | hands_max = max([max(nextstate.hands[i]) for i in range(self.numplayers)])
126 | return hands_min == 1 and hands_max == 1
127 |
128 |
129 | if __name__ == "__main__":
130 | from easyAI import Negamax, AI_Player, SSS, DUAL
131 | from easyAI.AI.TranspositionTable import TranspositionTable
132 |
133 | ai_algo_neg = Negamax(4)
134 | ai_algo_sss = SSS(4)
135 | dict_tt = DictTranspositionTable(32, JSWHashTranspositionTable())
136 | ai_algo_dual = DUAL(4, tt=TranspositionTable(dict_tt))
137 | Chopsticks(
138 | [AI_Player(ai_algo_neg), AI_Player(ai_algo_dual)]
139 | ).play() # first player never wins
140 |
141 | print("-" * 10)
142 | print("Statistics of custom dictionary:")
143 | print("Calls of hash: ", dict_tt.num_calls)
144 | print("Collisions: ", dict_tt.num_collisions)
145 |
--------------------------------------------------------------------------------
/easyAI/TwoPlayerGame.py:
--------------------------------------------------------------------------------
1 | from abc import ABC, abstractclassmethod
2 | from copy import deepcopy
3 |
4 |
5 | class TwoPlayerGame(ABC):
6 | """
7 | Base class for... wait for it... two-players games !
8 |
9 | To define a new game, make a subclass of TwoPlayerGame, and define
10 | the following methods:
11 |
12 | - ``__init__(self, players, ...)`` : initialization of the game
13 | - ``possible_moves(self)`` : returns of all moves allowed
14 | - ``make_move(self, move)``: transforms the game according to the move
15 | - ``is_over(self)``: check whether the game has ended
16 |
17 | The following methods are optional:
18 |
19 | - ``show(self)`` : prints/displays the game
20 | - ``scoring``: gives a score to the current game (for the AI)
21 | - ``unmake_move(self, move)``: how to unmake a move (speeds up the AI)
22 | - ``ttentry(self)``: returns a string/tuple describing the game.
23 | - ``ttrestore(self, entry)``: use string/tuple from ttentry to restore a game.
24 |
25 | The __init__ method *must* do the following actions:
26 |
27 | - Store ``players`` (which must be a list of two Players) into
28 | self.players
29 | - Tell which player plays first with ``self.current_player = 1 # or 2``
30 |
31 | When defining ``possible_moves``, you must keep in mind that you
32 | are in the scope of the *current player*. More precisely, a
33 | subclass of TwoPlayerGame has the following attributes that
34 | indicate whose turn it is. These methods can be used but should not
35 | be overwritten:
36 |
37 | - ``self.player`` : the current player (e.g. ``Human_Player``)
38 | - ``self.opponent`` : the current Player's opponent (Player).
39 | - ``self.current_player``: the number (1 or 2) of the current player.
40 | - ``self.opponent_index``: the number (1 or 2) of the opponent.
41 | - ``self.nmove``: How many moves have been played so far ?
42 |
43 | For more, see the examples in the dedicated folder.
44 |
45 | Examples:
46 | ----------
47 |
48 | ::
49 |
50 | from easyAI import TwoPlayerGame, Human_Player
51 |
52 | class Sticks( TwoPlayerGame ):
53 | ''' In turn, the players remove one, two or three sticks from
54 | a pile. The player who removes the last stick loses '''
55 |
56 | def __init__(self, players):
57 | self.players = players
58 | self.pile = 20 # start with 20 sticks
59 | self.current_player = 1 # player 1 starts
60 | def possible_moves(self): return ['1','2','3']
61 | def make_move(self,move): self.pile -= int(move)
62 | def is_over(self): return self.pile <= 0
63 |
64 | p
65 | game = Sticks( [Human_Player(), Human_Player() ] )
66 | game.play()
67 |
68 |
69 | """
70 |
71 | @abstractclassmethod
72 | def possible_moves(self):
73 | pass
74 |
75 | @abstractclassmethod
76 | def make_move(self, move):
77 | pass
78 |
79 | @abstractclassmethod
80 | def is_over(self):
81 | pass
82 |
83 | def play(self, nmoves=1000, verbose=True):
84 | """
85 | Method for starting the play of a game to completion. If one of the
86 | players is a Human_Player, then the interaction with the human is via
87 | the text terminal.
88 |
89 | Parameters
90 | -----------
91 |
92 | nmoves:
93 | The limit of how many moves (plies) to play unless the game ends on
94 | it's own first.
95 |
96 | verbose:
97 | Setting verbose=True displays additional text messages.
98 | """
99 |
100 | history = []
101 |
102 | if verbose:
103 | self.show()
104 |
105 | for self.nmove in range(1, nmoves + 1):
106 |
107 | if self.is_over():
108 | break
109 |
110 | move = self.player.ask_move(self)
111 | history.append((deepcopy(self), move))
112 | self.make_move(move)
113 |
114 | if verbose:
115 | print(
116 | "\nMove #%d: player %d plays %s :"
117 | % (self.nmove, self.current_player, str(move))
118 | )
119 | self.show()
120 |
121 | self.switch_player()
122 |
123 | history.append(deepcopy(self))
124 |
125 | return history
126 |
127 | @property
128 | def opponent_index(self):
129 | return 2 if (self.current_player == 1) else 1
130 |
131 | @property
132 | def player(self):
133 | return self.players[self.current_player - 1]
134 |
135 | @property
136 | def opponent(self):
137 | return self.players[self.opponent_index - 1]
138 |
139 | def switch_player(self):
140 | self.current_player = self.opponent_index
141 |
142 | def copy(self):
143 | return deepcopy(self)
144 |
145 | def get_move(self):
146 | """
147 | Method for getting a move from the current player. If the player is an
148 | AI_Player, then this method will invoke the AI algorithm to choose the
149 | move. If the player is a Human_Player, then the interaction with the
150 | human is via the text terminal.
151 | """
152 | return self.player.ask_move(self)
153 |
154 | def play_move(self, move):
155 | """
156 | Method for playing one move with the current player. After making the move,
157 | the current player will change to the next player.
158 |
159 | Parameters
160 | -----------
161 |
162 | move:
163 | The move to be played. ``move`` should match an entry in the ``.possibles_moves()`` list.
164 | """
165 | result = self.make_move(move)
166 | self.switch_player()
167 | return result
168 |
--------------------------------------------------------------------------------
/docs/source/_themes/flask_small/static/flasky.css_t:
--------------------------------------------------------------------------------
1 | /*
2 | * flasky.css_t
3 | * ~~~~~~~~~~~~
4 | *
5 | * Sphinx stylesheet -- flasky theme based on nature theme.
6 | *
7 | * :copyright: Copyright 2007-2010 by the Sphinx team, see AUTHORS.
8 | * :license: BSD, see LICENSE for details.
9 | *
10 | */
11 |
12 | @import url("basic.css");
13 |
14 | /* -- page layout ----------------------------------------------------------- */
15 |
16 | body {
17 | font-family: 'Georgia', serif;
18 | font-size: 17px;
19 | color: #000;
20 | background: white;
21 | margin: 0;
22 | padding: 0;
23 | }
24 |
25 | div.documentwrapper {
26 | float: left;
27 | width: 100%;
28 | }
29 |
30 | div.bodywrapper {
31 | margin: 40px auto 0 auto;
32 | width: 700px;
33 | }
34 |
35 | hr {
36 | border: 1px solid #B1B4B6;
37 | }
38 |
39 | div.body {
40 | background-color: #ffffff;
41 | color: #3E4349;
42 | padding: 0 30px 30px 30px;
43 | }
44 |
45 | img.floatingflask {
46 | padding: 0 0 10px 10px;
47 | float: right;
48 | }
49 |
50 | div.footer {
51 | text-align: right;
52 | color: #888;
53 | padding: 10px;
54 | font-size: 14px;
55 | width: 650px;
56 | margin: 0 auto 40px auto;
57 | }
58 |
59 | div.footer a {
60 | color: #888;
61 | text-decoration: underline;
62 | }
63 |
64 | div.related {
65 | line-height: 32px;
66 | color: #888;
67 | }
68 |
69 | div.related ul {
70 | padding: 0 0 0 10px;
71 | }
72 |
73 | div.related a {
74 | color: #444;
75 | }
76 |
77 | /* -- body styles ----------------------------------------------------------- */
78 |
79 | a {
80 | color: #004B6B;
81 | text-decoration: underline;
82 | }
83 |
84 | a:hover {
85 | color: #6D4100;
86 | text-decoration: underline;
87 | }
88 |
89 | div.body {
90 | padding-bottom: 40px; /* saved for footer */
91 | }
92 |
93 | div.body h1,
94 | div.body h2,
95 | div.body h3,
96 | div.body h4,
97 | div.body h5,
98 | div.body h6 {
99 | font-family: 'Garamond', 'Georgia', serif;
100 | font-weight: normal;
101 | margin: 30px 0px 10px 0px;
102 | padding: 0;
103 | }
104 |
105 | {% if theme_index_logo %}
106 | div.indexwrapper h1 {
107 | text-indent: -999999px;
108 | background: url({{ theme_index_logo }}) no-repeat center center;
109 | height: {{ theme_index_logo_height }};
110 | }
111 | {% endif %}
112 |
113 | div.body h2 { font-size: 180%; }
114 | div.body h3 { font-size: 150%; }
115 | div.body h4 { font-size: 130%; }
116 | div.body h5 { font-size: 100%; }
117 | div.body h6 { font-size: 100%; }
118 |
119 | a.headerlink {
120 | color: white;
121 | padding: 0 4px;
122 | text-decoration: none;
123 | }
124 |
125 | a.headerlink:hover {
126 | color: #444;
127 | background: #eaeaea;
128 | }
129 |
130 | div.body p, div.body dd, div.body li {
131 | line-height: 1.4em;
132 | }
133 |
134 | div.admonition {
135 | background: #fafafa;
136 | margin: 20px -30px;
137 | padding: 10px 30px;
138 | border-top: 1px solid #ccc;
139 | border-bottom: 1px solid #ccc;
140 | }
141 |
142 | div.admonition p.admonition-title {
143 | font-family: 'Garamond', 'Georgia', serif;
144 | font-weight: normal;
145 | font-size: 24px;
146 | margin: 0 0 10px 0;
147 | padding: 0;
148 | line-height: 1;
149 | }
150 |
151 | div.admonition p.last {
152 | margin-bottom: 0;
153 | }
154 |
155 | div.highlight{
156 | background-color: white;
157 | }
158 |
159 | dt:target, .highlight {
160 | background: #FAF3E8;
161 | }
162 |
163 | div.note {
164 | background-color: #eee;
165 | border: 1px solid #ccc;
166 | }
167 |
168 | div.seealso {
169 | background-color: #ffc;
170 | border: 1px solid #ff6;
171 | }
172 |
173 | div.topic {
174 | background-color: #eee;
175 | }
176 |
177 | div.warning {
178 | background-color: #ffe4e4;
179 | border: 1px solid #f66;
180 | }
181 |
182 | p.admonition-title {
183 | display: inline;
184 | }
185 |
186 | p.admonition-title:after {
187 | content: ":";
188 | }
189 |
190 | pre, tt {
191 | font-family: 'Consolas', 'Menlo', 'Deja Vu Sans Mono', 'Bitstream Vera Sans Mono', monospace;
192 | font-size: 0.85em;
193 | }
194 |
195 | img.screenshot {
196 | }
197 |
198 | tt.descname, tt.descclassname {
199 | font-size: 0.95em;
200 | }
201 |
202 | tt.descname {
203 | padding-right: 0.08em;
204 | }
205 |
206 | img.screenshot {
207 | -moz-box-shadow: 2px 2px 4px #eee;
208 | -webkit-box-shadow: 2px 2px 4px #eee;
209 | box-shadow: 2px 2px 4px #eee;
210 | }
211 |
212 | table.docutils {
213 | border: 1px solid #888;
214 | -moz-box-shadow: 2px 2px 4px #eee;
215 | -webkit-box-shadow: 2px 2px 4px #eee;
216 | box-shadow: 2px 2px 4px #eee;
217 | }
218 |
219 | table.docutils td, table.docutils th {
220 | border: 1px solid #888;
221 | padding: 0.25em 0.7em;
222 | }
223 |
224 | table.field-list, table.footnote {
225 | border: none;
226 | -moz-box-shadow: none;
227 | -webkit-box-shadow: none;
228 | box-shadow: none;
229 | }
230 |
231 | table.footnote {
232 | margin: 15px 0;
233 | width: 100%;
234 | border: 1px solid #eee;
235 | }
236 |
237 | table.field-list th {
238 | padding: 0 0.8em 0 0;
239 | }
240 |
241 | table.field-list td {
242 | padding: 0;
243 | }
244 |
245 | table.footnote td {
246 | padding: 0.5em;
247 | }
248 |
249 | dl {
250 | margin: 0;
251 | padding: 0;
252 | }
253 |
254 | dl dd {
255 | margin-left: 30px;
256 | }
257 |
258 | pre {
259 | padding: 0;
260 | margin: 15px -30px;
261 | padding: 8px;
262 | line-height: 1.3em;
263 | padding: 7px 30px;
264 | background: #eee;
265 | border-radius: 2px;
266 | -moz-border-radius: 2px;
267 | -webkit-border-radius: 2px;
268 | }
269 |
270 | dl pre {
271 | margin-left: -60px;
272 | padding-left: 60px;
273 | }
274 |
275 | tt {
276 | background-color: #ecf0f3;
277 | color: #222;
278 | /* padding: 1px 2px; */
279 | }
280 |
281 | tt.xref, a tt {
282 | background-color: #FBFBFB;
283 | }
284 |
285 | a:hover tt {
286 | background: #EEE;
287 | }
288 |
--------------------------------------------------------------------------------
/easyAI/AI/solving.py:
--------------------------------------------------------------------------------
1 | from easyAI.AI import Negamax
2 | from easyAI.Player import AI_Player
3 |
4 |
5 | def solve_with_iterative_deepening(
6 | game, ai_depths, win_score, scoring=None, tt=None, verbose=True, **game_params
7 | ):
8 | """
9 | Solves a game using iterative deepening, i.e. determines if by playing
10 | perfectly the first player can force a win, or whether it will always
11 | lose against a perfect opponent.
12 |
13 |
14 | This algorithm explores the game by using several times the Negamax
15 | algorithm, always starting at the initial state of the game, but
16 | taking increasing depth (in the list ai_depths) until the score of
17 | the initial condition indicates that the first player will certainly
18 | win or loose, in which case it stops.
19 | The use of transposition table leads to speed gain as the results
20 | of shallower searches are used to help exploring the deeper ones.
21 |
22 | Parameters
23 | -----------
24 |
25 | game
26 | An instance of a TwoPlayerGame
27 |
28 | ai_depths:
29 | List of AI depths to try (e.g. [5,6,7,8,9,10])
30 |
31 |
32 | win_score:
33 | Score above which a score means a win.
34 |
35 | scoring:
36 | Scoring function (see doc of class Negamax)
37 |
38 | tt:
39 | An optional transposition table to speed up computations.
40 |
41 | verbose:
42 | If set to ``True``, will print a summary of the best move
43 | after each depth tried.
44 |
45 | Returns
46 | --------
47 |
48 | (result, depth, move):
49 | As below
50 |
51 | result:
52 | Either 1 (certain victory of the first player) or -1
53 | (certain defeat) or 0 (either draw, or the search was not
54 | deep enough)
55 |
56 | depth:
57 | The minimal number of moves before victory (or defeat)
58 |
59 | move:
60 | Best move to play for the first player.
61 |
62 | Also returns with ``tt`` set.
63 | Will be None if ``use_tt`` was set to false, else will be a
64 | transposition table containing all the relevant situations to play
65 | a perfect game and can be used with ``AI_player(tt)``
66 |
67 | """
68 | if game.players is None:
69 | game.players = [AI_Player(None), AI_Player(None)]
70 |
71 | for depth in ai_depths:
72 | ai = Negamax(depth, scoring, tt=tt)
73 | ai(game)
74 | alpha = ai.alpha
75 | if verbose:
76 | print("d:%d, a:%d, m:%s" % (depth, alpha, str(game.ai_move)))
77 | if abs(alpha) >= win_score:
78 | break
79 |
80 | # 1:win, 0:draw, -1:defeat
81 | result = +1 if alpha >= win_score else (-1 if alpha <= -win_score else 0)
82 |
83 | return result, depth, game.ai_move
84 |
85 |
86 | def solve_with_depth_first_search(game, win_score, maxdepth=50, tt=None, depth=0):
87 | """
88 | Solves a game using a depth-first search: the game is explored until
89 | endgames are reached.
90 |
91 | The endgames are evaluated to see if there are victories or defeats.
92 | Then, a situation in which every move leads to a defeat is labelled
93 | as a (certain) defeat, and a situation in which one move leads to a
94 | (certain) defeat of the opponent is labelled as a (certain) victory.
95 | Situations are evaluated until the initial condition receives a label
96 | (victory or defeat). Draws are also possible.
97 |
98 | This algorithm can be faster but less informative than ``solve_with_iterative_deepening``,
99 | as it does not provide 'optimal' strategies (like shortest path to
100 | the victory). It returns simply 1, 0, or -1 to indicate certain
101 | victory, draw, or defeat of the first player.
102 |
103 | Parameters
104 | -----------
105 |
106 | game:
107 | An Game instance, initialized and ready to be played.
108 |
109 | win_score:
110 | Score above which a score means a win.
111 |
112 | maxdepth:
113 | Maximal recursion depth allowed.
114 |
115 | tt:
116 | An optional transposition table to speed up computations.
117 |
118 |
119 | depth:
120 | Index of the current depth (don't touch that).
121 |
122 | Returns
123 | --------
124 |
125 | result
126 | Either 1 (certain victory of the first player) or -1
127 | (certain defeat) or 0 (either draw, or the search was not
128 | deep enough)
129 |
130 | """
131 |
132 | # Is there a transposition table and is this game in it ?
133 | lookup = None if (tt is None) else tt.lookup(game)
134 | if lookup is not None:
135 | return lookup["value"]
136 |
137 | if depth == maxdepth:
138 | raise "Max recursion depth reached :("
139 |
140 | if game.is_over():
141 | score = game.scoring()
142 | value = 1 if (score >= win_score) else (-1 if -score >= win_score else 0)
143 | if tt is not None:
144 | tt.store(game=game, value=value, move=None)
145 | return value
146 |
147 | possible_moves = game.possible_moves()
148 |
149 | state = game
150 | unmake_move = hasattr(state, "unmake_move")
151 |
152 | best_value, best_move = -1, None
153 |
154 | for move in possible_moves:
155 |
156 | if not unmake_move:
157 | game = state.copy() # re-initialize move
158 |
159 | game.make_move(move)
160 | game.switch_player()
161 |
162 | move_value = -solve_with_depth_first_search(
163 | game, win_score, maxdepth, tt, depth + 1
164 | )
165 |
166 | if unmake_move:
167 | game.switch_player()
168 | game.unmake_move(move)
169 |
170 | if move_value == 1:
171 | if tt is not None:
172 | tt.store(game=state, value=1, move=move)
173 | return move_value
174 |
175 | if move_value == 0 and best_value == -1:
176 | # Is forcing a draw possible ?
177 | best_value = 0
178 | best_move = move
179 | if tt is not None:
180 | tt.store(game=state, value=best_value, move=best_move)
181 |
182 | return best_value
183 |
--------------------------------------------------------------------------------
/easyAI/AI/Hashes.py:
--------------------------------------------------------------------------------
1 | # contributed by mrfesol (Tomasz Wesolowski)
2 | """
3 | Different types of hashes.
4 | Try each to choose the one that cause the least collisions (you can check it
5 | by printing DictTranspositionTable.num_collisions)
6 | Also, you can easily create one of your own!
7 |
8 | You can read more about these hash function on:
9 | http://www.eternallyconfuzzled.com/tuts/algorithms/jsw_tut_hashing.aspx
10 | """
11 | from .HashTranspositionTable import HashTranspositionTable
12 |
13 |
14 | class SimpleHashTranspositionTable(HashTranspositionTable):
15 | """
16 | Surprisingly - very effective for strings
17 | """
18 |
19 | def join(self, one, two):
20 | return 101 * one + two
21 |
22 |
23 | class XorHashTranspositionTable(HashTranspositionTable):
24 | def join(self, one, two):
25 | return one ^ two
26 |
27 |
28 | class AddHashTranspositionTable(HashTranspositionTable):
29 | def join(self, one, two):
30 | return one + two
31 |
32 |
33 | class RotateHashTranspositionTable(HashTranspositionTable):
34 | def join(self, one, two):
35 | return (one << 4) ^ (one >> 28) ^ two
36 |
37 |
38 | class BernsteinHashTranspositionTable(HashTranspositionTable):
39 | def join(self, one, two):
40 | return 33 * one + two
41 |
42 |
43 | class ShiftAndAddHashTranspositionTable(HashTranspositionTable):
44 | def join(self, one, two):
45 | return one ^ (one << 5) + (one >> 2) + two
46 |
47 |
48 | class FNVHashTranspositionTable(HashTranspositionTable):
49 | def before(self, key):
50 | return 2166136261
51 |
52 | def join(self, one, two):
53 | return (one * 16777619) ^ two
54 |
55 |
56 | class OneAtATimeTranspositionTable(HashTranspositionTable):
57 | def join(self, one, two):
58 | one += two
59 | one += one << 10
60 | return one ^ (one >> 6)
61 |
62 | def after(self, key, hash):
63 | hash += hash << 3
64 | hash ^= hash >> 11
65 | hash += hash << 15
66 | return hash
67 |
68 |
69 | class JSWHashTranspositionTable(HashTranspositionTable):
70 | def before(self, key):
71 | return 16777551
72 |
73 | def join(self, one, two):
74 | return (one << 1 | one >> 31) ^ two
75 |
76 |
77 | class ELFHashTranspositionTable(HashTranspositionTable):
78 | def before(self, key):
79 | self.g = 0
80 | return 0
81 |
82 | def join(self, one, two):
83 | one = (one << 4) + two
84 | self.g = one & int("0xF0000000L", 16)
85 |
86 | if self.g != 0:
87 | one ^= self.g >> 24
88 |
89 | one &= ~self.g
90 | return (one << 1 | one >> 31) ^ two
91 |
92 |
93 | class JenkinsHashTranspositionTable(HashTranspositionTable):
94 | """
95 | The most advanced hash function on the list.
96 | Way too many things going on to put something smart in short comment.
97 | """
98 |
99 | def mix(self, a, b, c):
100 | """
101 | Auxiliary function.
102 | """
103 | a -= b
104 | a -= c
105 | a ^= c >> 13
106 | b -= c
107 | b -= a
108 | b ^= a << 8
109 | c -= a
110 | c -= b
111 | c ^= b >> 13
112 | a -= b
113 | a -= c
114 | a ^= c >> 12
115 | b -= c
116 | b -= a
117 | b ^= a << 16
118 | c -= a
119 | c -= b
120 | c ^= b >> 5
121 | a -= b
122 | a -= c
123 | a ^= c >> 3
124 | b -= c
125 | b -= a
126 | b ^= a << 10
127 | c -= a
128 | c -= b
129 | c ^= b >> 15
130 | return a, b, c
131 |
132 | def before(self, key):
133 | self.a = self.b = 0x9E3779B9
134 | self.c = 0
135 |
136 | def get_hash(self, key, depth=0):
137 | """
138 | Overridden.
139 | Just to create list of single elements to hash
140 | """
141 | if depth == 0:
142 | self.before(key)
143 | if type(key) is int:
144 | return [key]
145 | if type(key) is str and len(key) <= 1:
146 | return [key]
147 | tab = []
148 | for v in list(key):
149 | tab = tab + self.get_hash(v, depth + 1)
150 | return self.compute_hash(tab)
151 |
152 | def compute_hash(self, tab):
153 | """
154 | Computes real hash
155 | """
156 | length = len(tab)
157 | cur = 0
158 | while length >= 12:
159 | self.a += (
160 | abs(tab[cur + 0])
161 | + (tab[cur + 1] << 8)
162 | + (tab[cur + 2] << 16)
163 | + (tab[cur + 3] << 24)
164 | )
165 | self.b += (
166 | tab[cur + 4]
167 | + (tab[cur + 5] << 8)
168 | + (tab[cur + 6] << 16)
169 | + (tab[cur + 7] << 24)
170 | )
171 | self.c += (
172 | tab[cur + 8]
173 | + (tab[cur + 9] << 8)
174 | + (tab[cur + 10] << 16)
175 | + (tab[cur + 11] << 24)
176 | )
177 |
178 | self.a, self.b, self.c = self.mix(self.a, self.b, self.c)
179 |
180 | cur += 12
181 | length -= 12
182 |
183 | self.c += len(tab)
184 |
185 | if length == 11:
186 | self.c += tab[cur + 10] << 24
187 | if length == 10:
188 | self.c += tab[9] << 16
189 | if length == 9:
190 | self.c += tab[8] << 8
191 | if length == 8:
192 | self.b += tab[7] << 24
193 | if length == 7:
194 | self.b += tab[6] << 16
195 | if length == 6:
196 | self.b += tab[5] << 8
197 | if length == 5:
198 | self.b += tab[4]
199 | if length == 4:
200 | self.a += tab[3] << 24
201 | if length == 3:
202 | self.a += tab[2] << 16
203 | if length == 2:
204 | self.a += tab[1] << 8
205 | if length == 1:
206 | self.a += tab[0]
207 |
208 | self.a, self.b, self.c = self.mix(self.a, self.b, self.c)
209 |
210 | return self.c
--------------------------------------------------------------------------------
/easyAI/AI/Negamax.py:
--------------------------------------------------------------------------------
1 | """
2 | The standard AI algorithm of easyAI is Negamax with alpha-beta pruning
3 | and (optionnally), transposition tables.
4 | """
5 |
6 | import pickle
7 |
8 | LOWERBOUND, EXACT, UPPERBOUND = -1, 0, 1
9 | inf = float("infinity")
10 |
11 |
12 | def negamax(game, depth, origDepth, scoring, alpha=+inf, beta=-inf, tt=None):
13 | """
14 | This implements Negamax with transposition tables.
15 | This method is not meant to be used directly. See ``easyAI.Negamax``
16 | for an example of practical use.
17 | This function is implemented (almost) acccording to
18 | http://en.wikipedia.org/wiki/Negamax
19 | """
20 |
21 | alphaOrig = alpha
22 |
23 | # Is there a transposition table and is this game in it ?
24 | lookup = None if (tt is None) else tt.lookup(game)
25 |
26 | if lookup is not None:
27 | # The game has been visited in the past
28 |
29 | if lookup["depth"] >= depth:
30 | flag, value = lookup["flag"], lookup["value"]
31 | if flag == EXACT:
32 | if depth == origDepth:
33 | game.ai_move = lookup["move"]
34 | return value
35 | elif flag == LOWERBOUND:
36 | alpha = max(alpha, value)
37 | elif flag == UPPERBOUND:
38 | beta = min(beta, value)
39 |
40 | if alpha >= beta:
41 | if depth == origDepth:
42 | game.ai_move = lookup["move"]
43 | return value
44 |
45 | if (depth == 0) or game.is_over():
46 | # NOTE: the "depth" variable represents the depth left to recurse into,
47 | # so the smaller it is, the deeper we are in the negamax recursion.
48 | # Here we add 0.001 as a bonus to signify that victories in less turns
49 | # have more value than victories in many turns (and conversely, defeats
50 | # after many turns are preferred over defeats in less turns)
51 | return scoring(game) * (1 + 0.001 * depth)
52 |
53 | if lookup is not None:
54 | # Put the supposedly best move first in the list
55 | possible_moves = game.possible_moves()
56 | possible_moves.remove(lookup["move"])
57 | possible_moves = [lookup["move"]] + possible_moves
58 |
59 | else:
60 |
61 | possible_moves = game.possible_moves()
62 |
63 | state = game
64 | best_move = possible_moves[0]
65 | if depth == origDepth:
66 | state.ai_move = possible_moves[0]
67 |
68 | bestValue = -inf
69 | unmake_move = hasattr(state, "unmake_move")
70 |
71 | for move in possible_moves:
72 |
73 | if not unmake_move:
74 | game = state.copy() # re-initialize move
75 |
76 | game.make_move(move)
77 | game.switch_player()
78 |
79 | move_alpha = -negamax(game, depth - 1, origDepth, scoring, -beta, -alpha, tt)
80 |
81 | if unmake_move:
82 | game.switch_player()
83 | game.unmake_move(move)
84 |
85 | # bestValue = max( bestValue, move_alpha )
86 | if bestValue < move_alpha:
87 | bestValue = move_alpha
88 | best_move = move
89 |
90 | if alpha < move_alpha:
91 | alpha = move_alpha
92 | # best_move = move
93 | if depth == origDepth:
94 | state.ai_move = move
95 | if alpha >= beta:
96 | break
97 |
98 | if tt is not None:
99 |
100 | assert best_move in possible_moves
101 | tt.store(
102 | game=state,
103 | depth=depth,
104 | value=bestValue,
105 | move=best_move,
106 | flag=UPPERBOUND
107 | if (bestValue <= alphaOrig)
108 | else (LOWERBOUND if (bestValue >= beta) else EXACT),
109 | )
110 |
111 | return bestValue
112 |
113 |
114 | class Negamax:
115 | """
116 | This implements Negamax on steroids. The following example shows
117 | how to setup the AI and play a Connect Four game:
118 |
119 | >>> from easyAI.games import ConnectFour
120 | >>> from easyAI import Negamax, Human_Player, AI_Player
121 | >>> scoring = lambda game: -100 if game.lose() else 0
122 | >>> ai_algo = Negamax(8, scoring) # AI will think 8 turns in advance
123 | >>> game = ConnectFour([Human_Player(), AI_Player(ai_algo)])
124 | >>> game.play()
125 |
126 | Parameters
127 | -----------
128 |
129 | depth:
130 | How many moves in advance should the AI think ?
131 | (2 moves = 1 complete turn)
132 |
133 | scoring:
134 | A function f(game)-> score. If no scoring is provided
135 | and the game object has a ``scoring`` method it ill be used.
136 |
137 | win_score:
138 | Score above which the score means a win. This will be
139 | used to speed up computations if provided, but the AI will not
140 | differentiate quick defeats from long-fought ones (see next
141 | section).
142 |
143 | tt:
144 | A transposition table (a table storing game states and moves)
145 | scoring: can be none if the game that the AI will be given has a
146 | ``scoring`` method.
147 |
148 | Notes
149 | -----
150 |
151 | The score of a given game is given by
152 |
153 | >>> scoring(current_game) - 0.01*sign*current_depth
154 |
155 | for instance if a lose is -100 points, then losing after 4 moves
156 | will score -99.96 points but losing after 8 moves will be -99.92
157 | points. Thus, the AI will chose the move that leads to defeat in
158 | 8 turns, which makes it more difficult for the (human) opponent.
159 | This will not always work if a ``win_score`` argument is provided.
160 |
161 | """
162 |
163 | def __init__(self, depth, scoring=None, win_score=+inf, tt=None):
164 | self.scoring = scoring
165 | self.depth = depth
166 | self.tt = tt
167 | self.win_score = win_score
168 |
169 | def __call__(self, game):
170 | """
171 | Returns the AI's best move given the current state of the game.
172 | """
173 |
174 | scoring = (
175 | self.scoring if self.scoring else (lambda g: g.scoring())
176 | ) # horrible hack
177 |
178 | self.alpha = negamax(
179 | game,
180 | self.depth,
181 | self.depth,
182 | scoring,
183 | -self.win_score,
184 | +self.win_score,
185 | self.tt,
186 | )
187 | return game.ai_move
188 |
--------------------------------------------------------------------------------
/docs/source/get_started.rst:
--------------------------------------------------------------------------------
1 | Get Started With easyAI
2 | ========================
3 |
4 | The best way to get started is to have a look at :ref:`a-quick-example`. What follows is a summary of all there is to know about easyAI (you can also find these informations in the documentation of the code).
5 |
6 | Defining a game
7 | ---------------
8 |
9 | To define a new game, make a subclass of the class ``easyAI.TwoPlayerGame``, and define these methods:
10 |
11 | - ``__init__(self, players, ...)`` : initialization of the game
12 | - ``possible_moves(self)`` : returns of all moves allowed
13 | - ``make_move(self, move)``: transforms the game according to the move
14 | - ``is_over(self)``: check whether the game has ended
15 |
16 | The following methods are optional:
17 |
18 | - ``show(self)`` : prints/displays the game
19 | - ``scoring``: gives a score to the current game (for the AI)
20 | - ``unmake_move(self, move)``: how to unmake a move (speeds up the AI)
21 | - ``ttentry(self)``: returns a string/tuple describing the game.
22 | - ``ttrestore(self, entry)``: use string/tuple from ttentry to restore a game.
23 |
24 | The ``__init__`` method *must* do the following actions:
25 |
26 | - Store ``players`` (which must be a list of two Players) into
27 | self.players
28 | - Tell which player plays first with ``self.current_player = 1 # or 2``
29 |
30 | When defining ``possible_moves``, ``scoring``, etc. you must keep in mind that you are in the scope of the *current player*. More precisely, a subclass of TwoPlayerGame has the following attributes that indicate whose turn it is. These attributes can be used but should not be overwritten:
31 |
32 | - ``self.player`` : the current Player (e.g. a ``Human_Player()``).
33 | - ``self.opponent`` : the current Player's opponent (Player).
34 | - ``self.current_player``: the number (1 or 2) of the current player.
35 | - ``self.opponent_index``: the number (1 or 2) of the opponent.
36 | - ``self.nmove``: How many moves have been played so far ?
37 |
38 | To start a game you will write something like this ::
39 |
40 | game = MyGame(players = [player_1, player_2], *other_arguments)
41 | history = game.play() # start the match !
42 |
43 | When the game ends it stores the history into the variable ``history``. The history is a list *[(g1,m1),(g2,m2)...]* where *gi* is a copy of the game after i moves and *mi* is the move made by the player whose turn it was. So for instance: ::
44 |
45 | history = game.play()
46 | game8, move8 = history[8]
47 | game9, move9 = history[9]
48 | game8.make_move( move8 ) # Now game8 and game9 are alike.
49 |
50 |
51 | Human and AI players
52 | ---------------------
53 |
54 |
55 | The players can be either a ``Human_Player()`` (which will be asked interactively which moves it wants to play) or a ``AI_Player(algo)``, so you will have for instance ::
56 |
57 | game = MyGame( [ Human_Player(), AI_Player(algo) ])
58 |
59 | If you are a human player you will be asked to enter a move when it is your turn. You can also enter ``show moves`` to have a list of all moves allowed, or ``quit`` to quit.
60 |
61 | The variable `algo` is any function ``f(game)->move``. It can be an algorithm that determines the best move by thinking N turns in advance: ::
62 |
63 | from easyAI import AI_Player, Negamax
64 | ai_player = AI_Player( Negamax(9) )
65 |
66 | Or a transposition table (see below) filled in a previous game: ::
67 |
68 | ai_player = AI_Player( transpo_table )
69 |
70 | The Negamax algorithm will always look for the shortest path to victory, or the longest path to defeat. It is possible to go faster by not optimizing this (the disadvantage being that the AI can then make *suicidal* moves if it has found that it will eventually lose against a perfect opponent). To do so, you must provide the argument ``win_score`` to Negamax which indicates above which score a score is considered a win. Keep in mind that the AI adds maluses to the score, so if your scoring function looks like this ::
71 |
72 | scoring = lambda game: 100 if game.win() else 0
73 |
74 | you should write ``Negamax(9, win_score=90)``.
75 |
76 |
77 | Interactive Play
78 | ----------------
79 |
80 | If you are needing to be more interactive with the game play, such as when integrating with other frameworks, you can use the ``get_move`` and ``play_move`` methods instead. ``get_move`` get's an AI player's decision. ``play_move`` executes a move (for either player). To illustrate ::
81 |
82 | game.play()
83 |
84 | is functionally the same as ::
85 |
86 | while not game.is_over():
87 | game.show()
88 | if game.current_player==1: # we are assuming player 1 is a Human_Player
89 | poss = game.possible_moves()
90 | for index, move in enumerate(poss):
91 | print("{} : {}".format(index, move))
92 | index = int(input("enter move: "))
93 | move = poss[index]
94 | else: # we are assuming player 2 is an AI_Player
95 | move = game.get_move()
96 | print("AI plays {}".format(move))
97 | game.play_move(move)
98 |
99 |
100 | Solving a game
101 | ---------------
102 |
103 | You can try to solve a game (i.e. determine who will win if both players play perfectly and extract a winning strategy). There are two available algorithms to do so:
104 |
105 | **solve_with_iterative_deepening** solves a game using iterative deepening: it explores the game by using several times the Negamax algorithm, always starting at the initial state of the game, but taking increasing depth (in the list ai_depths) until the score of the initial condition indicates that the first player will certainly win or loose, at which case it stops: ::
106 |
107 | from easyAI import solve_with_iterative_deepening
108 | r,d,m = solve_with_iterative_deepening( MyGame, ai_depths=range(2,20), win_score=100)
109 |
110 | Note that the first argument can be either a game instance or a game class. We obtain ``r=1``, meaning that if both players play perfectly, the first player to play can always win (-1 would have meant always lose), ``d=10``, which means that the wins will be in ten moves (i.e. 5 moves per player) or less, and ``m='3'``, which indicates that the first player's first move should be ``'3'``.
111 |
112 |
113 | **solve_with_depth_first_search** solves a game using a depth-first search (therefore it cannot be used for games that can have an infinite number of moves). The game is explored until endgames are reached and these endgames are evaluated to see if their are victories or defeats (or draws). Then, a situation in which every move leads to a defeat is labelled as a (certain) defeat, and a situation in which one move leads to a (certain) defeat of the opponent is labelled as a (certain) victory. This way we come back up to the root (initial condition) which receives a label, which is returned. ::
114 |
115 | from easyAI import solve_with_depth_first_search
116 | game = MyGame(players = [... , ...]) # the players are not important
117 | tt = TranspositionTable() # optional, will speed up the algo
118 | r = solve_with_depth_first_search(game, winscore= 90, tt = tt)
119 |
120 | After this ``r`` is either -1 (certain defeat of the first player against a perfect opponent), 0 (it is possible to force a draw, but not to win), or 1 (certain victory if the first player plays perfectly).
121 |
122 |
123 |
--------------------------------------------------------------------------------
/docs/Makefile:
--------------------------------------------------------------------------------
1 | # Makefile for Sphinx documentation
2 | #
3 |
4 | # You can set these variables from the command line.
5 | SPHINXOPTS = -E
6 | SPHINXBUILD = sphinx-build
7 | PAPER =
8 | BUILDDIR = ../../docs
9 | PDFBUILDDIR = /tmp
10 | PDF = ../manual.pdf
11 |
12 | # User-friendly check for sphinx-build
13 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1)
14 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/)
15 | endif
16 |
17 | # Internal variables.
18 | PAPEROPT_a4 = -D latex_paper_size=a4
19 | PAPEROPT_letter = -D latex_paper_size=letter
20 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source
21 | # the i18n builder cannot share the environment and doctrees with the others
22 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source
23 |
24 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext
25 |
26 | help:
27 | @echo "Please use \`make ' where is one of"
28 | @echo " html to make standalone HTML files"
29 | @echo " dirhtml to make HTML files named index.html in directories"
30 | @echo " singlehtml to make a single large HTML file"
31 | @echo " pickle to make pickle files"
32 | @echo " json to make JSON files"
33 | @echo " htmlhelp to make HTML files and a HTML help project"
34 | @echo " qthelp to make HTML files and a qthelp project"
35 | @echo " devhelp to make HTML files and a Devhelp project"
36 | @echo " epub to make an epub"
37 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
38 | @echo " latexpdf to make LaTeX files and run them through pdflatex"
39 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx"
40 | @echo " text to make text files"
41 | @echo " man to make manual pages"
42 | @echo " texinfo to make Texinfo files"
43 | @echo " info to make Texinfo files and run them through makeinfo"
44 | @echo " gettext to make PO message catalogs"
45 | @echo " changes to make an overview of all changed/added/deprecated items"
46 | @echo " xml to make Docutils-native XML files"
47 | @echo " pseudoxml to make pseudoxml-XML files for display purposes"
48 | @echo " linkcheck to check all external links for integrity"
49 | @echo " doctest to run all doctests embedded in the documentation (if enabled)"
50 |
51 | clean:
52 | rm -rf $(BUILDDIR)/*
53 |
54 | html:
55 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
56 | @echo
57 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
58 |
59 | dirhtml:
60 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
61 | @echo
62 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
63 |
64 | singlehtml:
65 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
66 | @echo
67 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
68 |
69 | pickle:
70 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
71 | @echo
72 | @echo "Build finished; now you can process the pickle files."
73 |
74 | json:
75 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
76 | @echo
77 | @echo "Build finished; now you can process the JSON files."
78 |
79 | htmlhelp:
80 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
81 | @echo
82 | @echo "Build finished; now you can run HTML Help Workshop with the" \
83 | ".hhp project file in $(BUILDDIR)/htmlhelp."
84 |
85 | qthelp:
86 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
87 | @echo
88 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \
89 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:"
90 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/easyAI.qhcp"
91 | @echo "To view the help file:"
92 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/easyAI.qhc"
93 |
94 | devhelp:
95 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
96 | @echo
97 | @echo "Build finished."
98 | @echo "To view the help file:"
99 | @echo "# mkdir -p $$HOME/.local/share/devhelp/easyAI"
100 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/easyAI"
101 | @echo "# devhelp"
102 |
103 | epub:
104 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
105 | @echo
106 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub."
107 |
108 | latex:
109 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
110 | @echo
111 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
112 | @echo "Run \`make' in that directory to run these through (pdf)latex" \
113 | "(use \`make latexpdf' here to do that automatically)."
114 |
115 | latexpdf:
116 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
117 | @echo "Running LaTeX files through pdflatex..."
118 | $(MAKE) -C $(BUILDDIR)/latex all-pdf
119 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
120 |
121 | latexpdfja:
122 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
123 | @echo "Running LaTeX files through platex and dvipdfmx..."
124 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja
125 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
126 |
127 | text:
128 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
129 | @echo
130 | @echo "Build finished. The text files are in $(BUILDDIR)/text."
131 |
132 | man:
133 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
134 | @echo
135 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man."
136 |
137 | texinfo:
138 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
139 | @echo
140 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
141 | @echo "Run \`make' in that directory to run these through makeinfo" \
142 | "(use \`make info' here to do that automatically)."
143 |
144 | info:
145 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
146 | @echo "Running Texinfo files through makeinfo..."
147 | make -C $(BUILDDIR)/texinfo info
148 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
149 |
150 | gettext:
151 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
152 | @echo
153 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
154 |
155 | changes:
156 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
157 | @echo
158 | @echo "The overview file is in $(BUILDDIR)/changes."
159 |
160 | linkcheck:
161 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
162 | @echo
163 | @echo "Link check complete; look for any errors in the above output " \
164 | "or in $(BUILDDIR)/linkcheck/output.txt."
165 |
166 | doctest:
167 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
168 | @echo "Testing of doctests in the sources finished, look at the " \
169 | "results in $(BUILDDIR)/doctest/output.txt."
170 |
171 | xml:
172 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml
173 | @echo
174 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml."
175 |
176 | pseudoxml:
177 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml
178 | @echo
179 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml."
180 |
--------------------------------------------------------------------------------
/docs/source/speedup.rst:
--------------------------------------------------------------------------------
1 | .. _speedup:
2 |
3 | How To Make The AI Faster
4 | ==========================
5 |
6 | EasyAI has been written with clarity/simplicity and in mind, rather than speed. In this section we will see how to make the AI run faster with a few refinements in the way the game is defined.
7 |
8 | - Profile your code !
9 | - Optimize (avoid recomputing things that have already been computed).
10 | - For the most computer-intensive parts use fast libraries or code in C/Cython.
11 |
12 | Let's play Connect 4
13 | ----------------------
14 |
15 | So let's start. We want to play *Connect 4* (rules_) against the computer.
16 |
17 | .. image:: http://upload.wikimedia.org/wikipedia/commons/a/ad/Connect_Four.gif
18 | :align: center
19 |
20 | I chose this game because the implementation given in easyAI (see section :ref:`connect4`) is really not optimized: we will show that it can be sped up 25 times.
21 |
22 | We load the game and start a match, with the following code: ::
23 |
24 | from easyAI.games import ConnectFour
25 | from easyAI import Human_Player, AI_Player, Negamax
26 |
27 | ai = Negamax(7) # AI thinks 7 moves in advance
28 | game = ConnectFour( [ AI_Player(ai), Human_Player() ])
29 | game.play()
30 |
31 | We launch the script, and the computer plays after nine seconds... nine seconds !!! I mean, even in the 1990s it was considered slow ! So let's see what we can do about that.
32 |
33 | First we profile the AI with: ::
34 |
35 | import cProfile
36 | cProfile.run("game.play(1)") # play one move, stop the game, profile
37 |
38 | This will print every function used to play the first move, and the time taken by each function.
39 |
40 | Cythonize what you can
41 | -----------------------
42 | The results of ``cProfile`` are clear: 6.7 of the 9 seconds are spent computing the function ``find_four``, which checks whether there are 4 connected pieces in the board. This is not the kind of function that python likes (many ``for`` and ``while`` loops) so we will rewrite this function in a faster language. You can write it in C and link it to python, but for this example I prefer to write it in Cython, because it integrates well with Python and the iPython Notebook (:download:`Cython code `). After the function is rewritten, we run again ::
43 |
44 | import cProfile
45 | cProfile.run("game.play(1)") # play one move and profile
46 |
47 | Now it takes 2.7 seconds !
48 |
49 | Use ``unmake_move``
50 | --------------------
51 |
52 | So what is the next bottleneck ? Apparently the function ``deepcopy`` is called a lot and takes a total of 1.4 seconds.
53 |
54 | This problem is very much linked with the way easyAI works: when the AI thinks a few moves in advances, it creates whole copies of the entire game, on which it can experiment. In our case the AI has created 35000 copies of the game, no wonder it was slow.
55 |
56 | A better solution is to perform a move directly on the original game, and once the move is evaluated, undo the move and continue with the same game. This is very easy to do with easyAI, all we have to do is to add a method called ``unmake_move`` to the class ConnectFour, which explains how to cancel a given move: ::
57 |
58 | def unmake_move(game, column):
59 | """ Unmake a move by removing the last piece of the column """
60 | line = (6 if (game.board[:, column].min()>0) else
61 | np.argmin( game.board[:, column] != 0) )
62 | game.board[line-1, column] = 0
63 |
64 | Now as expected ``cProfile`` tells us that our program runs in 1.2 seconds.
65 | Note that for some games *undoing* a move knowing just the move is not easy (sometimes it is even impossible).
66 |
67 | Don't look twice if you have lost
68 | ----------------------------------
69 |
70 | Now the function ``Connect4.lose()`` is responsible for half of the duration. This is the method which calls ``find_four``, to see if the opponent has won. ``Connect4.lose()`` is really not called efficiently: first the AI checks if the player has lost with ``Connect4.lose()``, in order to know if the game is over (method `is_over`), then it computes a score, and for this it calls the function `scoring`, which will also call ``Connect4.lose()``.
71 |
72 | It would be better if ``is_over`` could directly tell to ``scoring`` something like *we have lost, I already checked*. So let's rewrite these two functions: ::
73 |
74 | def lose(self):
75 | """ You lose if your opponent has four 'connected' pieces """
76 | self.haslost = find_four(self.board,self.opponent_index) # store result
77 | return self.haslost
78 |
79 | def scoring(game):
80 | if game.haslost !=None:
81 | haslost = game.haslost # use the stored result of ``lose``.
82 | game.haslost = None
83 | return -100 if haslost else 0
84 | else:
85 | return -100 if game.lose() else 0
86 |
87 | Now that ``Connect4.lose()`` is called less our program runs in 0.74 seconds.
88 |
89 | Use transposition tables
90 | ------------------------
91 |
92 | Transposition tables store the values of already-computed moves and positions so that if the AI meets them again it will win time. To use such tables is very easy. First you need to tell easyAI how to represent a game in a simple form (a string or a tuple) to use as a key when you store the game in the table. In our example, the game will be represented by a string of 42 caracters indicating whether the different positions on the board are occupied by player 1, by player 2, or just empty. ::
93 |
94 | def ttentry(self):
95 | return "".join([".0X"[i] for i in self.board.flatten()])
96 |
97 | Then you simply tell the AI that you want to use transposition tables: ::
98 |
99 | from easyAI import TranspositionTable
100 | ai = Negamax(7, scoring, tt = TranspositionTable())
101 |
102 | The AI now runs in **0.4 seconds !**
103 |
104 | Transposition tables become more advantageous when you are thinking many moves in advance: Negamax(10) takes 2.4 seconds with transposition tables, and 9.4 second without (for Connect 4 it is known that the tables help the AI a lot. In some other games they might be useless).
105 |
106 | Solve the game first
107 | --------------------
108 |
109 | Not all games are solvable. But if it is possible to fully solve a game, you could solve it first, then store the results for use in your program. Using the GameOfBones example ::
110 |
111 | tt = TranspositionTable()
112 | GameOfBones.ttentry = lambda game : game.pile # key for the table
113 | r,d,m = solve_with_iterative_deepening(GameOfBones, range(2,20), win_score=100, tt=tt)
114 |
115 | After these lines are run the variable ``tt`` contains a transposition table storing the possible situations (here, the possible sizes of the pile) and the optimal moves to perform. With ``tt`` you can play perfectly without *thinking*: ::
116 |
117 | game = GameOfBones( [ AI_Player( tt ), Human_Player() ] )
118 | game.play() # you will always lose this game :)
119 |
120 | One could save the solved transposition table to a file using a python library such as ``pickle``. Then, your program need not recalculate the solution every time it starts. Instead it simply reads the saved tranposition table.
121 |
122 | Conclusion
123 | -----------
124 |
125 | Now there are no obvious ways to gain significant speed. Maybe speeding up Python in general by using an optimized compiler like PyPy could help win a few more percents. But if you really want a fast and smart AI you may also consider other strategies like mixing algortihms, using opening books, etc.
126 |
127 | .. _rules :
128 | .. _here :
129 |
--------------------------------------------------------------------------------
/docs/make.bat:
--------------------------------------------------------------------------------
1 | @ECHO OFF
2 |
3 | REM Command file for Sphinx documentation
4 |
5 | if "%SPHINXBUILD%" == "" (
6 | set SPHINXBUILD=sphinx-build
7 | )
8 | set BUILDDIR=build
9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% source
10 | set I18NSPHINXOPTS=%SPHINXOPTS% source
11 | if NOT "%PAPER%" == "" (
12 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS%
13 | set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS%
14 | )
15 |
16 | if "%1" == "" goto help
17 |
18 | if "%1" == "help" (
19 | :help
20 | echo.Please use `make ^` where ^ is one of
21 | echo. html to make standalone HTML files
22 | echo. dirhtml to make HTML files named index.html in directories
23 | echo. singlehtml to make a single large HTML file
24 | echo. pickle to make pickle files
25 | echo. json to make JSON files
26 | echo. htmlhelp to make HTML files and a HTML help project
27 | echo. qthelp to make HTML files and a qthelp project
28 | echo. devhelp to make HTML files and a Devhelp project
29 | echo. epub to make an epub
30 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter
31 | echo. text to make text files
32 | echo. man to make manual pages
33 | echo. texinfo to make Texinfo files
34 | echo. gettext to make PO message catalogs
35 | echo. changes to make an overview over all changed/added/deprecated items
36 | echo. xml to make Docutils-native XML files
37 | echo. pseudoxml to make pseudoxml-XML files for display purposes
38 | echo. linkcheck to check all external links for integrity
39 | echo. doctest to run all doctests embedded in the documentation if enabled
40 | goto end
41 | )
42 |
43 | if "%1" == "clean" (
44 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i
45 | del /q /s %BUILDDIR%\*
46 | goto end
47 | )
48 |
49 |
50 | %SPHINXBUILD% 2> nul
51 | if errorlevel 9009 (
52 | echo.
53 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
54 | echo.installed, then set the SPHINXBUILD environment variable to point
55 | echo.to the full path of the 'sphinx-build' executable. Alternatively you
56 | echo.may add the Sphinx directory to PATH.
57 | echo.
58 | echo.If you don't have Sphinx installed, grab it from
59 | echo.http://sphinx-doc.org/
60 | exit /b 1
61 | )
62 |
63 | if "%1" == "html" (
64 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html
65 | if errorlevel 1 exit /b 1
66 | echo.
67 | echo.Build finished. The HTML pages are in %BUILDDIR%/html.
68 | goto end
69 | )
70 |
71 | if "%1" == "dirhtml" (
72 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml
73 | if errorlevel 1 exit /b 1
74 | echo.
75 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml.
76 | goto end
77 | )
78 |
79 | if "%1" == "singlehtml" (
80 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml
81 | if errorlevel 1 exit /b 1
82 | echo.
83 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml.
84 | goto end
85 | )
86 |
87 | if "%1" == "pickle" (
88 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle
89 | if errorlevel 1 exit /b 1
90 | echo.
91 | echo.Build finished; now you can process the pickle files.
92 | goto end
93 | )
94 |
95 | if "%1" == "json" (
96 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json
97 | if errorlevel 1 exit /b 1
98 | echo.
99 | echo.Build finished; now you can process the JSON files.
100 | goto end
101 | )
102 |
103 | if "%1" == "htmlhelp" (
104 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp
105 | if errorlevel 1 exit /b 1
106 | echo.
107 | echo.Build finished; now you can run HTML Help Workshop with the ^
108 | .hhp project file in %BUILDDIR%/htmlhelp.
109 | goto end
110 | )
111 |
112 | if "%1" == "qthelp" (
113 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp
114 | if errorlevel 1 exit /b 1
115 | echo.
116 | echo.Build finished; now you can run "qcollectiongenerator" with the ^
117 | .qhcp project file in %BUILDDIR%/qthelp, like this:
118 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\easyAI.qhcp
119 | echo.To view the help file:
120 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\easyAI.ghc
121 | goto end
122 | )
123 |
124 | if "%1" == "devhelp" (
125 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp
126 | if errorlevel 1 exit /b 1
127 | echo.
128 | echo.Build finished.
129 | goto end
130 | )
131 |
132 | if "%1" == "epub" (
133 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub
134 | if errorlevel 1 exit /b 1
135 | echo.
136 | echo.Build finished. The epub file is in %BUILDDIR%/epub.
137 | goto end
138 | )
139 |
140 | if "%1" == "latex" (
141 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
142 | if errorlevel 1 exit /b 1
143 | echo.
144 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex.
145 | goto end
146 | )
147 |
148 | if "%1" == "latexpdf" (
149 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
150 | cd %BUILDDIR%/latex
151 | make all-pdf
152 | cd %BUILDDIR%/..
153 | echo.
154 | echo.Build finished; the PDF files are in %BUILDDIR%/latex.
155 | goto end
156 | )
157 |
158 | if "%1" == "latexpdfja" (
159 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
160 | cd %BUILDDIR%/latex
161 | make all-pdf-ja
162 | cd %BUILDDIR%/..
163 | echo.
164 | echo.Build finished; the PDF files are in %BUILDDIR%/latex.
165 | goto end
166 | )
167 |
168 | if "%1" == "text" (
169 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text
170 | if errorlevel 1 exit /b 1
171 | echo.
172 | echo.Build finished. The text files are in %BUILDDIR%/text.
173 | goto end
174 | )
175 |
176 | if "%1" == "man" (
177 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man
178 | if errorlevel 1 exit /b 1
179 | echo.
180 | echo.Build finished. The manual pages are in %BUILDDIR%/man.
181 | goto end
182 | )
183 |
184 | if "%1" == "texinfo" (
185 | %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo
186 | if errorlevel 1 exit /b 1
187 | echo.
188 | echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo.
189 | goto end
190 | )
191 |
192 | if "%1" == "gettext" (
193 | %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale
194 | if errorlevel 1 exit /b 1
195 | echo.
196 | echo.Build finished. The message catalogs are in %BUILDDIR%/locale.
197 | goto end
198 | )
199 |
200 | if "%1" == "changes" (
201 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes
202 | if errorlevel 1 exit /b 1
203 | echo.
204 | echo.The overview file is in %BUILDDIR%/changes.
205 | goto end
206 | )
207 |
208 | if "%1" == "linkcheck" (
209 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck
210 | if errorlevel 1 exit /b 1
211 | echo.
212 | echo.Link check complete; look for any errors in the above output ^
213 | or in %BUILDDIR%/linkcheck/output.txt.
214 | goto end
215 | )
216 |
217 | if "%1" == "doctest" (
218 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest
219 | if errorlevel 1 exit /b 1
220 | echo.
221 | echo.Testing of doctests in the sources finished, look at the ^
222 | results in %BUILDDIR%/doctest/output.txt.
223 | goto end
224 | )
225 |
226 | if "%1" == "xml" (
227 | %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml
228 | if errorlevel 1 exit /b 1
229 | echo.
230 | echo.Build finished. The XML files are in %BUILDDIR%/xml.
231 | goto end
232 | )
233 |
234 | if "%1" == "pseudoxml" (
235 | %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml
236 | if errorlevel 1 exit /b 1
237 | echo.
238 | echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml.
239 | goto end
240 | )
241 |
242 | :end
243 |
--------------------------------------------------------------------------------
/easyAI/AI/NonRecursiveNegamax.py:
--------------------------------------------------------------------------------
1 | """The standard AI algorithm of easyAI is Negamax with alpha-beta pruning.
2 | This version does not use recursion. It also does not support transposition
3 | tables, but it does REQUIRE the `tt_entry` method in the game.
4 |
5 | It does not make use of 'unmake_move', though having it will not cause a
6 | problem.
7 |
8 | It also requires a reverse function: 'ttrestore' that takes the value from
9 | 'ttentry' and restores the game state.
10 | """
11 |
12 | import copy
13 |
14 | LOWERBOUND, EXACT, UPPERBOUND = -1, 0, 1
15 |
16 | INF = float("infinity")
17 |
18 | # integer keys for 'state':
19 | IMAGE = 0
20 | MOVE_LIST = 1
21 | CURRENT_MOVE = 2
22 | BEST_MOVE = 3
23 | BEST_SCORE = 4
24 | PLAYER = 5
25 | ALPHA = 6
26 | BETA = 7
27 |
28 | DOWN = 1
29 | UP = 2
30 |
31 |
32 | class StateObject(object):
33 | def __init__(self):
34 | self.image = None
35 | self.move_list = []
36 | self.current_move = 0
37 | self.best_move = 0
38 | self.best_score = -INF
39 | self.player = None
40 | self.alpha = -INF
41 | self.beta = INF
42 |
43 | def prune(self):
44 | index = self.current_move + 1
45 | self.move_list = self.move_list[0:index]
46 |
47 | def out_of_moves(self):
48 | """ we are at or past the end of the move list """
49 | return self.current_move >= len(self.move_list) - 1
50 |
51 | def goto_next_move(self):
52 | self.current_move += 1
53 | return self.move_list[self.current_move]
54 |
55 | def swap_alpha_beta(self):
56 | (self.alpha, self.beta) = (self.beta, self.alpha)
57 |
58 |
59 | class StateList(object):
60 | def __init__(self, target_depth):
61 | self.state_list = [StateObject() for _ in range(target_depth + 2)]
62 |
63 | def __getitem__(self, key):
64 | return self.state_list[key + 1]
65 |
66 |
67 | def negamax_nr(game, target_depth, scoring, alpha=-INF, beta=+INF):
68 |
69 | ################################################
70 | #
71 | # INITIALIZE AND CHECK ENTRY CONDITIONS
72 | #
73 | ################################################
74 |
75 | if not hasattr(game, "ttentry"):
76 | raise AttributeError('Method "ttentry()" missing from game.')
77 | if not hasattr(game, "ttrestore"):
78 | raise AttributeError('Method "ttrestore()" missing from game.')
79 |
80 | if game.is_over():
81 | score = scoring(game)
82 | game.ai_move = None
83 | return score
84 |
85 | if target_depth == 0:
86 | current_game = game.ttentry()
87 | move_list = game.possible_moves()
88 | best_move = None
89 | best_score = -INF
90 | for move in move_list:
91 | game.make_move(move)
92 | score = scoring(game)
93 | if score > best_score:
94 | best_move = copy.copy(move)
95 | best_score = score
96 | game.ttrestore(current_game)
97 | game.ai_move = best_move
98 | return best_score
99 |
100 | states = StateList(target_depth)
101 |
102 | ################################################
103 | #
104 | # START GRAND LOOP
105 | #
106 | ################################################
107 |
108 | depth = -1 # proto-parent
109 | states[depth].alpha = alpha
110 | states[depth].beta = beta
111 | direction = DOWN
112 | depth = 0
113 |
114 | while True:
115 | parent = depth - 1
116 | if direction == DOWN:
117 | if (depth < target_depth) and not game.is_over(): # down we go...
118 | states[depth].image = game.ttentry()
119 | states[depth].move_list = game.possible_moves()
120 | states[depth].best_move = 0
121 | states[depth].best_score = -INF
122 | states[depth].current_move = 0
123 | states[depth].player = game.current_player
124 | states[depth].alpha = -states[parent].beta # inherit alpha from -beta
125 | states[depth].beta = -states[parent].alpha # inherit beta from -alpha
126 | index = states[depth].current_move
127 | game.make_move(states[depth].move_list[index])
128 | game.switch_player()
129 | direction = DOWN
130 | depth += 1
131 | else: # reached a leaf or the game is over; going back up
132 | leaf_score = -scoring(game)
133 | if leaf_score > states[parent].best_score:
134 | states[parent].best_score = leaf_score
135 | states[parent].best_move = states[parent].current_move
136 | if states[parent].alpha < leaf_score:
137 | states[parent].alpha = leaf_score
138 | direction = UP
139 | depth = parent
140 | continue
141 | elif direction == UP:
142 | prune_time = states[depth].alpha >= states[depth].beta
143 | if states[depth].out_of_moves() or prune_time: # out of moves
144 | bs = -states[depth].best_score
145 | if bs > states[parent].best_score:
146 | states[parent].best_score = bs
147 | states[parent].best_move = states[parent].current_move
148 | if states[parent].alpha < bs:
149 | states[parent].alpha = bs
150 | if depth <= 0:
151 | break # we are done.
152 | direction = UP
153 | depth = parent
154 | continue
155 | # else go down the next branch
156 | game.ttrestore(states[depth].image)
157 | game.current_player = states[depth].player
158 | next_move = states[depth].goto_next_move()
159 | game.make_move(next_move)
160 | game.switch_player()
161 | direction = DOWN
162 | depth += 1
163 |
164 | best_move_index = states[0].best_move
165 | best_move = states[0].move_list[best_move_index]
166 | best_value = states[0].best_score
167 | game.ai_move = best_move
168 | return best_value
169 |
170 |
171 | class NonRecursiveNegamax:
172 | """
173 | This implements Negamax without recursion. The following example shows
174 | how to setup the AI and play a Connect Four game:
175 |
176 | >>> from easyAI.games import ConnectFour
177 | >>> from easyAI import NonRecursiveNegamax, Human_Player, AI_Player
178 | >>> scoring = lambda game: -100 if game.lose() else 0
179 | >>> ai_algo = NonRecursiveNegamax(8, scoring) # AI will think 8 turns in advance
180 | >>> game = ConnectFour([Human_Player(), AI_Player(ai_algo)])
181 | >>> game.play()
182 |
183 | This algorithm also *REQUIRES* that the game class support the ``ttentry`` and
184 | ``ttrestore`` methods.
185 |
186 | This algorithm ignores any optional ``unmake_move`` method in the game class.
187 |
188 | This version of Negamax does not support transposition tables.
189 |
190 | Parameters
191 | -----------
192 |
193 | depth:
194 | How many moves in advance should the AI think ?
195 | (2 moves = 1 complete turn)
196 |
197 | scoring:
198 | A function f(game)-> score. If no scoring is provided
199 | and the game object has a ``scoring`` method it will be used.
200 |
201 | win_score:
202 | Score above which the score means a win.
203 |
204 | tt:
205 | A transposition table (a table storing game states and moves). Currently,
206 | this parameter is ignored.
207 |
208 | """
209 |
210 | def __init__(self, depth, scoring=None, win_score=+INF, tt=None):
211 | self.scoring = scoring
212 | self.depth = depth
213 | self.tt = tt
214 | self.win_score = win_score
215 |
216 | def __call__(self, game):
217 | """
218 | Returns the AI's best move given the current state of the game.
219 | """
220 | scoring = self.scoring if self.scoring else (lambda g: g.scoring())
221 | temp = game.copy()
222 | self.alpha = negamax_nr(
223 | temp, self.depth, scoring, -self.win_score, +self.win_score
224 | )
225 | return temp.ai_move
226 |
--------------------------------------------------------------------------------
/docs/source/_themes/flask/static/flasky.css_t:
--------------------------------------------------------------------------------
1 | /*
2 | * flasky.css_t
3 | * ~~~~~~~~~~~~
4 | *
5 | * :copyright: Copyright 2010 by Armin Ronacher.
6 | * :license: Flask Design License, see LICENSE for details.
7 | */
8 |
9 | {% set page_width = '940px' %}
10 | {% set sidebar_width = '220px' %}
11 |
12 | @import url("basic.css");
13 |
14 | /* -- page layout ----------------------------------------------------------- */
15 |
16 | body {
17 | font-family: 'Georgia', serif;
18 | font-size: 17px;
19 | background-color: white;
20 | color: #000;
21 | margin: 0;
22 | padding: 0;
23 | }
24 |
25 | div.document {
26 | width: {{ page_width }};
27 | margin: 30px auto 0 auto;
28 | }
29 |
30 | div.documentwrapper {
31 | float: left;
32 | width: 100%;
33 | }
34 |
35 | div.bodywrapper {
36 | margin: 0 0 0 {{ sidebar_width }};
37 | }
38 |
39 | div.sphinxsidebar {
40 | width: {{ sidebar_width }};
41 | }
42 |
43 | hr {
44 | border: 1px solid #B1B4B6;
45 | }
46 |
47 | div.body {
48 | background-color: #ffffff;
49 | color: #3E4349;
50 | padding: 0 30px 0 30px;
51 | }
52 |
53 | img.floatingflask {
54 | padding: 0 0 10px 10px;
55 | float: right;
56 | }
57 |
58 | div.footer {
59 | width: {{ page_width }};
60 | margin: 20px auto 30px auto;
61 | font-size: 14px;
62 | color: #888;
63 | text-align: right;
64 | }
65 |
66 | div.footer a {
67 | color: #888;
68 | }
69 |
70 | div.related {
71 | display: none;
72 | }
73 |
74 | div.sphinxsidebar a {
75 | color: #444;
76 | text-decoration: none;
77 | border-bottom: 1px dotted #999;
78 | }
79 |
80 | div.sphinxsidebar a:hover {
81 | border-bottom: 1px solid #999;
82 | }
83 |
84 | div.sphinxsidebar {
85 | font-size: 14px;
86 | line-height: 1.5;
87 | }
88 |
89 | div.sphinxsidebarwrapper {
90 | padding: 18px 10px;
91 | }
92 |
93 | div.sphinxsidebarwrapper p.logo {
94 | padding: 0 0 20px 0;
95 | margin: 0;
96 | text-align: center;
97 | }
98 |
99 | div.sphinxsidebar h3,
100 | div.sphinxsidebar h4 {
101 | font-family: 'Garamond', 'Georgia', serif;
102 | color: #444;
103 | font-size: 24px;
104 | font-weight: normal;
105 | margin: 0 0 5px 0;
106 | padding: 0;
107 | }
108 |
109 | div.sphinxsidebar h4 {
110 | font-size: 20px;
111 | }
112 |
113 | div.sphinxsidebar h3 a {
114 | color: #444;
115 | }
116 |
117 | div.sphinxsidebar p.logo a,
118 | div.sphinxsidebar h3 a,
119 | div.sphinxsidebar p.logo a:hover,
120 | div.sphinxsidebar h3 a:hover {
121 | border: none;
122 | }
123 |
124 | div.sphinxsidebar p {
125 | color: #555;
126 | margin: 10px 0;
127 | }
128 |
129 | div.sphinxsidebar ul {
130 | margin: 10px 0;
131 | padding: 0;
132 | color: #000;
133 | }
134 |
135 | div.sphinxsidebar input {
136 | border: 1px solid #ccc;
137 | font-family: 'Georgia', serif;
138 | font-size: 1em;
139 | }
140 |
141 | /* -- body styles ----------------------------------------------------------- */
142 |
143 | a {
144 | color: #004B6B;
145 | text-decoration: underline;
146 | }
147 |
148 | a:hover {
149 | color: #6D4100;
150 | text-decoration: underline;
151 | }
152 |
153 | div.body h1,
154 | div.body h2,
155 | div.body h3,
156 | div.body h4,
157 | div.body h5,
158 | div.body h6 {
159 | font-family: 'Garamond', 'Georgia', serif;
160 | font-weight: normal;
161 | margin: 30px 0px 10px 0px;
162 | padding: 0;
163 | }
164 |
165 | {% if theme_index_logo %}
166 | div.indexwrapper h1 {
167 | text-indent: -999999px;
168 | background: url({{ theme_index_logo }}) no-repeat center center;
169 | height: {{ theme_index_logo_height }};
170 | }
171 | {% endif %}
172 |
173 | div.body h1 { margin-top: 0; padding-top: 0; font-size: 240%; }
174 | div.body h2 { font-size: 180%; }
175 | div.body h3 { font-size: 150%; }
176 | div.body h4 { font-size: 130%; }
177 | div.body h5 { font-size: 100%; }
178 | div.body h6 { font-size: 100%; }
179 |
180 | a.headerlink {
181 | color: #ddd;
182 | padding: 0 4px;
183 | text-decoration: none;
184 | }
185 |
186 | a.headerlink:hover {
187 | color: #444;
188 | background: #eaeaea;
189 | }
190 |
191 | div.body p, div.body dd, div.body li {
192 | line-height: 1.4em;
193 | }
194 |
195 | div.admonition {
196 | background: #fafafa;
197 | margin: 20px -30px;
198 | padding: 10px 30px;
199 | border-top: 1px solid #ccc;
200 | border-bottom: 1px solid #ccc;
201 | }
202 |
203 | div.admonition tt.xref, div.admonition a tt {
204 | border-bottom: 1px solid #fafafa;
205 | }
206 |
207 | dd div.admonition {
208 | margin-left: -60px;
209 | padding-left: 60px;
210 | }
211 |
212 | div.admonition p.admonition-title {
213 | font-family: 'Garamond', 'Georgia', serif;
214 | font-weight: normal;
215 | font-size: 24px;
216 | margin: 0 0 10px 0;
217 | padding: 0;
218 | line-height: 1;
219 | }
220 |
221 | div.admonition p.last {
222 | margin-bottom: 0;
223 | }
224 |
225 | div.highlight {
226 | background-color: white;
227 | }
228 |
229 | dt:target, .highlight {
230 | background: #FAF3E8;
231 | }
232 |
233 | div.note {
234 | background-color: #eee;
235 | border: 1px solid #ccc;
236 | }
237 |
238 | div.seealso {
239 | background-color: #ffc;
240 | border: 1px solid #ff6;
241 | }
242 |
243 | div.topic {
244 | background-color: #eee;
245 | }
246 |
247 | p.admonition-title {
248 | display: inline;
249 | }
250 |
251 | p.admonition-title:after {
252 | content: ":";
253 | }
254 |
255 | pre, tt {
256 | font-family: 'Consolas', 'Menlo', 'Deja Vu Sans Mono', 'Bitstream Vera Sans Mono', monospace;
257 | font-size: 0.9em;
258 | }
259 |
260 | img.screenshot {
261 | }
262 |
263 | tt.descname, tt.descclassname {
264 | font-size: 0.95em;
265 | }
266 |
267 | tt.descname {
268 | padding-right: 0.08em;
269 | }
270 |
271 | img.screenshot {
272 | -moz-box-shadow: 2px 2px 4px #eee;
273 | -webkit-box-shadow: 2px 2px 4px #eee;
274 | box-shadow: 2px 2px 4px #eee;
275 | }
276 |
277 | table.docutils {
278 | border: 1px solid #888;
279 | -moz-box-shadow: 2px 2px 4px #eee;
280 | -webkit-box-shadow: 2px 2px 4px #eee;
281 | box-shadow: 2px 2px 4px #eee;
282 | }
283 |
284 | table.docutils td, table.docutils th {
285 | border: 1px solid #888;
286 | padding: 0.25em 0.7em;
287 | }
288 |
289 | table.field-list, table.footnote {
290 | border: none;
291 | -moz-box-shadow: none;
292 | -webkit-box-shadow: none;
293 | box-shadow: none;
294 | }
295 |
296 | table.footnote {
297 | margin: 15px 0;
298 | width: 100%;
299 | border: 1px solid #eee;
300 | background: #fdfdfd;
301 | font-size: 0.9em;
302 | }
303 |
304 | table.footnote + table.footnote {
305 | margin-top: -15px;
306 | border-top: none;
307 | }
308 |
309 | table.field-list th {
310 | padding: 0 0.8em 0 0;
311 | }
312 |
313 | table.field-list td {
314 | padding: 0;
315 | }
316 |
317 | table.footnote td.label {
318 | width: 0px;
319 | padding: 0.3em 0 0.3em 0.5em;
320 | }
321 |
322 | table.footnote td {
323 | padding: 0.3em 0.5em;
324 | }
325 |
326 | dl {
327 | margin: 0;
328 | padding: 0;
329 | }
330 |
331 | dl dd {
332 | margin-left: 30px;
333 | }
334 |
335 | blockquote {
336 | margin: 0 0 0 30px;
337 | padding: 0;
338 | }
339 |
340 | ul, ol {
341 | margin: 10px 0 10px 30px;
342 | padding: 0;
343 | }
344 |
345 | pre {
346 | background: #eee;
347 | padding: 7px 30px;
348 | margin: 15px -30px;
349 | line-height: 1.3em;
350 | }
351 |
352 | dl pre, blockquote pre, li pre {
353 | margin-left: -60px;
354 | padding-left: 60px;
355 | }
356 |
357 | dl dl pre {
358 | margin-left: -90px;
359 | padding-left: 90px;
360 | }
361 |
362 | tt {
363 | background-color: #ecf0f3;
364 | color: #222;
365 | /* padding: 1px 2px; */
366 | }
367 |
368 | tt.xref, a tt {
369 | background-color: #FBFBFB;
370 | border-bottom: 1px solid white;
371 | }
372 |
373 | a.reference {
374 | text-decoration: none;
375 | border-bottom: 1px dotted #004B6B;
376 | }
377 |
378 | a.reference:hover {
379 | border-bottom: 1px solid #6D4100;
380 | }
381 |
382 | a.footnote-reference {
383 | text-decoration: none;
384 | font-size: 0.7em;
385 | vertical-align: top;
386 | border-bottom: 1px dotted #004B6B;
387 | }
388 |
389 | a.footnote-reference:hover {
390 | border-bottom: 1px solid #6D4100;
391 | }
392 |
393 | a:hover tt {
394 | background: #EEE;
395 | }
396 |
--------------------------------------------------------------------------------
/docs/source/conf.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | #
3 | # easyAI documentation build configuration file, created by
4 | # sphinx-quickstart on Sat Dec 14 13:19:53 2013.
5 | #
6 | # This file is execfile()d with the current directory set to its containing dir.
7 | #
8 | # Note that not all possible configuration values are present in this
9 | # autogenerated file.
10 | #
11 | # All configuration values have a default; values that are commented out
12 | # serve to show the default.
13 |
14 | import sys
15 | import os
16 |
17 | # If extensions (or modules to document with autodoc) are in another directory,
18 | # add these directories to sys.path here. If the directory is relative to the
19 | # documentation root, use os.path.abspath to make it absolute, like shown here.
20 | # sys.path.insert(0, os.path.abspath('.'))
21 |
22 | # -- General configuration -----------------------------------------------------
23 |
24 | # If your documentation needs a minimal Sphinx version, state it here.
25 | # needs_sphinx = '1.0'
26 |
27 | # Add any Sphinx extension module names here, as strings. They can be extensions
28 | # coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
29 | extensions = ['sphinx.ext.autodoc', 'sphinx.ext.mathjax', 'sphinx.ext.viewcode', 'numpydoc']
30 | numpydoc_show_class_members = False
31 |
32 | # Add any paths that contain templates here, relative to this directory.
33 | templates_path = ['_templates']
34 |
35 | # The suffix of source filenames.
36 | source_suffix = '.rst'
37 |
38 | # The encoding of source files.
39 | # source_encoding = 'utf-8-sig'
40 |
41 | # The master toctree document.
42 | master_doc = 'index'
43 |
44 | # General information about the project.
45 | project = u'easyAI'
46 | copyright = u'2014-2017, Zulko'
47 |
48 | # The version info for the project you're documenting, acts as replacement for
49 | # |version| and |release|, also used in various other places throughout the
50 | # built documents.
51 | #
52 | # The short X.Y version.
53 | version = '1.0.0.4'
54 | # The full version, including alpha/beta/rc tags.
55 | release = '1.0.0.4'
56 |
57 | # The language for content autogenerated by Sphinx. Refer to documentation
58 | # for a list of supported languages.
59 | #language = None
60 |
61 | # There are two options for replacing |today|: either, you set today to some
62 | # non-false value, then it is used:
63 | #today = ''
64 | # Else, today_fmt is used as the format for a strftime call.
65 | #today_fmt = '%B %d, %Y'
66 |
67 | # List of patterns, relative to source directory, that match files and
68 | # directories to ignore when looking for source files.
69 | exclude_patterns = []
70 |
71 | # The reST default role (used for this markup: `text`) to use for all documents.
72 | #default_role = None
73 |
74 | # If true, '()' will be appended to :func: etc. cross-reference text.
75 | #add_function_parentheses = True
76 |
77 | # If true, the current module name will be prepended to all description
78 | # unit titles (such as .. function::).
79 | #add_module_names = True
80 |
81 | # If true, sectionauthor and moduleauthor directives will be shown in the
82 | # output. They are ignored by default.
83 | #show_authors = False
84 |
85 | # The name of the Pygments (syntax highlighting) style to use.
86 | pygments_style = 'sphinx'
87 |
88 | # A list of ignored prefixes for module index sorting.
89 | #modindex_common_prefix = []
90 |
91 | # If true, keep warnings as "system message" paragraphs in the built documents.
92 | #keep_warnings = False
93 |
94 |
95 | # -- Options for HTML output ---------------------------------------------------
96 |
97 | # The theme to use for HTML and HTML Help pages. See the documentation for
98 | # a list of builtin themes.
99 | sys.path.append(os.path.abspath('_themes'))
100 | sys.path.append("../../easyAI")
101 | html_theme_path = ['_themes']
102 | html_theme = 'kr'
103 |
104 | # Theme options are theme-specific and customize the look and feel of a theme
105 | # further. For a list of options available for each theme, see the
106 | # documentation.
107 | #html_theme_options = {}
108 |
109 | # Add any paths that contain custom themes here, relative to this directory.
110 | #html_theme_path = []
111 |
112 | # The name for this set of Sphinx documents. If None, it defaults to
113 | # " v documentation".
114 | #html_title = None
115 |
116 | # A shorter title for the navigation bar. Default is the same as html_title.
117 | #html_short_title = None
118 |
119 | # The name of an image file (relative to this directory) to place at the top
120 | # of the sidebar.
121 | html_logo = '_static/logo.jpeg'
122 |
123 | # The name of an image file (within the static path) to use as favicon of the
124 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
125 | # pixels large.
126 | #html_favicon = None
127 |
128 | # Add any paths that contain custom static files (such as style sheets) here,
129 | # relative to this directory. They are copied after the builtin static files,
130 | # so a file named "default.css" will overwrite the builtin "default.css".
131 | html_static_path = ['_static']
132 |
133 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
134 | # using the given strftime format.
135 | #html_last_updated_fmt = '%b %d, %Y'
136 |
137 | # If true, SmartyPants will be used to convert quotes and dashes to
138 | # typographically correct entities.
139 | #html_use_smartypants = True
140 |
141 | # Custom sidebar templates, maps document names to template names.
142 | #html_sidebars = {}
143 |
144 | # Additional templates that should be rendered to pages, maps page names to
145 | # template names.
146 | #html_additional_pages = {}
147 |
148 | # If false, no module index is generated.
149 | #html_domain_indices = True
150 |
151 | # If false, no index is generated.
152 | #html_use_index = True
153 |
154 | # If true, the index is split into individual pages for each letter.
155 | #html_split_index = False
156 |
157 | # If true, links to the reST sources are added to the pages.
158 | #html_show_sourcelink = True
159 |
160 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
161 | #html_show_sphinx = True
162 |
163 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
164 | #html_show_copyright = True
165 |
166 | # If true, an OpenSearch description file will be output, and all pages will
167 | # contain a tag referring to it. The value of this option must be the
168 | # base URL from which the finished HTML is served.
169 | #html_use_opensearch = ''
170 |
171 | # This is the file name suffix for HTML files (e.g. ".xhtml").
172 | #html_file_suffix = None
173 |
174 | # Output file base name for HTML help builder.
175 | htmlhelp_basename = 'easyAIdoc'
176 |
177 |
178 | # -- Options for LaTeX output --------------------------------------------------
179 |
180 | latex_elements = {
181 | # The paper size ('letterpaper' or 'a4paper').
182 | #'papersize': 'letterpaper',
183 |
184 | # The font size ('10pt', '11pt' or '12pt').
185 | #'pointsize': '10pt',
186 |
187 | # Additional stuff for the LaTeX preamble.
188 | #'preamble': '',
189 | }
190 |
191 | # Grouping the document tree into LaTeX files. List of tuples
192 | # (source start file, target name, title, author, documentclass [howto/manual]).
193 | latex_documents = [
194 | ('index', 'easyAI.tex', u'easyAI Documentation',
195 | u'Zulko', 'manual'),
196 | ]
197 |
198 | # The name of an image file (relative to this directory) to place at the top of
199 | # the title page.
200 | #latex_logo = None
201 |
202 | # For "manual" documents, if this is true, then toplevel headings are parts,
203 | # not chapters.
204 | #latex_use_parts = False
205 |
206 | # If true, show page references after internal links.
207 | #latex_show_pagerefs = False
208 |
209 | # If true, show URL addresses after external links.
210 | #latex_show_urls = False
211 |
212 | # Documents to append as an appendix to all manuals.
213 | #latex_appendices = []
214 |
215 | # If false, no module index is generated.
216 | #latex_domain_indices = True
217 |
218 |
219 | # -- Options for manual page output --------------------------------------------
220 |
221 | # One entry per manual page. List of tuples
222 | # (source start file, name, description, authors, manual section).
223 | man_pages = [
224 | ('index', 'easyai', u'easyAI Documentation',
225 | [u'Zulko'], 1)
226 | ]
227 |
228 | # If true, show URL addresses after external links.
229 | #man_show_urls = False
230 |
231 |
232 | # -- Options for Texinfo output ------------------------------------------------
233 |
234 | # Grouping the document tree into Texinfo files. List of tuples
235 | # (source start file, target name, title, author,
236 | # dir menu entry, description, category)
237 | texinfo_documents = [
238 | ('index', 'easyAI', u'easyAI Documentation',
239 | u'Zulko', 'easyAI', 'One line description of project.',
240 | 'Miscellaneous'),
241 | ]
242 |
243 | # Documents to append as an appendix to all manuals.
244 | #texinfo_appendices = []
245 |
246 | # If false, no module index is generated.
247 | #texinfo_domain_indices = True
248 |
249 | # How to display URL addresses: 'footnote', 'no', or 'inline'.
250 | #texinfo_show_urls = 'footnote'
251 |
252 | # If true, do not generate a @detailmenu in the "Top" node's menu.
253 | #texinfo_no_detailmenu = False
254 |
--------------------------------------------------------------------------------
/tests/test_basics.py:
--------------------------------------------------------------------------------
1 | #######################################
2 | #
3 | # TESTING FOR AIs IN EASYAI
4 | #
5 | # To run tests, simply run this script.
6 | #
7 | #######################################
8 |
9 | import unittest
10 |
11 | import easyAI
12 | import easyAI.games as games
13 |
14 |
15 | class Test_Negamax(unittest.TestCase):
16 | def test_play_knights_against_self(self):
17 | ai_algo_K1 = easyAI.Negamax(8)
18 | ai_algo_K2 = easyAI.Negamax(10)
19 | game = games.Knights(
20 | [easyAI.AI_Player(ai_algo_K1), easyAI.AI_Player(ai_algo_K2)]
21 | )
22 | move_list_K1 = []
23 | move_list_K2 = []
24 | while not game.is_over():
25 | move = game.get_move()
26 | if game.current_player == 1:
27 | move_list_K1.append(move)
28 | else:
29 | move_list_K2.append(move)
30 | game.play_move(move)
31 | K1_correct = [
32 | "B3",
33 | "C5",
34 | "D7",
35 | "E5",
36 | "F7",
37 | "G5",
38 | "H7",
39 | "F6",
40 | "G8",
41 | "H6",
42 | "G4",
43 | "H2",
44 | "F3",
45 | "G1",
46 | "H3",
47 | "F2",
48 | "E4",
49 | "D6",
50 | "C8",
51 | "B6",
52 | "C4",
53 | "A3",
54 | "B1",
55 | "D2",
56 | "F1",
57 | "G3",
58 | "H5",
59 | ]
60 | K2_correct = [
61 | "G6",
62 | "F8",
63 | "E6",
64 | "D8",
65 | "C6",
66 | "B8",
67 | "A6",
68 | "B4",
69 | "C2",
70 | "D4",
71 | "E2",
72 | "F4",
73 | "G2",
74 | "H4",
75 | "F5",
76 | "G7",
77 | "E8",
78 | "C7",
79 | "D5",
80 | "E3",
81 | "D1",
82 | "C3",
83 | "A4",
84 | "B2",
85 | "D3",
86 | "C1",
87 | "A2",
88 | ]
89 | self.assertEqual(move_list_K1, K1_correct)
90 | self.assertEqual(move_list_K2, K2_correct)
91 |
92 | # def test_play_awele_against_self(self):
93 | # ai_algo_P1 = easyAI.Negamax(3)
94 | # ai_algo_P2 = easyAI.Negamax(4)
95 | # game = games.AweleTactical(
96 | # [easyAI.AI_Player(ai_algo_P1), easyAI.AI_Player(ai_algo_P2)]
97 | # )
98 | # move_list_P1 = []
99 | # move_list_P2 = []
100 | # while not game.is_over():
101 | # move = game.get_move()
102 | # if game.current_player == 1:
103 | # move_list_P1.append(move)
104 | # else:
105 | # move_list_P2.append(move)
106 | # game.play_move(move)
107 | # P1_correct = [
108 | # "c",
109 | # "e",
110 | # "f",
111 | # "f",
112 | # "a",
113 | # "c",
114 | # "e",
115 | # "f",
116 | # "d",
117 | # "b",
118 | # "c",
119 | # "a",
120 | # "f",
121 | # "d",
122 | # "b",
123 | # "e",
124 | # ]
125 | # P2_correct = [
126 | # "i",
127 | # "j",
128 | # "l",
129 | # "h",
130 | # "g",
131 | # "k",
132 | # "j",
133 | # "i",
134 | # "l",
135 | # "h",
136 | # "j",
137 | # "l",
138 | # "k",
139 | # "l",
140 | # "i",
141 | # "g",
142 | # ]
143 | # self.assertEqual(move_list_P1, P1_correct)
144 | # self.assertEqual(move_list_P2, P2_correct)
145 |
146 |
147 | class Test_NonRecursiveNegamax(unittest.TestCase):
148 | def test_play_knights_against_self(self):
149 | ai_algo_K1 = easyAI.NonRecursiveNegamax(8)
150 | ai_algo_K2 = easyAI.NonRecursiveNegamax(10)
151 | game = games.Knights(
152 | [easyAI.AI_Player(ai_algo_K1), easyAI.AI_Player(ai_algo_K2)]
153 | )
154 | move_list_K1 = []
155 | move_list_K2 = []
156 | while not game.is_over():
157 | move = game.get_move()
158 | if game.current_player == 1:
159 | move_list_K1.append(move)
160 | else:
161 | move_list_K2.append(move)
162 | game.play_move(move)
163 | K1_correct = [
164 | "B3",
165 | "C5",
166 | "D7",
167 | "E5",
168 | "F7",
169 | "G5",
170 | "H7",
171 | "F6",
172 | "G8",
173 | "H6",
174 | "G4",
175 | "H2",
176 | "F3",
177 | "G1",
178 | "H3",
179 | "F2",
180 | "E4",
181 | "D6",
182 | "C8",
183 | "B6",
184 | "C4",
185 | "A3",
186 | "B1",
187 | "D2",
188 | "F1",
189 | "G3",
190 | "H5",
191 | ]
192 | K2_correct = [
193 | "G6",
194 | "F8",
195 | "E6",
196 | "D8",
197 | "C6",
198 | "B8",
199 | "A6",
200 | "B4",
201 | "C2",
202 | "D4",
203 | "E2",
204 | "F4",
205 | "G2",
206 | "H4",
207 | "F5",
208 | "G7",
209 | "E8",
210 | "C7",
211 | "D5",
212 | "E3",
213 | "D1",
214 | "C3",
215 | "A4",
216 | "B2",
217 | "D3",
218 | "C1",
219 | "A2",
220 | ]
221 | self.assertEqual(move_list_K1, K1_correct)
222 | self.assertEqual(move_list_K2, K2_correct)
223 |
224 | # def test_play_awele_against_self(self):
225 | # ai_algo_P1 = easyAI.NonRecursiveNegamax(3)
226 | # ai_algo_P2 = easyAI.NonRecursiveNegamax(4)
227 | # game = games.AweleTactical(
228 | # [easyAI.AI_Player(ai_algo_P1), easyAI.AI_Player(ai_algo_P2)]
229 | # )
230 | # move_list_P1 = []
231 | # move_list_P2 = []
232 | # while not game.is_over():
233 | # move = game.get_move()
234 | # if game.current_player == 1:
235 | # move_list_P1.append(move)
236 | # else:
237 | # move_list_P2.append(move)
238 | # game.play_move(move)
239 | # P1_correct = [
240 | # "c",
241 | # "e",
242 | # "f",
243 | # "f",
244 | # "a",
245 | # "c",
246 | # "e",
247 | # "f",
248 | # "d",
249 | # "b",
250 | # "c",
251 | # "a",
252 | # "f",
253 | # "d",
254 | # "b",
255 | # "e",
256 | # ]
257 | # P2_correct = [
258 | # "i",
259 | # "j",
260 | # "l",
261 | # "h",
262 | # "g",
263 | # "k",
264 | # "j",
265 | # "i",
266 | # "l",
267 | # "h",
268 | # "j",
269 | # "l",
270 | # "k",
271 | # "l",
272 | # "i",
273 | # "g",
274 | # ]
275 | # self.assertEqual(move_list_P1, P1_correct)
276 | # self.assertEqual(move_list_P2, P2_correct)
277 |
278 |
279 | class Test_SSS(unittest.TestCase):
280 | def test_play_knights_against_self(self):
281 | ai_algo_K1 = easyAI.SSS(8)
282 | ai_algo_K2 = easyAI.SSS(10)
283 | game = games.Knights(
284 | [easyAI.AI_Player(ai_algo_K1), easyAI.AI_Player(ai_algo_K2)]
285 | )
286 | move_list_K1 = []
287 | move_list_K2 = []
288 | while not game.is_over():
289 | move = game.get_move()
290 | if game.current_player == 1:
291 | move_list_K1.append(move)
292 | else:
293 | move_list_K2.append(move)
294 | game.play_move(move)
295 | K1_correct = [
296 | "B3",
297 | "C5",
298 | "D7",
299 | "E5",
300 | "F7",
301 | "G5",
302 | "H7",
303 | "F6",
304 | "G8",
305 | "H6",
306 | "G4",
307 | "H2",
308 | "F3",
309 | "G1",
310 | "H3",
311 | "F2",
312 | "E4",
313 | "D6",
314 | "C8",
315 | "B6",
316 | "C4",
317 | "A3",
318 | "B1",
319 | "D2",
320 | "F1",
321 | "G3",
322 | "H5",
323 | ]
324 | K2_correct = [
325 | "G6",
326 | "F8",
327 | "E6",
328 | "D8",
329 | "C6",
330 | "B8",
331 | "A6",
332 | "B4",
333 | "C2",
334 | "D4",
335 | "E2",
336 | "F4",
337 | "G2",
338 | "H4",
339 | "F5",
340 | "G7",
341 | "E8",
342 | "C7",
343 | "D5",
344 | "E3",
345 | "D1",
346 | "C3",
347 | "A4",
348 | "B2",
349 | "D3",
350 | "C1",
351 | "A2",
352 | ]
353 | self.assertEqual(move_list_K1, K1_correct)
354 | self.assertEqual(move_list_K2, K2_correct)
355 |
356 |
357 | class Test_DUAL(unittest.TestCase):
358 | def test_play_knights_against_self(self):
359 | ai_algo_K1 = easyAI.DUAL(8)
360 | ai_algo_K2 = easyAI.DUAL(10)
361 | game = games.Knights(
362 | [easyAI.AI_Player(ai_algo_K1), easyAI.AI_Player(ai_algo_K2)]
363 | )
364 | move_list_K1 = []
365 | move_list_K2 = []
366 | while not game.is_over():
367 | move = game.get_move()
368 | if game.current_player == 1:
369 | move_list_K1.append(move)
370 | else:
371 | move_list_K2.append(move)
372 | game.play_move(move)
373 | K1_correct = [
374 | "B3",
375 | "C5",
376 | "D7",
377 | "E5",
378 | "F7",
379 | "G5",
380 | "H7",
381 | "F6",
382 | "G8",
383 | "H6",
384 | "G4",
385 | "H2",
386 | "F3",
387 | "G1",
388 | "H3",
389 | "F2",
390 | "E4",
391 | "D6",
392 | "C8",
393 | "B6",
394 | "C4",
395 | "A3",
396 | "B1",
397 | "D2",
398 | "F1",
399 | "G3",
400 | "H5",
401 | ]
402 | K2_correct = [
403 | "G6",
404 | "F8",
405 | "E6",
406 | "D8",
407 | "C6",
408 | "B8",
409 | "A6",
410 | "B4",
411 | "C2",
412 | "D4",
413 | "E2",
414 | "F4",
415 | "G2",
416 | "H4",
417 | "F5",
418 | "G7",
419 | "E8",
420 | "C7",
421 | "D5",
422 | "E3",
423 | "D1",
424 | "C3",
425 | "A4",
426 | "B2",
427 | "D3",
428 | "C1",
429 | "A2",
430 | ]
431 | self.assertEqual(move_list_K1, K1_correct)
432 | self.assertEqual(move_list_K2, K2_correct)
433 |
434 |
435 | class Test_TranspositionTable(unittest.TestCase):
436 | def test_pickle_save_and_restore(self):
437 | # 1. solve game/save TranspositionTable
438 | tt = easyAI.TranspositionTable()
439 | winner, depth, best_player_move = easyAI.solve_with_iterative_deepening(
440 | games.Nim(), range(13, 16), tt=tt, win_score=80, verbose=False
441 | )
442 | tt.to_file("tt-data.pickle.temp")
443 | # 2. restore TranspositionTable from file
444 | restored_tt = easyAI.TranspositionTable()
445 | restored_tt.from_file("tt-data.pickle.temp")
446 | # 3. get first AI move using the TranspositionTable
447 | players = [easyAI.Human_Player(), easyAI.AI_Player(restored_tt)]
448 | game = games.Nim(players)
449 | game.play_move(best_player_move) # let the human play
450 | ai_move = game.get_move() # get the AI's move based on tt
451 | self.assertEqual(ai_move, "2,1")
452 | self.assertEqual(best_player_move, "1,1")
453 |
454 | def test_json_save_and_restore(self):
455 | # 1. solve game/save TranspositionTable
456 | tt = easyAI.TranspositionTable()
457 | winner, depth, best_player_move = easyAI.solve_with_iterative_deepening(
458 | games.Nim(), range(13, 16), tt=tt, win_score=80, verbose=False
459 | )
460 | tt.to_json_file("tt-data.json.temp", use_tuples=True)
461 | # 2. restore TranspositionTable from file
462 | restored_tt = easyAI.TranspositionTable()
463 | restored_tt.from_json_file("tt-data.json.temp", use_tuples=True)
464 | # 3. get first AI move using the TranspositionTable
465 | players = [easyAI.Human_Player(), easyAI.AI_Player(restored_tt)]
466 | game = games.Nim(players)
467 | game.play_move(best_player_move) # let the human play
468 | ai_move = game.get_move() # get the AI's move based on tt
469 | self.assertEqual(ai_move, "2,1")
470 | self.assertEqual(best_player_move, "1,1")
471 |
472 |
473 | if __name__ == "__main__":
474 | unittest.main(exit=False)
475 |
--------------------------------------------------------------------------------