├── .bumpversion.cfg ├── .github └── workflows │ ├── tests.yml │ └── version-bump-and-release.yml ├── .gitignore ├── LICENCE.txt ├── README.rst ├── docs ├── Makefile ├── make.bat ├── makehtml.sh └── source │ ├── _static │ ├── easyAI.css │ └── logo.jpeg │ ├── _themes │ ├── .gitignore │ ├── LICENSE │ ├── README │ ├── flask │ │ ├── layout.html │ │ ├── relations.html │ │ ├── static │ │ │ ├── flasky.css_t │ │ │ └── small_flask.css │ │ └── theme.conf │ ├── flask_small │ │ ├── layout.html │ │ ├── static │ │ │ └── flasky.css_t │ │ └── theme.conf │ └── flask_theme_support.py │ ├── ai_descriptions.rst │ ├── conf.py │ ├── crash_course.rst │ ├── examples │ ├── examples.rst │ ├── games.rst │ ├── integrate.rst │ ├── quick_example.rst │ └── speedup_cython.pyx │ ├── get_started.rst │ ├── index.rst │ ├── installation.rst │ ├── ref.rst │ └── speedup.rst ├── easyAI ├── AI │ ├── DUAL.py │ ├── DictTranspositionTable.py │ ├── HashTranspositionTable.py │ ├── Hashes.py │ ├── MTdriver.py │ ├── Negamax.py │ ├── NonRecursiveNegamax.py │ ├── SSS.py │ ├── TranspositionTable.py │ ├── __init__.py │ └── solving.py ├── Player.py ├── TwoPlayerGame.py ├── __init__.py ├── games │ ├── Awele.py │ ├── AweleTactical.py │ ├── Chopsticks.py │ ├── ConnectFour.py │ ├── Cram.py │ ├── GameOfBones.py │ ├── Hexapawn.py │ ├── Knights-Kivy.py │ ├── Knights.py │ ├── Nim.py │ ├── Reversi.py │ ├── ThreeMusketeers.py │ ├── TicTacToe-Flask.py │ ├── TicTacToe.py │ └── __init__.py └── version.py ├── setup.py └── tests ├── test_basics.py └── test_negamax.py /.bumpversion.cfg: -------------------------------------------------------------------------------- 1 | [bumpversion] 2 | current_version = 2.0.12 3 | commit = True 4 | message = 5 | [SKIP] version bump {current_version} -> {new_version} 6 | 7 | {$COMMIT_MSG} 8 | tag = True 9 | tag_message = 10 | version bump {current_version} -> {new_version} 11 | 12 | {$COMMIT_MSG} 13 | 14 | [bumpversion:file:easyAI/version.py] 15 | search = __version__ = "{current_version}" 16 | replace = __version__ = "{new_version}" 17 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | 3 | # C extensions 4 | *.so 5 | 6 | # Packages 7 | *.egg 8 | *.egg-info 9 | dist 10 | build 11 | eggs 12 | parts 13 | bin 14 | var 15 | sdist 16 | develop-eggs 17 | .installed.cfg 18 | lib 19 | lib64 20 | __pycache__ 21 | 22 | # Installer logs 23 | pip-log.txt 24 | 25 | # Unit test / coverage reports 26 | .coverage 27 | .tox 28 | nosetests.xml 29 | 30 | # Translations 31 | *.mo 32 | 33 | # Mr Developer 34 | .mr.developer.cfg 35 | .project 36 | .pydevproject 37 | 38 | # Temp files 39 | 40 | *~ 41 | *.temp 42 | 43 | # Pipy codes 44 | 45 | .pypirc 46 | 47 | # virtual env 48 | venv/ 49 | .vscode -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /docs/makehtml.sh: -------------------------------------------------------------------------------- 1 | make html 2 | firefox ../../docs/html/index.html 3 | -------------------------------------------------------------------------------- /docs/source/_static/easyAI.css: -------------------------------------------------------------------------------- 1 | @import url(flasky.css) 2 | 3 | .indexwrapper .sphinxsidebar { visibility: hidden; } 4 | 5 | .indexwrapper div.body #wand.section h1 { 6 | background-size: 75px 192px; 7 | background-repeat: no-repeat; 8 | padding-left: 75px; 9 | padding-top: 10px; 10 | height: 75px; 11 | } 12 | 13 | .logo img.logo { width: 390px; height: 192px; padding-right: 30px; } 14 | 15 | div.body h1, div.body h2, div.body h3, div.body h4, div.body h5, div.body h6 16 | { font-family: 'Times New Roman', 'Garamond', 'Georgia', serif; } 17 | 18 | -------------------------------------------------------------------------------- /docs/source/_static/logo.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zulko/easyAI/a5cbd0b600ebbeadc3730df9e7a211d7643cff8b/docs/source/_static/logo.jpeg -------------------------------------------------------------------------------- /docs/source/_themes/.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.pyo 3 | .DS_Store 4 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /docs/source/_themes/flask/layout.html: -------------------------------------------------------------------------------- 1 | {%- extends "basic/layout.html" %} 2 | {%- block extrahead %} 3 | {{ super() }} 4 | {% if theme_touch_icon %} 5 | 6 | {% endif %} 7 | 9 | {% endblock %} 10 | {%- block relbar2 %}{% endblock %} 11 | {% block header %} 12 | {{ super() }} 13 | {% if pagename == 'index' %} 14 |
15 | {% endif %} 16 | {% endblock %} 17 | {%- block footer %} 18 | 22 | {% if pagename == 'index' %} 23 |
24 | {% endif %} 25 | {%- endblock %} 26 | -------------------------------------------------------------------------------- /docs/source/_themes/flask/relations.html: -------------------------------------------------------------------------------- 1 |

Related Topics

2 | 20 | -------------------------------------------------------------------------------- /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/_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/_themes/flask/theme.conf: -------------------------------------------------------------------------------- 1 | [theme] 2 | inherit = basic 3 | stylesheet = flasky.css 4 | pygments_style = flask_theme_support.FlaskyStyle 5 | 6 | [options] 7 | index_logo = 'logo.jpeg' 8 | index_logo_height = 192px 9 | github_fork = 'http://github.com/zulko/easyAI' 10 | -------------------------------------------------------------------------------- /docs/source/_themes/flask_small/layout.html: -------------------------------------------------------------------------------- 1 | {% extends "basic/layout.html" %} 2 | {% block header %} 3 | {{ super() }} 4 | {% if pagename == 'index' %} 5 |
6 | {% endif %} 7 | {% endblock %} 8 | {% block footer %} 9 | {% if pagename == 'index' %} 10 |
11 | {% endif %} 12 | {% endblock %} 13 | {# do not display relbars #} 14 | {% block relbar1 %}{% endblock %} 15 | {% block relbar2 %} 16 | {% if theme_github_fork %} 17 | Fork me on GitHub 19 | {% endif %} 20 | {% endblock %} 21 | {% block sidebar1 %}{% endblock %} 22 | {% block sidebar2 %}{% endblock %} 23 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /docs/source/_themes/flask_small/theme.conf: -------------------------------------------------------------------------------- 1 | [theme] 2 | inherit = basic 3 | stylesheet = flasky.css 4 | nosidebar = true 5 | pygments_style = flask_theme_support.FlaskyStyle 6 | 7 | [options] 8 | index_logo = '' 9 | index_logo_height = 120px 10 | github_fork = '' 11 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /docs/source/examples/examples.rst: -------------------------------------------------------------------------------- 1 | Examples Of Use 2 | =============== 3 | 4 | In these pages you will find a few examples of use of easyAI: 5 | 6 | .. toctree:: 7 | :maxdepth: 1 8 | 9 | quick_example 10 | games 11 | integrate 12 | -------------------------------------------------------------------------------- /docs/source/examples/games.rst: -------------------------------------------------------------------------------- 1 | .. _ExamplesOfGames: 2 | 3 | Games implemented with easyAI 4 | ============================= 5 | 6 | Here are reported some games implementations provided in the 7 | ``examples`` folder of easyAI. 8 | 9 | Tic-Tac-Toe 10 | ------------ 11 | 12 | .. literalinclude:: ../../../easyAI/games/TicTacToe.py 13 | 14 | Game of Nim 15 | ------------ 16 | 17 | .. literalinclude:: ../../../easyAI/games/Nim.py 18 | 19 | Game of Knights 20 | --------------- 21 | 22 | .. literalinclude:: ../../../easyAI/games/Knights.py 23 | 24 | Awele 25 | ----- 26 | 27 | .. literalinclude:: ../../../easyAI/games/Awele.py 28 | 29 | .. _connect4: 30 | 31 | Connect 4 32 | ---------- 33 | 34 | .. literalinclude:: ../../../easyAI/games/ConnectFour.py 35 | 36 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | 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 | Fork me on GitHub 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 | -------------------------------------------------------------------------------- /docs/source/installation.rst: -------------------------------------------------------------------------------- 1 | Installation 2 | ============= 3 | 4 | if you have ``pip`` installed, type this in a terminal :: 5 | 6 | sudo pip install easyAI 7 | 8 | Otherwise, dowload the source code (for instance on Github), unzip everything into one folder and in this folder, in a terminal, type :: 9 | 10 | sudo python setup.py install 11 | 12 | Additionnally you will need to install Numpy to be able to run some of the examples. -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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/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/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/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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /easyAI/AI/__init__.py: -------------------------------------------------------------------------------- 1 | from .Negamax import Negamax 2 | from .NonRecursiveNegamax import NonRecursiveNegamax 3 | from .TranspositionTable import TranspositionTable 4 | from .solving import solve_with_iterative_deepening, solve_with_depth_first_search 5 | from .MTdriver import mtd 6 | from .SSS import SSS 7 | from .DUAL import DUAL 8 | from .HashTranspositionTable import HashTranspositionTable -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /easyAI/__init__.py: -------------------------------------------------------------------------------- 1 | __all__ = [ 2 | "TwoPlayerGame", 3 | "Human_Player", 4 | "AI_Player", 5 | "Negamax", 6 | "TranspositionTable", 7 | "solve_with_iterative_deepening", 8 | "solve_with_depth_first_search", 9 | "NonRecursiveNegamax", 10 | "mtd", 11 | "SSS", 12 | "DUAL", 13 | "HashTranspositionTable", 14 | "DictTranspositionTable", 15 | ] 16 | 17 | from .TwoPlayerGame import TwoPlayerGame 18 | from .Player import Human_Player, AI_Player 19 | from .AI import ( 20 | Negamax, 21 | solve_with_iterative_deepening, 22 | solve_with_depth_first_search, 23 | NonRecursiveNegamax, 24 | TranspositionTable, 25 | mtd, 26 | SSS, 27 | DUAL, 28 | HashTranspositionTable, 29 | DictTranspositionTable, 30 | ) 31 | -------------------------------------------------------------------------------- /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/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/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/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) -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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/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/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/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/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 |
90 | 91 | {% for j in range(0, 3) %} 92 | 93 | {% for i in range(0, 3) %} 94 | 100 | {% endfor %} 101 | 102 | {% endfor %} 103 |
95 | 99 |
104 | 105 |
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/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 | -------------------------------------------------------------------------------- /easyAI/games/__init__.py: -------------------------------------------------------------------------------- 1 | from .TicTacToe import TicTacToe 2 | from .ConnectFour import ConnectFour 3 | from .Knights import Knights 4 | from .GameOfBones import GameOfBones 5 | from .Nim import Nim 6 | from .Cram import Cram 7 | from .ThreeMusketeers import ThreeMusketeers 8 | from .AweleTactical import AweleTactical 9 | -------------------------------------------------------------------------------- /easyAI/version.py: -------------------------------------------------------------------------------- 1 | __version__ = "2.0.12" 2 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import os 4 | from setuptools import setup, find_packages 5 | 6 | with open(os.path.join("easyAI", "version.py"), "r") as f: 7 | __version__ = f.read().split(" ")[2].strip("\n").strip('"') 8 | 9 | setup( 10 | name="easyAI", 11 | version=__version__, 12 | description="Easy-to-use game AI algorithms (Negamax etc. )", 13 | long_description=open("README.rst").read(), 14 | license="LICENSE.txt", 15 | keywords="board games AI artificial intelligence negamax", 16 | packages=find_packages(exclude="docs"), 17 | install_requires=["numpy"], 18 | ) 19 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------