diff --git a/blackbest b/blackbest
index 920812e..1daafce 100644
--- a/blackbest
+++ b/blackbest
@@ -1 +1 @@
-[-0.2967439805419083, 0.3682500438280648, 0.5820416163057722, 1.6675946813493288, 5.800111570322901]
\ No newline at end of file
+[0.3, 0.5, 0.5, 1.66, 0]
\ No newline at end of file
diff --git a/blackheuristics.py b/blackheuristics.py
index 8daca96..0a505ca 100644
--- a/blackheuristics.py
+++ b/blackheuristics.py
@@ -1,5 +1,5 @@
# Black heuristics
-def black_fitness(board):
+def black_fitness(board, alpha0, beta0, gamma0, theta0, epsilon0):
"""
Black heuristics should be based on:
- Number of black pawns
@@ -9,16 +9,19 @@ def black_fitness(board):
- A coefficient of encirclement of the king
"""
- fitness = 0
+ with open("blackbest", "r") as f:
+ line = f.readline()
+ line = line.replace("[", "")
+ line = line.replace("]", "")
+ line = line.replace(" ", "")
+ line = line.split(",")
+ line = [float(x) for x in line]
- # TODO: Use the correct weights
- alpha0 = 0.3
- beta0 = 0.5
- gamma0 = 0.5820416163057722
- theta0 = 1.6675946813493288
+ alpha0, beta0, gamma0, theta0, epsilon0 = line
king_pos = board.get_king()
+ fitness = 0
# Number of black pawns
fitness += alpha0 * len(board.blacks)
diff --git a/blackplayer/.gitignore b/blackplayer/.gitignore
new file mode 100644
index 0000000..8a9584c
--- /dev/null
+++ b/blackplayer/.gitignore
@@ -0,0 +1,162 @@
+# Byte-compiled / optimized / DLL files
+__pycache__/
+*.py[cod]
+*$py.class
+
+# C extensions
+*.so
+
+# Distribution / packaging
+.Python
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+wheels/
+share/python-wheels/
+*.egg-info/
+.installed.cfg
+*.egg
+MANIFEST
+
+# PyInstaller
+# Usually these files are written by a python script from a template
+# before PyInstaller builds the exe, so as to inject date/other infos into it.
+*.manifest
+*.spec
+
+# Installer logs
+pip-log.txt
+pip-delete-this-directory.txt
+
+# Unit test / coverage reports
+htmlcov/
+.tox/
+.nox/
+.coverage
+.coverage.*
+.cache
+nosetests.xml
+coverage.xml
+*.cover
+*.py,cover
+.hypothesis/
+.pytest_cache/
+cover/
+
+# Translations
+*.mo
+*.pot
+
+# Django stuff:
+*.log
+local_settings.py
+db.sqlite3
+db.sqlite3-journal
+
+# Flask stuff:
+instance/
+.webassets-cache
+
+# Scrapy stuff:
+.scrapy
+
+# Sphinx documentation
+docs/_build/
+
+# PyBuilder
+.pybuilder/
+target/
+
+# Jupyter Notebook
+.ipynb_checkpoints
+
+# IPython
+profile_default/
+ipython_config.py
+
+# pyenv
+# For a library or package, you might want to ignore these files since the code is
+# intended to run in multiple environments; otherwise, check them in:
+# .python-version
+
+# pipenv
+# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
+# However, in case of collaboration, if having platform-specific dependencies or dependencies
+# having no cross-platform support, pipenv may install dependencies that don't work, or not
+# install all needed dependencies.
+#Pipfile.lock
+
+# poetry
+# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
+# This is especially recommended for binary packages to ensure reproducibility, and is more
+# commonly ignored for libraries.
+# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
+#poetry.lock
+
+# pdm
+# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
+#pdm.lock
+# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
+# in version control.
+# https://pdm.fming.dev/#use-with-ide
+.pdm.toml
+
+# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
+__pypackages__/
+
+# Celery stuff
+celerybeat-schedule
+celerybeat.pid
+
+# SageMath parsed files
+*.sage.py
+
+# Environments
+.env
+.venv
+env/
+venv/
+ENV/
+env.bak/
+venv.bak/
+
+# Spyder project settings
+.spyderproject
+.spyproject
+
+# Rope project settings
+.ropeproject
+
+# mkdocs documentation
+/site
+
+# mypy
+.mypy_cache/
+.dmypy.json
+dmypy.json
+
+# Pyre type checker
+.pyre/
+
+# pytype static type analyzer
+.pytype/
+
+# Cython debug symbols
+cython_debug/
+
+# PyCharm
+# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
+# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
+# and can be added to the global gitignore or merged into this file. For a more nuclear
+# option (not recommended) you can uncomment the following to ignore the entire idea folder.
+#.idea/
+
+
diff --git a/blackplayer/LICENSE b/blackplayer/LICENSE
new file mode 100644
index 0000000..0313c6d
--- /dev/null
+++ b/blackplayer/LICENSE
@@ -0,0 +1,201 @@
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [2023] [Matteo Fasulo]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/blackplayer/README.md b/blackplayer/README.md
new file mode 100644
index 0000000..3dcf86d
--- /dev/null
+++ b/blackplayer/README.md
@@ -0,0 +1,84 @@
+# \tLut ππ€
+
+
+
+
+
+## Introduction π
+
+Hey there, code explorers! π Welcome to the mind-blowing world of Tablut, brought to you by the genius minds of [University of Bologna's AI course](https://corsi.unibo.it/2cycle/artificial-intelligence) for the mind-bending academic year 2023/2024! π§ π
+
+Our mission? Oh, just to create an AI wizard π§ capable of dominating the [epic game of Tablut](https://en.wikipedia.org/wiki/Tafl_games) using the jaw-dropping [Minimax algorithm with Alpha-Beta pruning](https://en.wikipedia.org/wiki/Alpha%E2%80%93beta_pruning). Yeah, we're that ambitious! π
+
+And who's behind this magical project? None other than the fantastic foursome:
+
+- [Matteo Fasulo](https://github.com/MatteoFasulo) π
+- [Luca Tedeschini](https://github.com/LucaTedeschini) π
+- [Antonio Gravina](https://github.com/GravAnt) π₯
+- [Norberto Casarin](https://github.com/BandoleroNext) π
+
+## Project Philosophy π
+
+Hold on to your hats, folks! This project is pure Python 3.9 sorcery! π The code? It's organized so neatly, even Marie Kondo would be proud! π§Ή Plus, it's comment city, making it a breeze to understand. Oh, and did we mention we've sprinkled some [AIMA Python code](https://github.com/aimacode/aima-python) magic? Yup, we went all out! πβ¨
+
+## How to Run the Code π
+
+Ready for the magic show? π©β¨ To dive into the enchanting world of Tablut, simply run the mystical [`play.py`](play.py) file. Just toss in these enchanting arguments:
+
+- `--team`: Choose your team, either `WHITE` or `BLACK`. π΄ββ οΈπ³οΈ
+- `--name`: Declare the name of your agent. β¨
+- `--ip`: Provide the IP address of the server. Default is `localhost`. π
+
+Behold the spell to run this enchanting code:
+
+```bash
+py play.py --team WHITE --name "\tLut" --ip
+```
+
+Oh, and if you're in the Windows realm, use `python3` for Linux adventures. π§β¨
+
+### WHITE Heuristics
+
+In the implementation of the WHITE player's heuristics, several factors are taken into consideration to evaluate the current state of the Tablut board. These factors contribute to the overall fitness of the position for the WHITE player. The key components of the WHITE heuristics include:
+
+1. **Vulnerability to Capture:**
+ - A measure of the risk of WHITE pieces being captured by BLACK pawns.
+ - Utilizes the concept of clear views to assess potential threats to WHITE pieces.
+
+2. **King's Distance and Safety:**
+ - Evaluates the distance of the WHITE king from the corners of the board.
+ - Incorporates the number of BLACK pieces in each quadrant to assess the safety of the king.
+
+3. **External Pawn Distribution:**
+ - Calculates the fitness based on the distribution of WHITE pawns relative to the king.
+ - Promotes a strategic arrangement of WHITE pieces with respect to the center of the board.
+
+4. **King's Defense:**
+ - Considers the ability of BLACK pieces to potentially capture the WHITE king in the next move.
+ - Applies a strong negative fitness if the king is at risk of capture.
+
+5. **King's Movement:**
+ - Evaluates the movement of the WHITE king, promoting a balanced position across the board.
+
+The combination of these factors aims to provide a comprehensive assessment of the Tablut board from the perspective of the WHITE player.
+
+### BLACK Heuristics
+
+The BLACK player's heuristics focus on evaluating the strategic aspects of the Tablut board, taking into account various factors that influence the overall fitness of the position. The primary considerations in the BLACK heuristics are:
+
+1. **Number of BLACK and WHITE Pawns:**
+ - Assigns fitness based on the count of BLACK and WHITE pawns on the board.
+
+2. **Proximity of BLACK Pawns to the King:**
+ - Evaluates the number of BLACK pawns within a certain distance of the WHITE king.
+ - A higher count encourages a more aggressive position.
+
+3. **Path to the King:**
+ - Assesses the availability of free paths for BLACK pawns to reach and potentially capture the WHITE king.
+
+4. **Encirclement of the King:**
+ - Introduces a coefficient of encirclement, considering the positioning of BLACK pawns around the WHITE king.
+
+The BLACK heuristics aim to capture the strategic advantages, focusing on pawn count, proximity to the king, and the potential to create threats against the WHITE player. These factors collectively contribute to the fitness evaluation for the BLACK player in the Tablut game.
+
+Prepare to be amazed, mortals! π©β¨
diff --git a/blackplayer/blackbest b/blackplayer/blackbest
new file mode 100644
index 0000000..920812e
--- /dev/null
+++ b/blackplayer/blackbest
@@ -0,0 +1 @@
+[-0.2967439805419083, 0.3682500438280648, 0.5820416163057722, 1.6675946813493288, 5.800111570322901]
\ No newline at end of file
diff --git a/blackplayer/blackheuristics.py b/blackplayer/blackheuristics.py
new file mode 100644
index 0000000..3480262
--- /dev/null
+++ b/blackplayer/blackheuristics.py
@@ -0,0 +1,46 @@
+# Black heuristics
+def black_fitness(board, alpha0, beta0, gamma0, theta0, epsilon0):
+ """
+ Black heuristics should be based on:
+ - Number of black pawns
+ - Number of white pawns
+ - Number of black pawns next to the king
+ - Free path to the king
+ - A coefficient of encirclement of the king
+ """
+
+
+ king_pos = board.get_king()
+
+ fitness = 0
+ # Number of black pawns
+ fitness += alpha0 * len(board.blacks)
+
+ # Number of white pawns
+ fitness -= beta0 * len(board.whites)
+
+ # Number of black pawns next to the king
+ fitness += gamma0 * pawns_around(board, king_pos, distance=2)
+
+ # Free path to the king
+ free_paths = [board._is_there_a_clear_view(black_pawn, king_pos)
+ for black_pawn in board.blacks]
+ # theta0 times the nΒ° free ways to king
+ fitness += theta0 * sum(free_paths)
+
+ return fitness
+
+
+def pawns_around(board, pawn, distance: int):
+ """
+ Returns the number of pawns around a given pawn within a certain distance (usually the king)
+ """
+ x, y = pawn
+ count = 0
+ for i in range(-distance, distance+1):
+ for j in range(-distance, distance+1):
+ if i == 0 and j == 0:
+ continue
+ if (x+i, y+j) in board.blacks:
+ count += 1
+ return count
diff --git a/blackplayer/board.py b/blackplayer/board.py
new file mode 100644
index 0000000..e5cd53c
--- /dev/null
+++ b/blackplayer/board.py
@@ -0,0 +1,250 @@
+import time
+from collections import defaultdict
+
+import numpy as np
+
+# utils
+from utils import Pawn, WHITE, WHITE2, RED, RED2, GREEN, GREEN2, BLUE, GRAY
+
+
+class Board(defaultdict):
+ empty = Pawn.EMPTY
+ off = '#'
+
+ def __init__(self, width, height, to_move, **kwds):
+ self.__dict__.update(width=width, height=height,
+ to_move=to_move, **kwds)
+
+ self.board = [
+ [GRAY, WHITE, WHITE2, RED2, RED, RED2, WHITE2, WHITE, GRAY],
+ [WHITE, WHITE2, WHITE, WHITE2, RED2, WHITE2, WHITE, WHITE2, WHITE],
+ [WHITE2, WHITE, WHITE2, WHITE, GREEN, WHITE, WHITE2, WHITE, WHITE2],
+ [RED2, WHITE2, WHITE, WHITE2, GREEN2, WHITE2, WHITE, WHITE2, RED2],
+ [RED, RED2, GREEN, GREEN2, BLUE, GREEN2, GREEN, RED2, RED],
+ [RED2, WHITE2, WHITE, WHITE2, GREEN2, WHITE2, WHITE, WHITE2, RED2],
+ [WHITE2, WHITE, WHITE2, WHITE, GREEN, WHITE, WHITE2, WHITE, WHITE2],
+ [WHITE, WHITE2, WHITE, WHITE2, RED2, WHITE2, WHITE, WHITE2, WHITE],
+ [GRAY, WHITE, WHITE2, RED2, RED, RED2, WHITE2, WHITE, GRAY],
+ ]
+
+ self.winning_positions = set([(0, 1), (0, 2), (0, 7), (0, 8), (1, 0), (2, 0), (2, 8), (
+ 1, 8), (6, 0), (6, 8), (7, 0), (7, 8), (8, 1), (8, 2), (8, 6), (8, 7)])
+
+ # AIMA methods
+ def to_move(self, state):
+ return self.__dict__['to_move']
+
+ def __missing__(self, loc):
+ x, y = loc
+ if 0 <= x < self.width and 0 <= y < self.height:
+ return self.empty
+ else:
+ return self.off
+
+ def __hash__(self):
+ return hash(tuple(sorted(self.items()))) + hash(self.to_move)
+
+ def __str__(self):
+ """
+ Given an np.array of pieces, return a string representation of the board
+ with rows separated by newlines.
+ """
+ return str(self.pieces)
+
+ # Board methods
+ def get_white(self):
+ pawns = np.where(self.pieces == Pawn.WHITE.value)
+ coordinates = list(zip(pawns[0], pawns[1]))
+ self.white = coordinates
+
+ king = self.get_king()
+ if king is not None:
+ self.white.insert(0, king)
+
+ self.whites = self.white
+ return coordinates
+
+ def get_black(self):
+ pawns = np.where(self.pieces == Pawn.BLACK.value)
+ coordinates = list(zip(pawns[0], pawns[1]))
+ self.black = coordinates
+ self.blacks = self.black
+ return coordinates
+
+ def get_king(self):
+ pawns = np.where(self.pieces == Pawn.KING.value)
+ coordinates = list(zip(pawns[0], pawns[1]))
+ try:
+ self.king = coordinates[0]
+ return coordinates[0]
+ except IndexError: # king has been captured
+ return None
+
+ def _is_there_a_clear_view(self, piece1, piece2):
+ if piece1[0] == piece2[0]:
+ offset = 1 if piece1[1] <= piece2[1] else -1
+ for i in range(piece1[1] + offset, piece2[1], offset):
+ if self.pieces[piece1[0]][i] != 0:
+ return False
+ return True
+ elif piece1[1] == piece2[1]:
+ offset = 1 if piece1[0] <= piece2[0] else -1
+ for i in range(piece1[0] + offset, piece2[0], offset):
+ if self.pieces[i][piece1[1]] != 0:
+ return False
+ return True
+ else:
+ return False
+
+ def check_attacks(self, x, y):
+ # Horizontal check
+ if x > 1:
+ if self.pieces[x-2][y] == self.pieces[x][y] and self.pieces[x-1][y] != self.pieces[x][y]:
+ self.pieces[x-1][y] = 0
+ has_deleted = False
+ for black in self.blacks:
+ if black[0] == x-1 and black[1] == y:
+ del black
+ has_deleted = True
+ break
+ if not has_deleted:
+ for white in self.whites:
+ if white[0] == x-1 and white[1] == y:
+ del white
+ break
+ if x < len(self.pieces)-2:
+ if self.pieces[x+2][y] == self.pieces[x][y] and self.pieces[x+1][y] != self.pieces[x][y]:
+ self.pieces[x+1][y] = 0
+ has_deleted = False
+ for black in self.blacks:
+ if black[0] == x-1 and black[1] == y:
+ del black
+ has_deleted = True
+ break
+ if not has_deleted:
+ for white in self.whites:
+ if white[0] == x-1 and white[1] == y:
+ del white
+ break
+ # Vertical check
+ if y > 1:
+ if self.pieces[x][y-2] == self.pieces[x][y] and self.pieces[x][y-1] != self.pieces[x][y]:
+ self.pieces[x][y-1] = 0
+ has_deleted = False
+ for black in self.blacks:
+ if black[0] == x-1 and black[1] == y:
+ del black
+ has_deleted = True
+ break
+ if not has_deleted:
+ for white in self.whites:
+ if white[0] == x-1 and white[1] == y:
+ del white
+ break
+ if y < len(self.pieces)-2:
+ if self.pieces[x][y+2] == self.pieces[x][y] and self.pieces[x][y+1] != self.pieces[x][y]:
+ self.pieces[x][y+1] = 0
+ has_deleted = False
+ for black in self.blacks:
+ if black[0] == x-1 and black[1] == y:
+ del black
+ has_deleted = True
+ break
+ if not has_deleted:
+ for white in self.whites:
+ if white[0] == x-1 and white[1] == y:
+ del white
+ break
+
+ return self
+
+ def eat_black(self):
+ '''
+ If a black piece can be eaten it returns a list of initial and final position of the white piece that eats
+ '''
+ moves_to_eat = []
+ for white in self.whites:
+ for i in range(white[1]+1, len(self.pieces)-1, 1):
+ try:
+ if self.pieces[white[0]+1][i] == 1 and self.pieces[white[0]+2][i] == 2:
+ moves_to_eat.append([white[0], white[1], white[0], i])
+ if self.pieces[white[0]-1][i] == 1 and self.pieces[white[0]-2][i] == 2:
+ moves_to_eat.append([white[0], white[1], white[0], i])
+ except:
+ pass
+ if self.pieces[white[0]][i] == 1 and self.pieces[white[0]][i+1] == 2:
+ moves_to_eat.append([white[0], white[1], white[0], i-1])
+ break
+ for i in range(0, white[1]):
+ try:
+ if self.pieces[white[0]+1][i] == 1 and self.pieces[white[0]+2][i] == 2:
+ moves_to_eat.append([white[0], white[1], white[0], i])
+ if self.pieces[white[0]-1][i] == 1 and self.pieces[white[0]-2][i] == 2:
+ moves_to_eat.append([white[0], white[1], white[0], i])
+ except:
+ pass
+ if self.pieces[white[0]][i] == 1 and self.pieces[white[0]][i-1] == 2:
+ moves_to_eat.append([white[0], white[1], i+1])
+ break
+ for i in range(0, white[0]):
+ try:
+ if self.pieces[i][white[1]+1] == 1 and self.pieces[i][white[1]+2] == 2:
+ moves_to_eat.append([white[0], white[1], i, white[1]])
+ if self.pieces[i][white[1]-1] == 1 and self.pieces[i][white[1]-2] == 2:
+ moves_to_eat.append([white[0], white[1], i, white[1]])
+ except:
+ pass
+ if self.pieces[i][white[1]] == 1 and self.pieces[i-1][white[1]] == 2:
+ moves_to_eat.append([white[0], white[1], i+1, white[1]])
+ break
+ for i in range(white[0]+1, len(self.pieces)-1, 1):
+ try:
+ if self.pieces[i][white[1]+1] == 1 and self.pieces[i][white[1]+2] == 2:
+ moves_to_eat.append([white[0], white[1], i, white[1]])
+ if self.pieces[i][white[1]-1] == 1 and self.pieces[i][white[1]-2] == 2:
+ moves_to_eat.append([white[0], white[1], i, white[1]])
+ except:
+ pass
+ if self.pieces[i][white[1]] == 1 and self.pieces[i+1][white[1]] == 2:
+ moves_to_eat.append([white[0], white[1], i-1, white[1]])
+ break
+
+ self.white_moves_to_eat = moves_to_eat
+
+ def check_num_pieces_in_quadrant(self, n_quadrant, black_or_white):
+ """
+ Returns the number of pieces in the quadrant of selected color
+ (1 = left up, 2 = right up, 3 = left down, 4 = right down)
+ """
+ if black_or_white not in (1, 2):
+ raise Exception("The color must be 1 (black) or 2 (white)")
+
+ num_pieces = 0
+ if n_quadrant == 1:
+ quadrant = [row[:4] for row in self.board[:4]]
+ for i in range(4):
+ for j in range(4):
+ if quadrant[i][j] == black_or_white:
+ num_pieces += 1
+ elif n_quadrant == 2:
+ quadrant = [row[5:] for row in self.board[:4]]
+ for i in range(4):
+ for j in range(4):
+ if quadrant[i][j] == black_or_white:
+ num_pieces += 1
+ elif n_quadrant == 3:
+ quadrant = [row[:4] for row in self.board[5:]]
+ for i in range(4):
+ for j in range(4):
+ if quadrant[i][j] == black_or_white:
+ num_pieces += 1
+ elif n_quadrant == 4:
+ quadrant = [row[5:] for row in self.board[5:]]
+ for i in range(4):
+ for j in range(4):
+ if quadrant[i][j] == black_or_white:
+ num_pieces += 1
+ else:
+ raise Exception("The quadrant number must be between 1 and 4")
+
+ return num_pieces
diff --git a/blackplayer/genetic.py b/blackplayer/genetic.py
new file mode 100644
index 0000000..f8403e8
--- /dev/null
+++ b/blackplayer/genetic.py
@@ -0,0 +1,131 @@
+from play import play_game
+import time
+import random
+import copy
+import sys
+
+def cutoff_depth(d):
+ return lambda game, state, depth: depth > d
+
+def fitness(turns):
+ return 30-turns
+
+def create_starting_population(elements = 10):
+ population = []
+ for i in range(elements):
+ population.append([random.uniform(0, 2), random.uniform(0, 2), random.uniform(0, 10), random.uniform(0, 2), random.uniform(0, 3)])
+ return population
+
+def cross_chromosome(chromosome1, chromosome2):
+ alpha0, beta0, gamma0, theta0, epsilon0 = chromosome1
+ alpha1, beta1, gamma1, theta1, epsilon1 = chromosome2
+ new_chromosome1 = [alpha0, beta1, gamma0, theta1, epsilon0]
+ new_chromosome2 = [alpha1, beta0, gamma1, theta0, epsilon1]
+ return new_chromosome1, new_chromosome2
+
+def mate(elements):
+ random1 = copy.deepcopy(random.choice(elements))
+ random2 = copy.deepcopy(random.choice(elements))
+
+ alpha0, beta0, gamma0, theta0, epsilon0 = random1
+ alpha1, beta1, gamma1, theta1, epsilon1 = random2
+
+ new_chromosome = [(alpha0+alpha1)/2, (beta0+beta1)/2, (gamma0+gamma1)/2, (theta0+theta1)/2, (epsilon0+epsilon1)/2]
+ return new_chromosome
+
+def create_random_chromosome():
+ return [random.uniform(0, 5), random.uniform(0, 5), random.uniform(0, 5), random.uniform(0, 5), random.uniform(0, 5)]
+
+def mutate(population, mutation_rate=0.6):
+ print(population)
+ for i in range(1, len(population)):
+ if random.random() < mutation_rate:
+ alpha, beta, gamma, theta, epsilon = population[i]
+ alpha += random.uniform(-1, 1)
+ beta += random.uniform(-1, 1)
+ gamma += random.uniform(-1, 1)
+ theta += random.uniform(-1, 1)
+ epsilon += random.uniform(-1, 1)
+ population[i] = [alpha, beta, gamma, theta, epsilon]
+ return population
+
+def create_starting_from_params(alpha, beta, gamma, theta, epsilon, elements=10):
+ population = []
+
+ for i in range(elements):
+ population.append([alpha+random.uniform(-1,1), beta+random.uniform(-1,1), gamma+random.uniform(-1,1), theta+random.uniform(-1,1), epsilon+random.uniform(-1,1)])
+ return population
+
+
+iterations = 50
+elements = 10
+max_moves = 30
+cod = cutoff_depth(2)
+population = create_starting_population(elements)
+population = create_starting_from_params(0.3, 0.5, 0.5, 1.66, 0)
+
+for iteration in range(iterations):
+ results = []
+ for pop in population:
+ print(pop)
+ alpha0, beta0, gamma0, theta0, epsilon0 = pop
+ result, turns = play_game(alpha0, beta0, gamma0, theta0, epsilon0, cod, max_moves, name="blackPlayer", team=sys.argv[1], server_ip="127.0.0.1", timeout=60)
+ #If result = 0, white wins
+ #If result = 1, black wins
+ #If result = 2, draw
+ #If result = 3, max moves reached
+ if result == 1:
+ results.append([pop, fitness(turns)])
+ else:
+ results.append([pop, -1])
+ time.sleep(15)
+
+
+ results = sorted(results, key=lambda x: x[1], reverse=True)
+ print("Best fitness: ", results[0][1])
+ print("Best parameters: ", results[0][0])
+ with open("../resultsBLACK.txt", "a") as f:
+ f.write("Generation: " + str(iteration) + "\n")
+ f.write("Best fitness: " + str(results[0][1]) + "\n")
+ f.write("Best parameters: " + str(results[0][0]) + "\n\n")
+
+ with open("../blackbest", "w") as f:
+ f.write(str(results[0][0]))
+
+
+ #I copy the first element of the population
+ new_population = []
+ new_population.append(copy.deepcopy(results[0][0]))
+
+ #I cross the first element with the second and the third
+ e1, e2 = cross_chromosome(results[0][0], results[1][0])
+ new_population.append(e1)
+ new_population.append(e2)
+ e1, e2 = cross_chromosome(results[0][0], results[2][0])
+ new_population.append(e1)
+ new_population.append(e2)
+
+
+ #I mate from the top 5 elements creating the remaining-3 elements
+ top_five = copy.deepcopy(results[:5])
+ random.shuffle(top_five)
+ top_five = [x[0] for x in top_five]
+
+ new_population.append(mate(top_five))
+ new_population.append(mate(top_five))
+ new_population.append(mate(top_five))
+
+ #I add three random elements
+ new_population.append(create_random_chromosome())
+ new_population.append(create_random_chromosome())
+
+ #I mutate everything except the first element
+ new_population = mutate(new_population)
+
+ #I update the population
+ population = copy.deepcopy(new_population)
+
+
+
+
+
diff --git a/blackplayer/mkdocs.yml b/blackplayer/mkdocs.yml
new file mode 100644
index 0000000..b96d500
--- /dev/null
+++ b/blackplayer/mkdocs.yml
@@ -0,0 +1,55 @@
+site_name: \tLut
+theme:
+ name: material
+ features:
+ - navigation.tabs
+ - navigation.sections
+ - toc.integrate
+ - navigation.top
+ - search.suggest
+ - search.highlight
+ - content.tabs.link
+ - content.code.annotate
+ - content.code.copy
+ language: en
+ palette:
+ - scheme: default
+ toggle:
+ icon: material/toggle-switch-off-outline
+ name: Switch to dark mode
+ primary: teal
+ accent: purple
+ - scheme: slate
+ toggle:
+ icon: material/toggle-switch
+ name: Switch to light mode
+ primary: teal
+ accent: lime
+
+extra:
+ social:
+ - icon: fontawesome/brands/github-alt
+ link: https://github.com/MatteoFasulo/Tablut
+ - icon: fontawesome/brands/linkedin
+ link: https://www.linkedin.com/in/matteofasulo/
+
+markdown_extensions:
+ - pymdownx.highlight:
+ anchor_linenums: true
+ - pymdownx.inlinehilite
+ - pymdownx.snippets
+ - admonition
+ - pymdownx.arithmatex:
+ generic: true
+ - footnotes
+ - pymdownx.superfences
+ - pymdownx.mark
+ - attr_list
+ - pymdownx.emoji:
+ emoji_index: !!python/name:materialx.emoji.twemoji
+ emoji_generator: !!python/name:materialx.emoji.to_svg
+
+copyright: |
+ © 2023 Matteo Fasulo, Luca Tedeschini, Antonio Gravina, Norberto Casarin
+
+repo_url: https://github.com/MatteoFasulo/Tablut
\ No newline at end of file
diff --git a/blackplayer/play.py b/blackplayer/play.py
new file mode 100644
index 0000000..6b88331
--- /dev/null
+++ b/blackplayer/play.py
@@ -0,0 +1,258 @@
+import argparse
+import os
+import sys
+import random
+import time
+import copy
+
+# numpy
+import numpy as np
+
+# threading
+import threading
+
+# Tablut Class
+from tablut import Tablut
+from board import Board
+
+# utils
+from utils import Network, WinException
+
+
+def cache(function):
+ """
+ A decorator that caches the result of a function based on its arguments.
+
+ Args:
+ function: The function to be cached.
+
+ Returns:
+ The wrapped function that caches the result.
+ """
+ cache = {}
+
+ def wrapped(x, *args, **kwargs):
+ pieces = x.pieces.data.tobytes()
+ if pieces not in cache:
+ cache[pieces] = function(x, *args, **kwargs)
+ return cache[pieces]
+ return wrapped
+
+
+def cutoff_depth(d):
+ """
+ Returns a function that determines if the search should be cut off at a certain depth.
+
+ Parameters:
+ d (int): The maximum depth at which the search should be cut off.
+
+ Returns:
+ function: A function that takes the current game, state, and depth as parameters and returns True if the search should be cut off, False otherwise.
+ """
+ return lambda game, state, depth: depth > d
+
+
+def h_alphabeta_search(state, game, alpha0, beta0, gamma0, theta0, epsilon0, cutoff, time_limit=55):
+ """
+ Performs a heuristic alpha-beta search to find the best move for a given game state.
+
+ Args:
+ state: The current game state.
+ game: The game object representing the rules of the game.
+ cutoff: The cutoff function that determines when to stop the search.
+ time_limit: The maximum time limit for the search.
+
+ Returns:
+ The best move to be played from the current state.
+ """
+ player = state.to_move
+ backtrack_dict = dict()
+
+ @cache
+ def max_value(state, alpha, beta, depth, alpha0, beta0, gamma0, theta0, epsilon0, action_backtrack=None):
+ nonlocal backtrack_dict
+ if game.terminal_test(state):
+ # print("TERMINAL STATE REACHED")
+ return game.utility(state, player), None
+ if cutoff(game, state, depth):
+ # print(f"CUTOFF at {depth = }")
+ v = game.compute_utility(
+ state, None, alpha0, beta0, gamma0, theta0, epsilon0, player)
+ # print(f"PRINT AL CUTOFF: {v}")
+ return v, None
+ if time.time() - start_time > time_limit:
+ # print(f"TIMEOUT at {depth = }, player: {player}")
+ best_action = max(backtrack_dict, key=backtrack_dict.get)
+ raise TimeoutError(best_action)
+ v, move = -np.inf, None
+ if isinstance(state, Tablut):
+ pieces = copy.deepcopy(state.initial.pieces)
+ board = state.initial.board # red2...
+ elif isinstance(state, Board):
+ pieces = copy.deepcopy(state.pieces)
+ board = state.board
+ for a in game.actions(pieces, player, board):
+ if depth == 0:
+ from_pos, to_pos = a
+ if from_pos == state.get_king() and to_pos in state.winning_positions:
+ raise WinException(a)
+ action_backtrack = a
+ backtrack_dict[a] = 0
+ v2, _ = min_value(game.result(state, a, alpha0, beta0, gamma0, theta0, epsilon0),
+ alpha, beta, depth+1, alpha0, beta0, gamma0, theta0, epsilon0, action_backtrack)
+ # print(f"Move: {a} | Score: {v2}")
+ if v2 > v:
+ v, move = v2, a
+ alpha = max(alpha, v)
+ backtrack_dict[action_backtrack] = v
+ if v >= beta:
+ return v, move
+ return v, move
+
+ @cache
+ def min_value(state, alpha, beta, depth, alpha0, beta0, gamma0, theta0, epsilon0, action_backtrack):
+ nonlocal backtrack_dict
+ if game.terminal_test(state):
+ # print("TERMINAL STATE REACHED")
+ return game.utility(state, player), None
+ if cutoff(game, state, depth):
+ # print(f"CUTOFF at {depth = }")
+ v = game.compute_utility(
+ state, None, alpha0, beta0, gamma0, theta0, epsilon0, player)
+ # print(f"PRINT AL CUTOFF: {v}")
+ return v, None
+ if time.time() - start_time > time_limit:
+ # print(f"TIMEOUT at {depth = }, player: {player}")
+ best_action = max(backtrack_dict, key=backtrack_dict.get)
+ raise TimeoutError(best_action)
+ v, move = +np.inf, None
+ if isinstance(state, Tablut):
+ pieces = copy.deepcopy(state.initial.pieces)
+ board = state.initial.board # red2...
+ elif isinstance(state, Board):
+ pieces = copy.deepcopy(state.pieces)
+ board = state.board
+ for a in game.actions(pieces, player, board):
+ v2, _ = max_value(game.result(state, a, alpha0, beta0, gamma0, theta0, epsilon0),
+ alpha, beta, depth+1, alpha0, beta0, gamma0, theta0, epsilon0, action_backtrack)
+ if v2 < v:
+ v, move = v2, a
+ beta = min(beta, v)
+ if v <= alpha:
+ return v, move
+ return v, move
+
+ start_time = time.time()
+ try:
+ result = max_value(state, -np.inf, +np.inf, 0, alpha0,
+ beta0, gamma0, theta0, epsilon0)[-1]
+ except TimeoutError as e:
+ result = e.args[0]
+ except WinException as e:
+ result = e.args[0]
+ return result
+
+
+def play_game(alpha0, beta0, gamma0, theta0, epsilon0, cod, max_moves, name: str, team: str, server_ip: str, timeout: int):
+ # Clear the screen
+ # os.system('cls' if os.name == 'nt' else 'clear')
+
+ # Initialize game
+ game = Tablut()
+
+ cond = threading.Condition()
+
+ # Initialize network
+ network = Network(name, team, server_ip, timeout=timeout)
+
+ # Get initial state and turn
+ pieces, turn = network.connect()
+
+ game.update_state(pieces, turn)
+
+ # Play game
+ state = game.initial
+ turns = 0
+ while turns < max_moves:
+ turns += 1
+ with cond:
+ while not network.check_turn(player=team):
+ # print('Waiting for opponent move...')
+ cond.wait(timeout=1)
+ pieces, turn = network.get_state()
+ # print(pieces, turn)
+ if type(pieces) != int:
+
+ # Update the game state for the current player
+ game.update_state(pieces, turn)
+ else:
+ return pieces, turns
+
+ # Get move
+
+ move = h_alphabeta_search(
+ state, game, alpha0, beta0, gamma0, theta0, epsilon0, cod, time_limit=55)
+
+ # Send move to server
+ # print("OLD STATE:\n", state)
+ # print("MOVE BEFORE CONVERSIONE: ", move)
+ converted_move = game.convert_move(move)
+ # print(
+ # f"Move: {converted_move}")
+ network.send_move(converted_move)
+ # state.display()
+ # print(f"Old state: {state}")
+ try:
+ pieces, turn = network.get_state()
+ except:
+ return 3, turns
+ if type(pieces) != int:
+ # Update the game state for the current player
+ game.update_state(pieces, turn)
+ else:
+ return pieces, turns
+
+ # Update the game state for the current player
+ game.update_state(pieces, turn)
+
+ # print(f"New state: {game.initial}")
+
+ # Update state
+ state = copy.copy(game)
+ state = state.initial
+
+ # Notify the other thread
+ cond.notify_all()
+ if turns >= max_moves:
+ return 3, turns
+
+
+if __name__ == "__main__":
+ argparse = argparse.ArgumentParser()
+
+ argparse.add_argument(
+ "--team", help="The color of the player: WHITE or BLACK", type=str, choices=["WHITE", "BLACK"], required=True),
+ argparse.add_argument(
+ "--name", help="The name of the player", type=str, default='\tLut')
+ argparse.add_argument(
+ "--ip", help="The IP address of the server", type=str, default="localhost")
+ argparse.add_argument(
+ "--timeout", help="The timeout for the server", type=int, default=55)
+ args = argparse.parse_args()
+
+ alpha0 = -10
+ beta0 = 0.05
+ gamma0 = 1
+ theta0 = 2
+ epsilon0 = 15
+ max_moves = 30
+ cod = cutoff_depth(2)
+ result, turns = play_game(alpha0, beta0, gamma0, theta0, epsilon0,
+ cod, max_moves, name=args.name, team=args.team, server_ip=args.ip, timeout=args.timeout)
+ # If result = 0, white wins
+ # If result = 1, black wins
+ # If result = 2, draw
+ # If result = 3, max moves reached
+
+ print(result)
+ print(turns)
\ No newline at end of file
diff --git a/blackplayer/requirements.txt b/blackplayer/requirements.txt
new file mode 100644
index 0000000..daf6cb6
--- /dev/null
+++ b/blackplayer/requirements.txt
@@ -0,0 +1,5 @@
+aima
+numpy
+
+# docs
+mkdocs-material
\ No newline at end of file
diff --git a/blackplayer/runmyplayer.sh b/blackplayer/runmyplayer.sh
new file mode 100644
index 0000000..6befcc0
--- /dev/null
+++ b/blackplayer/runmyplayer.sh
@@ -0,0 +1,12 @@
+#!/bin/bash
+
+if [[ $# -ne 3 ]] ; then
+ echo "Need all arguments"
+ echo "white or black as first parameter"
+ echo "timeout as second parameter"
+ echo "server ip as third parameter"
+ echo "Example: $0 WHITE 60 192.168.20.254"
+ exit 1
+fi
+
+python3 /home/tablut/tablut/play.py --team "$1" --timeout "$2" --ip "$3"
\ No newline at end of file
diff --git a/blackplayer/tablut.py b/blackplayer/tablut.py
new file mode 100644
index 0000000..dd2919a
--- /dev/null
+++ b/blackplayer/tablut.py
@@ -0,0 +1,500 @@
+import copy
+import random
+
+# numpy
+import numpy as np
+
+# AIMA
+from aima.games import Game
+
+# Board class
+from board import Board
+
+# utils
+from utils import Pawn
+
+# heuristics
+from whiteheuristics import white_fitness # white_fitness_dynamic
+from blackheuristics import black_fitness # black_fitness_dynamic
+
+
+class Tablut(Game):
+ def __init__(self, height: int = 9, width: int = 9):
+ """
+ Initializes a Tablut game object.
+
+ Parameters:
+ - height (int): The height of the game board. Default is 9.
+ - width (int): The width of the game board. Default is 9.
+ """
+ self.initial = Board(height=height, width=width,
+ to_move='WHITE', utility=0)
+
+ self.width = width
+ self.height = height
+
+ def update_state(self, pieces, turn):
+ """
+ Update the state of the board.
+
+ Args:
+ pieces (list): The positions of the pieces on the board.
+ turn (str): The current turn ('WHITE' or 'BLACK').
+
+ Returns:
+ None
+ """
+ # Update board state
+ self.initial.pieces = pieces
+ self.initial.to_move = turn
+ self.to_move = turn
+
+ # Update pawns coordinates
+ white_pos = self.initial.get_white()
+ black_pos = self.initial.get_black()
+ king_pos = self.initial.get_king()
+
+ # Shuffle the list of possible moves
+ random.shuffle(white_pos)
+
+ # White has also the king
+ white_pos.insert(0, king_pos)
+
+ # Get the current player and compute the list of possible moves
+ if turn == 'WHITE':
+ self.squares = [[x, (k, l)] for x in white_pos for k in range(
+ self.width) for l in range(self.height)]
+ elif turn == 'BLACK':
+ self.squares = [[x, (k, l)] for x in black_pos for k in range(
+ self.width) for l in range(self.height)]
+
+ def move(self, move):
+ """
+ Moves a pawn on the board according to the given move.
+
+ Args:
+ move (tuple): A tuple containing the starting and ending positions of the move.
+
+ Returns:
+ Tablut: The updated Tablut object after the move has been made.
+ """
+ # Extract the starting and ending position from the move
+ from_pos, to_pos = move
+ x1, y1 = from_pos
+ x2, y2 = to_pos
+
+ pawn_type = self.initial.pieces[x1][y1]
+
+ new_board = copy.deepcopy(self.initial.pieces)
+
+ # Get the pawn type
+ if pawn_type == Pawn.EMPTY.value or pawn_type == Pawn.THRONE.value:
+ self.initial.pieces = new_board
+ return self
+
+ if self.initial.pieces[x2][y2] != Pawn.EMPTY.value:
+ self.initial.pieces = new_board
+ return self
+
+ if x1 == 4 and y1 == 4:
+ new_board[x1][y1] = Pawn.THRONE.value
+ else:
+ new_board[x1][y1] = Pawn.EMPTY.value
+
+ # Remove the pawn from the starting position and add it to the ending position
+ new_board[x2][y2] = pawn_type
+
+ # Check if there are any captures (important otherwise heuristics won't work)
+ self.initial.check_attacks(x2, y2)
+
+ self.initial.pieces = new_board
+
+ # Change turn
+ self.initial.to_move = (
+ "BLACK" if self.initial.to_move == "WHITE" else "WHITE")
+ return self
+
+ def actions(self, pieces, player, board) -> set:
+ """
+ Returns a set of allowed moves for the current player.
+
+ Args:
+ pieces (numpy.ndarray): The current state of the game board.
+ player (str): The current player ('WHITE' or 'BLACK').
+ board (numpy.ndarray): The current state of the game board.
+
+ Returns:
+ set: A set of allowed moves for the current player.
+ """
+ pawns = np.where(pieces == Pawn.WHITE.value)
+ coordinates = list(zip(pawns[0], pawns[1]))
+ white = coordinates
+
+ pawns = np.where(pieces == Pawn.BLACK.value)
+ coordinates = list(zip(pawns[0], pawns[1]))
+ black = coordinates
+
+ pawns = np.where(pieces == Pawn.KING.value)
+ coordinates = list(zip(pawns[0], pawns[1]))
+ king = coordinates[0]
+
+ white.insert(0, king)
+
+ throne = [(4, 4)]
+
+ # print("White pieces:", white)
+
+ # Get the list of the opponent pieces
+ if player == 'WHITE':
+ player_pieces = white
+ opponent_pieces = black
+
+ elif player == 'BLACK':
+ player_pieces = black
+ opponent_pieces = white
+
+ # Occupied squares as the set of tuples of white, black and king pieces
+ occupied_squares = player_pieces + opponent_pieces + throne
+
+ # Get the list of the occupied squares in coordinates (x, y)
+ occupied_squares = set(map(tuple, occupied_squares))
+ # throne is always an occupied square (even if it is empty) | you can't go through it
+
+ # Initialize forbidden moves set
+ forbidden_moves = set()
+
+ # Starting position (tuple) and ending position (tuple)
+ for move in self.squares:
+ from_pos = move[0]
+ from_row, from_col = from_pos
+
+ to_pos = move[1]
+ to_row, to_col = to_pos
+
+ for occ_place in occupied_squares:
+ forbidden_moves.add(
+ (from_pos, occ_place)) # occupied squares
+
+ forbidden_moves.add((from_pos, from_pos)) # starting position
+
+ # Can't move enemy's pawn
+ if from_pos in opponent_pieces:
+ forbidden_moves.add((from_pos, to_pos))
+
+ # remove diagonal moves
+ if from_row != to_row and from_col != to_col:
+ forbidden_moves.add((from_pos, to_pos))
+
+ # remove moves which implies jumping over a piece
+ # The idea is to check in a "circular" way the perimeter around the piece
+ # So it only needs one for-loop.
+ # If flags are not set, the move is legal. If the flag is set (= i'm on a piece or over it)
+ # Add the move to the forbiddens one
+
+ flags = [
+ False,
+ False,
+ False,
+ False
+ ]
+
+ # If i move from outside a barrack inside a barrack, that move is invalid. But i can move freely move inside barracks
+ from utils import RED, RED2
+ for i in range(1, self.width):
+ if from_col - i >= 0:
+ if (flags[0] or (from_row, from_col - i) in occupied_squares) or \
+ (board[from_row][from_col] not in (RED, RED2) and board[from_row][from_col - i] in (RED, RED2)):
+
+ flags[0] = True
+ forbidden_moves.add(
+ (from_pos, (from_row, from_col - i)))
+
+ if from_row - i >= 0:
+ if flags[1] or (from_row - i, from_col) in occupied_squares or \
+ (board[from_row][from_col] not in (RED, RED2) and board[from_row - i][from_col] in (RED, RED2)):
+
+ flags[1] = True
+ forbidden_moves.add(
+ (from_pos, (from_row - i, from_col)))
+
+ if from_col + i < self.width:
+ if flags[2] or (from_row, from_col+i) in occupied_squares or \
+ (board[from_row][from_col] not in (RED, RED2) and board[from_row][from_col + i] in (RED, RED2)):
+
+ flags[2] = True
+ forbidden_moves.add(
+ (from_pos, (from_row, from_col+i)))
+
+ if from_row + i < self.width:
+ if flags[3] or (from_row + i, from_col) in occupied_squares or \
+ (board[from_row][from_col] not in (RED, RED2) and board[from_row + i][from_col] in (RED, RED2)):
+
+ flags[3] = True
+ forbidden_moves.add(
+ (from_pos, (from_row + i, from_col)))
+
+ # Barrack's check
+ barracks = (
+ (
+ (0, 3),
+ (0, 4),
+ (0, 5),
+ (1, 4)
+ ),
+ (
+ (3, 0),
+ (4, 0),
+ (5, 0),
+ (4, 1)
+ ),
+ (
+ (self.width-1, 3),
+ (self.width-1, 4),
+ (self.width-1, 5),
+ (self.width-2, 4)
+ ),
+ (
+ (3, self.width-1),
+ (4, self.width-1),
+ (5, self.width-1),
+ (4, self.width-2),
+ )
+ )
+
+ # return difference between all possible moves and forbidden moves (list of tuples)
+ total_moves = set(tuple(tuple(k) for k in h)
+ for h in self.squares)
+
+ allowed_moves = total_moves - forbidden_moves
+ return allowed_moves
+
+ def result(self, state, move, alpha0, beta0, gamma0, theta0, epsilon0, flag: bool = False):
+ """
+ Apply the given move to the state and return the resulting state.
+
+ Parameters:
+ - state: The current state of the game.
+ - move: The move to be applied to the state.
+ - alpha0, beta0, gamma0, theta0, epsilon0: Parameters for computing the utility of the board.
+ - flag: A flag indicating whether the move should be applied to the state or not. Default is False.
+
+ Returns:
+ The resulting state after applying the move.
+ """
+ # Apply the move locally and check for captures
+ # print("OLD STATE BEFORE MOVE\n", state)
+ game = Tablut()
+
+ if isinstance(state, Tablut):
+ game.initial.pieces = copy.deepcopy(state.initial.pieces)
+ elif isinstance(state, Board):
+ game.initial.pieces = copy.deepcopy(state.pieces)
+
+ game.initial.get_white()
+ game.initial.get_black()
+ game.initial.get_king()
+
+ # print("KING POSITION:", game.initial.get_king())
+
+ if flag is False:
+ game = game.move(move)
+
+ # print("KING POSITION:", game.initial.get_king())
+
+ # print("NEW STATE AFTER MOVE\n", game.initial)
+
+ game.update_state(game.initial.pieces, game.initial.to_move)
+
+ # print("STATE AFTER GAME UPDATE\n", game.initial)
+
+ win = self.terminal_test(game.initial)
+
+ # Compute the utility of the board
+ fitness = self.compute_utility(
+ game.initial, move, alpha0, beta0, gamma0, theta0, epsilon0, player=game.initial.to_move)
+
+ # Update the utility of the board
+ game.initial.utility = (
+ 0 if not win else fitness if game.initial.to_move == 'WHITE' else -fitness)
+
+ # return the new board
+ return game.initial
+
+ def utility(self, board, player):
+ """Return the value to player; 1 for win, -1 for loss, 0 otherwise.
+
+ Args:
+ board (Board): The current game board.
+ player (str): The player for whom to calculate the utility.
+
+ Returns:
+ int: The utility value for the specified player.
+ """
+ return board.utility if player == 'WHITE' else -board.utility
+
+ def terminal_test(self, board):
+ """
+ Check if the current board state is a terminal state.
+
+ Parameters:
+ - board: The current board state.
+
+ Returns:
+ - True if the game is in a terminal state (winning move or no more moves), False otherwise.
+ """
+ return self.check_win(board)
+
+ def compute_utility(self, board, move, alpha0, beta0, gamma0, theta0, epsilon0, player) -> float:
+ """
+ alpha : is_white_gonna_be_eaten
+ beta : external_pawn_more_fitness
+ gamma : white_pieces
+ theta : black_pieces
+ epsilon : king_movement
+ """
+
+ if player == 'WHITE':
+ # print(move, board.get_king())
+ if move and move[0] == board.get_king() and move[1] in board.winning_positions:
+ return 1e10
+
+ # Additional heuristics
+ fitness = white_fitness(board, alpha0, beta0,
+ gamma0, theta0, epsilon0)
+ # print(f"Fitness: {fitness}")
+
+ elif player == 'BLACK':
+
+ fitness = black_fitness(
+ board, alpha0, beta0, gamma0, theta0, epsilon0)
+ # print(f"Fitness: {fitness}")
+
+ return fitness
+
+ def check_win(self, state):
+ """
+ End of game:
+ - King captured (BLACK wins)
+ - King escaped (WHITE wins)
+ - A player canβt move any checker in any direction: that player loses
+ - The same "state" of the game is reached twice: draw
+ """
+
+ if isinstance(state, Board):
+ white_pieces = state.get_white()
+ black_pieces = state.get_black()
+ king_pieces = state.get_king()
+
+ elif isinstance(state, Tablut):
+ white_pieces = state.initial.get_white()
+ black_pieces = state.initial.get_black()
+ king_pieces = state.initial.get_king()
+
+ # No more white pawns
+ if len(white_pieces) == 0:
+ return True
+
+ # No more black pawns
+ if len(black_pieces) == 0:
+ return True
+
+ # King is dead (captured)
+ if king_pieces is None:
+ return True # king is dead
+
+ return False
+
+ def convert_move(self, move):
+ """Convert move to (A1, A2) format
+ Move: ((x1, y1), (x2, y2))
+ Example: ((0,3), (3,3))
+ Convert to: (D1, D4)"""
+
+ cols = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I']
+
+ from_pos = move[0]
+ x1, y1 = from_pos
+
+ to_pos = move[1]
+ x2, y2 = to_pos
+
+ from_letter = cols[y1]
+ to_letter = cols[y2]
+
+ new_from_row = x1 + 1
+ new_to_row = x2 + 1
+
+ return (from_letter+str(new_from_row), to_letter+str(new_to_row))
+
+ def display(self, board):
+ board.display()
+
+ # Black heuristics
+ def moves_to_eat_king(self):
+ '''
+ If the king can be eaten, self.black_moves_to_eat_king is set to a list of initial and final position of the white piece that eats
+ Otherwise, self.black_moves_to_eat_king is set to [[-1,-1], [-1,-1]]
+ '''
+
+ if self.initial.get_king() == (4, 4):
+ self.black_moves_to_eat_king = self.eat_king_in_castle()
+ else:
+ self.black_moves_to_eat_king = self.eat_king_outside_castle()
+
+ def eat_king_in_castle(self):
+ '''
+ It returns starting and ending position of the piece that can eat the king, otherwise it returns [-1,-1], [-1,-1]
+ '''
+ position = self.check_sourrounded_king_castle()
+ if position == [-1, -1]:
+ return [[-1, -1], [-1, -1]]
+ for black in self.initial.blacks:
+ if ((black[0], black[1]), (position[0], position[1])) in self.initial.moves:
+ return black, position
+ return [[-1, -1], [-1, -1]]
+
+ def check_sourrounded_king_castle(self):
+ '''
+ It returns the coordinates of one of the four tiles around the king, if the other three are occupied by three blacks
+ It returns [-1,-1] if the king is not sorrounded or he's not in the castle
+ '''
+ if self.initial.pieces[4][3] == 1 and self.initial.pieces[3][4] == 1 and self.initial.pieces[4][5] == 1:
+ return [5, 4]
+ if self.initial.pieces[4][3] == 1 and self.initial.pieces[5][4] == 1 and self.initial.pieces[4][5] == 1:
+ return [3, 4]
+ if self.initial.pieces[5][4] == 1 and self.initial.pieces[3][4] == 1 and self.initial.pieces[4][5] == 1:
+ return [4, 3]
+ if self.initial.pieces[4][3] == 1 and self.initial.pieces[3][4] == 1 and self.initial.pieces[5][4] == 1:
+ return [4, 5]
+ return [-1, -1]
+
+ def check_sourrounded_king_outside(self):
+ '''
+ It returns a list of possible moves to eat the king, when he's outside the castle
+ If there isn't any feasable move, it returns [-1,-1]
+ '''
+ r, c = self.initial.get_king()
+ tiles = []
+ if self.initial.pieces[r][c-1] == 1:
+ tiles.append([r, c+1])
+ elif self.initial.pieces[r][c+1] == 1:
+ tiles.append([r, c-1])
+ if self.initial.pieces[r-1][c] == 1:
+ tiles.append([r+1][c])
+ elif self.initial.pieces[r+1][c] == 1:
+ tiles.append([r-1][c])
+ return tiles if len(tiles) > 0 else [-1, -1]
+
+ def eat_king_outside_castle(self):
+ '''
+ It returns starting and ending position of the piece that can eat the king, otherwise it returns [[-1,-1], [-1,-1]]
+ '''
+ position = self.check_sourrounded_king_outside()
+ if position == [-1, -1]:
+ return [[-1, -1], [-1, -1]]
+ for black in self.initial.blacks:
+ if ((black[0], black[1]), (position[0][0], position[0][1])) in self.initial.moves:
+ return [black, [position[0][0], position[0][1]]]
+ if len(position) == 2 and ((black[0], black[1]), (position[1][0], position[1][1])) in self.initial.moves:
+ return [black, [position[1][0], position[1][1]]]
+ return [[-1, -1], [-1, -1]]
diff --git a/blackplayer/utils.py b/blackplayer/utils.py
new file mode 100644
index 0000000..e77368d
--- /dev/null
+++ b/blackplayer/utils.py
@@ -0,0 +1,138 @@
+import json
+import socket
+import sys
+import struct
+from enum import Enum
+
+import numpy as np
+
+from concurrent.futures._base import TimeoutError
+
+######## COSTANTS ########
+GRAY = (150, 150, 150)
+WHITE = (200, 200, 200)
+WHITE2 = (180, 180, 180)
+RED = (255, 0, 0)
+RED2 = (200, 0, 0)
+GREEN = (0, 255, 0)
+GREEN2 = (0, 200, 0)
+BLUE = (0, 0, 255)
+##########################
+
+
+class Pawn(Enum):
+ EMPTY = 0
+ WHITE = 1
+ BLACK = 2
+ KING = 3
+ THRONE = 4
+
+
+class WinException(Exception):
+ pass
+
+
+class Converter:
+ def json_to_matrix(self, json_state):
+ data = list(json_state.items())
+ board, turn = data[0], data[1]
+
+ if isinstance(board, tuple):
+ board = board[1]
+
+ if isinstance(turn, tuple):
+ turn = turn[1]
+
+ board = np.vectorize(lambda x: Pawn[x].value)(board)
+ board = board.reshape(9, 9)
+
+ return board, turn
+
+
+class Network:
+ def __init__(self, name, player, server_ip='localhost', converter=None, sock=None, timeout=60):
+ if not sock:
+ self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ else:
+ self.sock = sock
+
+ if converter:
+ self.converter = converter
+ else:
+ self.converter = Converter()
+
+ self.server_ip = server_ip
+ self.name = name
+ self.player = player
+ self.timeout = timeout
+
+ def recvall(self, n):
+ # Helper function to recv n bytes or return None if EOF is hit
+ data = b''
+ while len(data) < n:
+ packet = self.sock.recv(n - len(data))
+ if not packet:
+ return None
+ data += packet
+ return data
+
+ def connect(self):
+ if self.player == 'WHITE':
+ # Connect the socket to the port where the server is listening
+ server_address = (self.server_ip, 5800)
+ elif self.player == 'BLACK':
+ # Connect the socket to the port where the server is listening
+ server_address = (self.server_ip, 5801)
+ else:
+ raise ConnectionError("Player must be WHITE or BLACK!")
+
+ # Establish a connection with the server
+ self.sock.connect(server_address)
+
+ # Send the player's name to the server
+ self.sock.send(struct.pack('>i', len(self.name)))
+ self.sock.send(self.name.encode())
+
+ return self.get_state()
+
+ def get_state(self):
+ len_bytes = struct.unpack('>i', self.recvall(4))[0]
+ current_state_server_bytes = self.sock.recv(len_bytes)
+
+ # Converting byte into json
+ json_current_state_server = json.loads(current_state_server_bytes)
+
+ state, turn = self.converter.json_to_matrix(json_current_state_server)
+
+ if not turn in ['WHITEWIN', 'BLACKWIN', 'DRAW']:
+
+ self.turn = turn
+ self.state = state
+
+ return state, turn
+
+ else:
+ if turn == 'WHITEWIN':
+ return 0, "WHITE WINS!"
+ sys.exit(0)
+
+ elif turn == 'BLACKWIN':
+ return 1, "BLACK WINS!"
+ sys.exit(0)
+
+ elif turn == 'DRAW':
+ return 2, "DRAW!"
+ sys.exit(0)
+
+ def send_move(self, move):
+ _from, _to = move
+ turn = self.player
+
+ move = json.dumps({"from": _from, "to": _to, "turn": turn})
+
+ self.sock.send(struct.pack('>i', len(move)))
+ self.sock.send(move.encode())
+ return move
+
+ def check_turn(self, player):
+ return self.turn == player
diff --git a/blackplayer/whiteheuristics.py b/blackplayer/whiteheuristics.py
new file mode 100644
index 0000000..befcfbf
--- /dev/null
+++ b/blackplayer/whiteheuristics.py
@@ -0,0 +1,106 @@
+from utils import RED, RED2, BLUE, Pawn
+import copy
+
+
+def can_this_tile_be_reached_by_a_black_pawn(board, x, y):
+ if x < 0 or x >= len(board.pieces):
+ return False
+ if y < 0 or y >= len(board.pieces):
+ return False
+ for i in range(0, x+1):
+ if board.pieces[i][y] == 1 and board._is_there_a_clear_view([x, y], [i, y]):
+ return True
+ for i in range(x, len(board.pieces)):
+ if board.pieces[i][y] == 1 and board._is_there_a_clear_view([x, y], [i, y]):
+ return True
+ for j in range(0, y+1):
+ if board.pieces[x][j] == 1 and board._is_there_a_clear_view([x, y], [x, j]):
+ return True
+ for j in range(y, len(board.pieces)):
+ if board.pieces[x][j] == 1 and board._is_there_a_clear_view([x, y], [x, j]):
+ return True
+ return False
+
+
+def king_distance_from_center(king):
+ return ((king[0] - 4)**2 + (king[1] - 4)**2)**0.5
+
+
+def king_surrounded(board):
+ king = board.king
+ c = 0
+ blocked_pos = []
+ try:
+ if board.pieces[king[0]+1][king[1]] == Pawn.BLACK.value:
+ c += 1
+ blocked_pos.append((king[0]+1, king[1]))
+ except:
+ pass
+ try:
+ if board.pieces[king[0]-1][king[1]] == Pawn.BLACK.value:
+ c += 1
+ blocked_pos.append((king[0]-1, king[1]))
+ except:
+ pass
+ try:
+ if board.pieces[king[0]][king[1]+1] == Pawn.BLACK.value:
+ c += 1
+ blocked_pos.append((king[0], king[1]+1))
+ except:
+ pass
+ try:
+ if board.pieces[king[0]][king[1]-1] == Pawn.BLACK.value:
+ c += 1
+ blocked_pos.append((king[0], king[1]-1))
+ except:
+ pass
+ return c, blocked_pos
+
+
+weights = [[0, 20, 20, -6, -6, -6, 20, 20, 0],
+ [20, 1, 1, -5, -6, -5, 1, 1, 20],
+ [20, 1, 4, 1, -2, 1, 4, 1, 20],
+ [-6, -5, 1, 1, 1, 1, 1, -5, -6],
+ [-6, -6, -2, 1, 2, 1, -2, -6, -6],
+ [-6, -5, 1, 1, 1, 1, 1, -5, -6],
+ [20, 1, 4, 1, -2, 1, 4, 1, 20],
+ [20, 1, 1, -5, -6, -5, 1, 1, 20],
+ [0, 20, 20, -6, -6, -6, 20, 20, 0]]
+
+
+def position_weight(king):
+ global weights
+ return weights[king[0]][king[1]]
+
+
+def white_fitness(board, alpha0, beta0, gamma0, theta0, epsilon0):
+ """
+ Returns the float value of the current state of the board for white
+ """
+
+
+ with open("blackbest", "r") as f:
+ line = f.readline()
+ line = line.replace("[", "")
+ line = line.replace("]", "")
+ line = line.replace(" ", "")
+ line = line.split(",")
+ line = [float(x) for x in line]
+
+ alpha0, beta0, gamma0, theta0, epsilon0 = line
+
+ fitness = 0
+ # Whitepieces
+ fitness -= alpha0 * len(board.blacks)
+ # Blackpieces
+ fitness += beta0 * len(board.whites)
+
+ # king distance
+ fitness += king_distance_from_center(board.king) * gamma0
+ # king surrounded
+ king_vals, _ = king_surrounded(board)
+ fitness -= king_vals * theta0
+
+ fitness += position_weight(board.king) * epsilon0
+
+ return fitness
diff --git a/genetic.py b/genetic.py
index ea76bc5..326a834 100644
--- a/genetic.py
+++ b/genetic.py
@@ -62,14 +62,14 @@ def create_starting_from_params(alpha, beta, gamma, theta, epsilon, elements=10)
max_moves = 30
cod = cutoff_depth(2)
population = create_starting_population(elements)
-
+population = create_starting_from_params(0.41639120828483156, 0.723587137336777, 9, 1.06923818569000507, 2.115749207248323)
for iteration in range(iterations):
results = []
for pop in population:
print(pop)
alpha0, beta0, gamma0, theta0, epsilon0 = pop
- result, turns = play_game(alpha0, beta0, gamma0, theta0, epsilon0, cod, max_moves, name="Player", team=sys.argv[1], server_ip="127.0.0.1", timeout=60)
+ result, turns = play_game(alpha0, beta0, gamma0, theta0, epsilon0, cod, max_moves, name="whitePlayer", team=sys.argv[1], server_ip="127.0.0.1", timeout=60)
#If result = 0, white wins
#If result = 1, black wins
#If result = 2, draw
@@ -84,11 +84,13 @@ def create_starting_from_params(alpha, beta, gamma, theta, epsilon, elements=10)
results = sorted(results, key=lambda x: x[1], reverse=True)
print("Best fitness: ", results[0][1])
print("Best parameters: ", results[0][0])
- with open("results.txt", "a") as f:
+ with open("resultsWHITE.txt", "a") as f:
f.write("Generation: " + str(iteration) + "\n")
f.write("Best fitness: " + str(results[0][1]) + "\n")
f.write("Best parameters: " + str(results[0][0]) + "\n\n")
+ with open("whitebest", "w") as f:
+ f.write(str(results[0][0]))
#I copy the first element of the population
@@ -124,5 +126,6 @@ def create_starting_from_params(alpha, beta, gamma, theta, epsilon, elements=10)
population = copy.deepcopy(new_population)
+
diff --git a/play.py b/play.py
index a6dde13..6b88331 100644
--- a/play.py
+++ b/play.py
@@ -1,4 +1,7 @@
import argparse
+import os
+import sys
+import random
import time
import copy
@@ -15,8 +18,6 @@
# utils
from utils import Network, WinException
-from whiteheuristics import king_surrounded
-
def cache(function):
"""
@@ -51,7 +52,7 @@ def cutoff_depth(d):
return lambda game, state, depth: depth > d
-def h_alphabeta_search(state, game, cutoff, time_limit=55):
+def h_alphabeta_search(state, game, alpha0, beta0, gamma0, theta0, epsilon0, cutoff, time_limit=55):
"""
Performs a heuristic alpha-beta search to find the best move for a given game state.
@@ -68,21 +69,25 @@ def h_alphabeta_search(state, game, cutoff, time_limit=55):
backtrack_dict = dict()
@cache
- def max_value(state, alpha, beta, depth, action_backtrack=None):
+ def max_value(state, alpha, beta, depth, alpha0, beta0, gamma0, theta0, epsilon0, action_backtrack=None):
nonlocal backtrack_dict
if game.terminal_test(state):
+ # print("TERMINAL STATE REACHED")
return game.utility(state, player), None
if cutoff(game, state, depth):
+ # print(f"CUTOFF at {depth = }")
v = game.compute_utility(
- state, None, player)
+ state, None, alpha0, beta0, gamma0, theta0, epsilon0, player)
+ # print(f"PRINT AL CUTOFF: {v}")
return v, None
if time.time() - start_time > time_limit:
+ # print(f"TIMEOUT at {depth = }, player: {player}")
best_action = max(backtrack_dict, key=backtrack_dict.get)
raise TimeoutError(best_action)
v, move = -np.inf, None
if isinstance(state, Tablut):
pieces = copy.deepcopy(state.initial.pieces)
- board = state.initial.board
+ board = state.initial.board # red2...
elif isinstance(state, Board):
pieces = copy.deepcopy(state.pieces)
board = state.board
@@ -91,29 +96,11 @@ def max_value(state, alpha, beta, depth, action_backtrack=None):
from_pos, to_pos = a
if from_pos == state.get_king() and to_pos in state.winning_positions:
raise WinException(a)
- if from_pos in state.blacks:
- king_pos = state.get_king()
- coef, blocked_pos = king_surrounded(state)
-
- print(coef, blocked_pos)
-
- if king_pos == (4, 4) and coef == 3 and to_pos in [(3, 4), (5, 4), (4, 3), (4, 5)]:
- raise WinException(a)
-
- elif king_pos != (4, 4) and coef > 0:
- new_state = game.result(state, a)
- _, new_blocked_pos = king_surrounded(new_state)
-
- print(new_blocked_pos, "NEW")
-
- # Check if there are two pawns in new_blocked_pos which have same row or same column
- if any(p1[0] == p2[0] or p1[1] == p2[1] and p1 != p2 for p1 in new_blocked_pos for p2 in new_blocked_pos):
- raise WinException(a)
-
action_backtrack = a
backtrack_dict[a] = 0
- v2, _ = min_value(game.result(state, a), alpha,
- beta, depth+1, action_backtrack)
+ v2, _ = min_value(game.result(state, a, alpha0, beta0, gamma0, theta0, epsilon0),
+ alpha, beta, depth+1, alpha0, beta0, gamma0, theta0, epsilon0, action_backtrack)
+ # print(f"Move: {a} | Score: {v2}")
if v2 > v:
v, move = v2, a
alpha = max(alpha, v)
@@ -123,15 +110,19 @@ def max_value(state, alpha, beta, depth, action_backtrack=None):
return v, move
@cache
- def min_value(state, alpha, beta, depth, action_backtrack):
+ def min_value(state, alpha, beta, depth, alpha0, beta0, gamma0, theta0, epsilon0, action_backtrack):
nonlocal backtrack_dict
if game.terminal_test(state):
+ # print("TERMINAL STATE REACHED")
return game.utility(state, player), None
if cutoff(game, state, depth):
+ # print(f"CUTOFF at {depth = }")
v = game.compute_utility(
- state, None, player)
+ state, None, alpha0, beta0, gamma0, theta0, epsilon0, player)
+ # print(f"PRINT AL CUTOFF: {v}")
return v, None
if time.time() - start_time > time_limit:
+ # print(f"TIMEOUT at {depth = }, player: {player}")
best_action = max(backtrack_dict, key=backtrack_dict.get)
raise TimeoutError(best_action)
v, move = +np.inf, None
@@ -142,8 +133,8 @@ def min_value(state, alpha, beta, depth, action_backtrack):
pieces = copy.deepcopy(state.pieces)
board = state.board
for a in game.actions(pieces, player, board):
- v2, _ = max_value(game.result(state, a),
- alpha, beta, depth+1, action_backtrack)
+ v2, _ = max_value(game.result(state, a, alpha0, beta0, gamma0, theta0, epsilon0),
+ alpha, beta, depth+1, alpha0, beta0, gamma0, theta0, epsilon0, action_backtrack)
if v2 < v:
v, move = v2, a
beta = min(beta, v)
@@ -153,7 +144,8 @@ def min_value(state, alpha, beta, depth, action_backtrack):
start_time = time.time()
try:
- result = max_value(state, -np.inf, +np.inf, 0)[-1]
+ result = max_value(state, -np.inf, +np.inf, 0, alpha0,
+ beta0, gamma0, theta0, epsilon0)[-1]
except TimeoutError as e:
result = e.args[0]
except WinException as e:
@@ -161,7 +153,7 @@ def min_value(state, alpha, beta, depth, action_backtrack):
return result
-def play_game(name: str, team: str, server_ip: str, timeout: int):
+def play_game(alpha0, beta0, gamma0, theta0, epsilon0, cod, max_moves, name: str, team: str, server_ip: str, timeout: int):
# Clear the screen
# os.system('cls' if os.name == 'nt' else 'clear')
@@ -180,23 +172,36 @@ def play_game(name: str, team: str, server_ip: str, timeout: int):
# Play game
state = game.initial
- while True:
+ turns = 0
+ while turns < max_moves:
+ turns += 1
with cond:
while not network.check_turn(player=team):
+ # print('Waiting for opponent move...')
cond.wait(timeout=1)
pieces, turn = network.get_state()
+ # print(pieces, turn)
if type(pieces) != int:
+
+ # Update the game state for the current player
game.update_state(pieces, turn)
else:
return pieces, turns
# Get move
+
move = h_alphabeta_search(
- state, game, cutoff_depth(2), time_limit=timeout-5) # 5 seconds of tolerance for sending the move
+ state, game, alpha0, beta0, gamma0, theta0, epsilon0, cod, time_limit=55)
# Send move to server
+ # print("OLD STATE:\n", state)
+ # print("MOVE BEFORE CONVERSIONE: ", move)
converted_move = game.convert_move(move)
+ # print(
+ # f"Move: {converted_move}")
network.send_move(converted_move)
+ # state.display()
+ # print(f"Old state: {state}")
try:
pieces, turn = network.get_state()
except:
@@ -210,26 +215,44 @@ def play_game(name: str, team: str, server_ip: str, timeout: int):
# Update the game state for the current player
game.update_state(pieces, turn)
+ # print(f"New state: {game.initial}")
+
# Update state
state = copy.copy(game)
state = state.initial
# Notify the other thread
cond.notify_all()
+ if turns >= max_moves:
+ return 3, turns
if __name__ == "__main__":
argparse = argparse.ArgumentParser()
argparse.add_argument(
- "--team", help="The color of the player: WHITE or BLACK", type=str.upper, choices=["WHITE", "BLACK"], required=True),
+ "--team", help="The color of the player: WHITE or BLACK", type=str, choices=["WHITE", "BLACK"], required=True),
argparse.add_argument(
"--name", help="The name of the player", type=str, default='\tLut')
argparse.add_argument(
"--ip", help="The IP address of the server", type=str, default="localhost")
argparse.add_argument(
- "--timeout", help="The timeout for the server", type=int, default=60)
+ "--timeout", help="The timeout for the server", type=int, default=55)
args = argparse.parse_args()
- result, turns = play_game(
- name=args.name, team=args.team, server_ip=args.ip, timeout=args.timeout)
+ alpha0 = -10
+ beta0 = 0.05
+ gamma0 = 1
+ theta0 = 2
+ epsilon0 = 15
+ max_moves = 30
+ cod = cutoff_depth(2)
+ result, turns = play_game(alpha0, beta0, gamma0, theta0, epsilon0,
+ cod, max_moves, name=args.name, team=args.team, server_ip=args.ip, timeout=args.timeout)
+ # If result = 0, white wins
+ # If result = 1, black wins
+ # If result = 2, draw
+ # If result = 3, max moves reached
+
+ print(result)
+ print(turns)
\ No newline at end of file
diff --git a/tablut.py b/tablut.py
index e22d8bd..dd2919a 100644
--- a/tablut.py
+++ b/tablut.py
@@ -268,7 +268,7 @@ def actions(self, pieces, player, board) -> set:
allowed_moves = total_moves - forbidden_moves
return allowed_moves
- def result(self, state, move, flag: bool = False):
+ def result(self, state, move, alpha0, beta0, gamma0, theta0, epsilon0, flag: bool = False):
"""
Apply the given move to the state and return the resulting state.
@@ -282,6 +282,7 @@ def result(self, state, move, flag: bool = False):
The resulting state after applying the move.
"""
# Apply the move locally and check for captures
+ # print("OLD STATE BEFORE MOVE\n", state)
game = Tablut()
if isinstance(state, Tablut):
@@ -293,16 +294,24 @@ def result(self, state, move, flag: bool = False):
game.initial.get_black()
game.initial.get_king()
+ # print("KING POSITION:", game.initial.get_king())
+
if flag is False:
game = game.move(move)
+ # print("KING POSITION:", game.initial.get_king())
+
+ # print("NEW STATE AFTER MOVE\n", game.initial)
+
game.update_state(game.initial.pieces, game.initial.to_move)
+ # print("STATE AFTER GAME UPDATE\n", game.initial)
+
win = self.terminal_test(game.initial)
# Compute the utility of the board
fitness = self.compute_utility(
- game.initial, move, player=game.initial.to_move)
+ game.initial, move, alpha0, beta0, gamma0, theta0, epsilon0, player=game.initial.to_move)
# Update the utility of the board
game.initial.utility = (
@@ -335,7 +344,7 @@ def terminal_test(self, board):
"""
return self.check_win(board)
- def compute_utility(self, board, move, player) -> float:
+ def compute_utility(self, board, move, alpha0, beta0, gamma0, theta0, epsilon0, player) -> float:
"""
alpha : is_white_gonna_be_eaten
beta : external_pawn_more_fitness
@@ -350,12 +359,14 @@ def compute_utility(self, board, move, player) -> float:
return 1e10
# Additional heuristics
- fitness = white_fitness(board)
+ fitness = white_fitness(board, alpha0, beta0,
+ gamma0, theta0, epsilon0)
# print(f"Fitness: {fitness}")
elif player == 'BLACK':
- fitness = black_fitness(board)
+ fitness = black_fitness(
+ board, alpha0, beta0, gamma0, theta0, epsilon0)
# print(f"Fitness: {fitness}")
return fitness
diff --git a/whitebest b/whitebest
index e0f66f9..966f1af 100644
--- a/whitebest
+++ b/whitebest
@@ -1 +1 @@
-[0.55, -0.67, 9.05, 0.16, 1.59]
\ No newline at end of file
+[0.41639120828483156, 0.723587137336777, 9, 1.06923818569000507, 2.115749207248323]
\ No newline at end of file
diff --git a/whiteheuristics.py b/whiteheuristics.py
index 11066b4..4c95a9c 100644
--- a/whiteheuristics.py
+++ b/whiteheuristics.py
@@ -73,20 +73,13 @@ def position_weight(king):
return weights[king[0]][king[1]]
-def white_fitness(board):
+def white_fitness(board, alpha0, beta0, gamma0, theta0, epsilon0):
"""
Returns the float value of the current state of the board for white
"""
- # TODO: use the correct weights
- alpha0 = 0
- beta0 = 0
- gamma0 = 0
- theta0 = 0
- epsilon0 = 0
- alpha0, beta0, gamma0, theta0, epsilon0 = [
- 0.41639120828483156, 0.723587137336777, 9, 1.06923818569000507, 2.115749207248323]
+
fitness = 0
# Whitepieces