Created base game with working minimax algorithm, now working on reinforcement learning
This commit is contained in:
commit
1eb0a04f30
162
.gitignore
vendored
Normal file
162
.gitignore
vendored
Normal file
@ -0,0 +1,162 @@
|
|||||||
|
### Python template
|
||||||
|
# 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/
|
||||||
|
|
8
.idea/.gitignore
vendored
Normal file
8
.idea/.gitignore
vendored
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
# Default ignored files
|
||||||
|
/shelf/
|
||||||
|
/workspace.xml
|
||||||
|
# Editor-based HTTP Client requests
|
||||||
|
/httpRequests/
|
||||||
|
# Datasource local storage ignored files
|
||||||
|
/dataSources/
|
||||||
|
/dataSources.local.xml
|
6
.idea/GitLink.xml
Normal file
6
.idea/GitLink.xml
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="uk.co.ben_gibson.git.link.SettingsState">
|
||||||
|
<option name="host" value="e0f86390-1091-4871-8aeb-f534fbc99cf0" />
|
||||||
|
</component>
|
||||||
|
</project>
|
5
.idea/codeStyles/codeStyleConfig.xml
Normal file
5
.idea/codeStyles/codeStyleConfig.xml
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
<component name="ProjectCodeStyleConfiguration">
|
||||||
|
<state>
|
||||||
|
<option name="PREFERRED_PROJECT_CODE_STYLE" value="Default" />
|
||||||
|
</state>
|
||||||
|
</component>
|
10
.idea/draughts.iml
Normal file
10
.idea/draughts.iml
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<module type="PYTHON_MODULE" version="4">
|
||||||
|
<component name="NewModuleRootManager">
|
||||||
|
<content url="file://$MODULE_DIR$">
|
||||||
|
<excludeFolder url="file://$MODULE_DIR$/venv" />
|
||||||
|
</content>
|
||||||
|
<orderEntry type="jdk" jdkName="Python 3.11 (draughts)" jdkType="Python SDK" />
|
||||||
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
|
</component>
|
||||||
|
</module>
|
12
.idea/inspectionProfiles/Project_Default.xml
Normal file
12
.idea/inspectionProfiles/Project_Default.xml
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<component name="InspectionProjectProfileManager">
|
||||||
|
<profile version="1.0">
|
||||||
|
<option name="myName" value="Project Default" />
|
||||||
|
<inspection_tool class="PyStubPackagesAdvertiser" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="ignoredPackages">
|
||||||
|
<list>
|
||||||
|
<option value="pyspark-stubs==3.0.0.post3" />
|
||||||
|
</list>
|
||||||
|
</option>
|
||||||
|
</inspection_tool>
|
||||||
|
</profile>
|
||||||
|
</component>
|
6
.idea/inspectionProfiles/profiles_settings.xml
Normal file
6
.idea/inspectionProfiles/profiles_settings.xml
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<component name="InspectionProjectProfileManager">
|
||||||
|
<settings>
|
||||||
|
<option name="USE_PROJECT_PROFILE" value="false" />
|
||||||
|
<version value="1.0" />
|
||||||
|
</settings>
|
||||||
|
</component>
|
4
.idea/misc.xml
Normal file
4
.idea/misc.xml
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.11 (draughts)" project-jdk-type="Python SDK" />
|
||||||
|
</project>
|
8
.idea/modules.xml
Normal file
8
.idea/modules.xml
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ProjectModuleManager">
|
||||||
|
<modules>
|
||||||
|
<module fileurl="file://$PROJECT_DIR$/.idea/draughts.iml" filepath="$PROJECT_DIR$/.idea/draughts.iml" />
|
||||||
|
</modules>
|
||||||
|
</component>
|
||||||
|
</project>
|
6
.idea/vcs.xml
Normal file
6
.idea/vcs.xml
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="VcsDirectoryMappings">
|
||||||
|
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||||
|
</component>
|
||||||
|
</project>
|
BIN
Report.pdf
Normal file
BIN
Report.pdf
Normal file
Binary file not shown.
0
__init__.py
Normal file
0
__init__.py
Normal file
213
main.py
Normal file
213
main.py
Normal file
@ -0,0 +1,213 @@
|
|||||||
|
import sys
|
||||||
|
|
||||||
|
import pygame
|
||||||
|
|
||||||
|
from utilities.constants import WIDTH, HEIGHT, SQUARE_SIZE, WHITE, GREEN
|
||||||
|
from utilities.gameManager import GameManager
|
||||||
|
from minimax.minimaxAlgo import MiniMax
|
||||||
|
|
||||||
|
FPS = 60
|
||||||
|
WIN = pygame.display.set_mode((WIDTH, HEIGHT))
|
||||||
|
pygame.display.set_caption("Draughts")
|
||||||
|
|
||||||
|
|
||||||
|
def getRowColFromMouse(pos):
|
||||||
|
x, y = pos
|
||||||
|
row = y // SQUARE_SIZE
|
||||||
|
col = x // SQUARE_SIZE
|
||||||
|
return row, col
|
||||||
|
|
||||||
|
|
||||||
|
def drawText(text, font, color, surface, x, y):
|
||||||
|
textobj = font.render(text, 1, color)
|
||||||
|
textrect = textobj.get_rect()
|
||||||
|
textrect.topleft = (x, y)
|
||||||
|
surface.blit(textobj, textrect)
|
||||||
|
|
||||||
|
|
||||||
|
def drawMultiLineText(surface, text, pos, font, color=pygame.Color('black')):
|
||||||
|
words = [word.split(' ') for word in text.splitlines()] # 2D array where each row is a list of words.
|
||||||
|
space = font.size(' ')[0] # The width of a space.
|
||||||
|
max_width, max_height = surface.get_size()
|
||||||
|
x, y = pos
|
||||||
|
word_height = None
|
||||||
|
for line in words:
|
||||||
|
for word in line:
|
||||||
|
word_surface = font.render(word, 0, color)
|
||||||
|
word_width, word_height = word_surface.get_size()
|
||||||
|
if x + word_width >= max_width:
|
||||||
|
x = pos[0] # Reset the x.
|
||||||
|
y += word_height # Start on new row.
|
||||||
|
surface.blit(word_surface, (x, y))
|
||||||
|
x += word_width + space
|
||||||
|
x = pos[0] # Reset the x.
|
||||||
|
y += word_height # Start on new row.
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
pygame.init()
|
||||||
|
screen = pygame.display.set_mode((WIDTH, HEIGHT))
|
||||||
|
menuClock = pygame.time.Clock()
|
||||||
|
click = False
|
||||||
|
width = screen.get_width()
|
||||||
|
font = pygame.font.SysFont(None, 25)
|
||||||
|
difficulty = 0
|
||||||
|
|
||||||
|
while True:
|
||||||
|
# menu
|
||||||
|
screen.fill((128, 128, 128))
|
||||||
|
drawText('Main Menu', font, (255, 255, 255), screen, width / 2, 20)
|
||||||
|
|
||||||
|
mx, my = pygame.mouse.get_pos()
|
||||||
|
|
||||||
|
easy = pygame.Rect(width / 2 - 50, 100, 200, 50)
|
||||||
|
pygame.draw.rect(screen, (0, 255, 0), easy)
|
||||||
|
drawText("easy", font, (255, 255, 255), screen, width / 2, 100)
|
||||||
|
medium = pygame.Rect(width / 2 - 50, 200, 200, 50)
|
||||||
|
pygame.draw.rect(screen, (255, 125, 0), medium)
|
||||||
|
drawText("medium", font, (255, 255, 255), screen, width / 2, 200)
|
||||||
|
hard = pygame.Rect(width / 2 - 50, 300, 200, 50)
|
||||||
|
pygame.draw.rect(screen, (255, 0, 0), hard)
|
||||||
|
drawText("hard", font, (255, 255, 255), screen, width / 2, 300)
|
||||||
|
rules = pygame.Rect(width / 2 - 50, 400, 200, 50)
|
||||||
|
pygame.draw.rect(screen, (0, 0, 255), rules)
|
||||||
|
drawText("rules", font, (255, 255, 255), screen, width / 2, 400)
|
||||||
|
quitGame = pygame.Rect(width / 2 - 50, 500, 200, 50)
|
||||||
|
pygame.draw.rect(screen, (0, 0, 0), quitGame)
|
||||||
|
drawText("quit", font, (255, 255, 255), screen, width / 2, 500)
|
||||||
|
|
||||||
|
if easy.collidepoint((mx, my)):
|
||||||
|
if click:
|
||||||
|
difficulty = 1
|
||||||
|
break
|
||||||
|
if medium.collidepoint((mx, my)):
|
||||||
|
if click:
|
||||||
|
difficulty = 3
|
||||||
|
break
|
||||||
|
if hard.collidepoint((mx, my)):
|
||||||
|
if click:
|
||||||
|
difficulty = 5
|
||||||
|
break
|
||||||
|
if rules.collidepoint((mx, my)):
|
||||||
|
if click:
|
||||||
|
rulesGUI()
|
||||||
|
break
|
||||||
|
if quitGame.collidepoint((mx, my)):
|
||||||
|
if click:
|
||||||
|
pygame.quit()
|
||||||
|
sys.exit()
|
||||||
|
click = False
|
||||||
|
for event in pygame.event.get():
|
||||||
|
if event.type == pygame.QUIT:
|
||||||
|
pygame.quit()
|
||||||
|
sys.exit()
|
||||||
|
if event.type == pygame.MOUSEBUTTONDOWN:
|
||||||
|
if event.button == 1:
|
||||||
|
click = True
|
||||||
|
|
||||||
|
pygame.display.update()
|
||||||
|
menuClock.tick(60)
|
||||||
|
if difficulty != 0:
|
||||||
|
game(difficulty)
|
||||||
|
|
||||||
|
|
||||||
|
def rulesGUI():
|
||||||
|
screen = pygame.display.set_mode((WIDTH, HEIGHT))
|
||||||
|
menuClock = pygame.time.Clock()
|
||||||
|
click = False
|
||||||
|
width = screen.get_width()
|
||||||
|
titleFont = pygame.font.SysFont(None, 48)
|
||||||
|
font = pygame.font.SysFont(None, 21)
|
||||||
|
while True:
|
||||||
|
screen.fill((128, 128, 128))
|
||||||
|
drawText("Rules", titleFont, (255, 255, 255), screen, width / 2, 20)
|
||||||
|
|
||||||
|
mx, my = pygame.mouse.get_pos()
|
||||||
|
drawMultiLineText(screen, """Both the player and AI start with 12 pieces on the dark squares of the three rows closest to that
|
||||||
|
player's side. The row closest to each player is called the kings row or crownhead. The player moves first.
|
||||||
|
Then turns alternate.
|
||||||
|
\n
|
||||||
|
Move rules
|
||||||
|
\n
|
||||||
|
There are two different ways to move in utilities:
|
||||||
|
\n
|
||||||
|
Simple move: A simple move consists of moving a piece one square diagonally to an adjacent unoccupied dark square.
|
||||||
|
Uncrowned pieces can move diagonally forward only; kings can move in any diagonal direction. Jump: A jump consists of
|
||||||
|
moving a piece that is diagonally adjacent an opponent's piece, to an empty square immediately beyond it in the same
|
||||||
|
direction (thus "jumping over" the opponent's piece front and back ). Pieces can jump diagonally forward only; kings
|
||||||
|
can jump in any diagonal direction. A jumped piece is considered "captured" and removed from the game. Any piece,
|
||||||
|
king or piece, can jump a king.
|
||||||
|
\n
|
||||||
|
Forced capture, is always mandatory: if a player has the option to jump, he/she must take it, even if doing so
|
||||||
|
results in disadvantage for the jumping player. For example, a piecedated single jump might set up the player such
|
||||||
|
that the opponent has a multi-jump in reply.
|
||||||
|
\n
|
||||||
|
Multiple jumps are possible, if after one jump, another piece is immediately eligible to be jumped by the moved
|
||||||
|
piece—even if that jump is in a different diagonal direction. If more than one multi-jump is available, the player
|
||||||
|
can choose which piece to jump with, and which sequence of jumps to make. The sequence chosen is not required to be
|
||||||
|
the one that maximizes the number of jumps in the turn; however, a player must make all available jumps in the
|
||||||
|
sequence chosen. Kings If a piece moves into the kings row on the opponent's side of the board, it is crowned as a
|
||||||
|
king and gains the ability to move both forward and backward. If a piece moves into the kings row or if it jumps into
|
||||||
|
the kings row, the current move terminates; the piece is crowned as a king but cannot jump back out as in a
|
||||||
|
multi-jump until the next move.""", (50, 50), font)
|
||||||
|
back = pygame.Rect(width / 2 - 50, 700, 200, 50)
|
||||||
|
pygame.draw.rect(screen, (0, 0, 0), back)
|
||||||
|
drawText("back", font, (255, 255, 255), screen, width / 2, 700)
|
||||||
|
|
||||||
|
if back.collidepoint((mx, my)):
|
||||||
|
if click:
|
||||||
|
main()
|
||||||
|
break
|
||||||
|
|
||||||
|
for event in pygame.event.get():
|
||||||
|
if event.type == pygame.QUIT:
|
||||||
|
pygame.quit()
|
||||||
|
sys.exit()
|
||||||
|
if event.type == pygame.MOUSEBUTTONDOWN:
|
||||||
|
if event.button == 1:
|
||||||
|
click = True
|
||||||
|
|
||||||
|
pygame.display.update()
|
||||||
|
menuClock.tick(60)
|
||||||
|
|
||||||
|
|
||||||
|
def game(difficulty):
|
||||||
|
run = True
|
||||||
|
clock = pygame.time.Clock()
|
||||||
|
gameManager = GameManager(WIN, GREEN)
|
||||||
|
|
||||||
|
while run:
|
||||||
|
clock.tick(FPS)
|
||||||
|
|
||||||
|
if gameManager.turn == WHITE:
|
||||||
|
mm = MiniMax()
|
||||||
|
value, newBoard = mm.AI(gameManager.getBoard(), difficulty, WHITE, gameManager)
|
||||||
|
gameManager.aiMove(newBoard)
|
||||||
|
# time.sleep(0.15)
|
||||||
|
|
||||||
|
if gameManager.turn == GREEN:
|
||||||
|
mm = MiniMax()
|
||||||
|
value, newBoard = mm.AI(gameManager.getBoard(), difficulty, GREEN, gameManager)
|
||||||
|
gameManager.aiMove(newBoard)
|
||||||
|
# time.sleep(0.15)
|
||||||
|
|
||||||
|
if gameManager.winner() != None:
|
||||||
|
print(gameManager.winner())
|
||||||
|
run = False
|
||||||
|
|
||||||
|
for event in pygame.event.get():
|
||||||
|
if event.type == pygame.QUIT:
|
||||||
|
run = False
|
||||||
|
if event.type == pygame.MOUSEBUTTONDOWN:
|
||||||
|
pos = pygame.mouse.get_pos()
|
||||||
|
row, col = getRowColFromMouse(pos)
|
||||||
|
# if gameManager.turn == GREEN:
|
||||||
|
gameManager.select(row, col)
|
||||||
|
|
||||||
|
gameManager.update()
|
||||||
|
pygame.display.update()
|
||||||
|
|
||||||
|
# pygame.quit()
|
||||||
|
|
||||||
|
|
||||||
|
main()
|
0
minimax/__init__.py
Normal file
0
minimax/__init__.py
Normal file
56
minimax/minimaxAlgo.py
Normal file
56
minimax/minimaxAlgo.py
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
import random
|
||||||
|
from copy import deepcopy
|
||||||
|
from math import inf
|
||||||
|
|
||||||
|
from utilities.constants import GREEN, WHITE
|
||||||
|
|
||||||
|
|
||||||
|
class MiniMax():
|
||||||
|
|
||||||
|
def AI(self, board, depth, maxPlayer, gameManager):
|
||||||
|
if depth == 0 or board.winner() is not None:
|
||||||
|
return board.scoreOfTheBoard(), board
|
||||||
|
|
||||||
|
if maxPlayer:
|
||||||
|
maxEval = -inf
|
||||||
|
bestMove = None
|
||||||
|
for move in self.getAllMoves(board, maxPlayer):
|
||||||
|
evaluation = self.AI(move, depth - 1, False, gameManager)[0]
|
||||||
|
maxEval = max(maxEval, evaluation)
|
||||||
|
if maxEval > evaluation:
|
||||||
|
bestMove = move
|
||||||
|
if maxEval == evaluation:
|
||||||
|
bestMove = bestMove if random.choice([True, False]) else move
|
||||||
|
return maxEval, bestMove
|
||||||
|
else:
|
||||||
|
minEval = inf
|
||||||
|
bestMove = None
|
||||||
|
colour = WHITE if gameManager.turn == GREEN else GREEN
|
||||||
|
for move in self.getAllMoves(board, colour):
|
||||||
|
evaluation = self.AI(move, depth - 1, True, gameManager)[0]
|
||||||
|
minEval = min(minEval, evaluation)
|
||||||
|
if minEval < evaluation:
|
||||||
|
bestMove = move
|
||||||
|
if minEval == evaluation:
|
||||||
|
bestMove = bestMove if random.choice([True, False]) else move
|
||||||
|
|
||||||
|
return minEval, bestMove
|
||||||
|
|
||||||
|
def _simulateMove(self, piece, move, board, skip):
|
||||||
|
board.move(piece, move[0], move[1])
|
||||||
|
if skip:
|
||||||
|
board.remove(skip)
|
||||||
|
|
||||||
|
return board
|
||||||
|
|
||||||
|
def getAllMoves(self, board, colour):
|
||||||
|
moves = []
|
||||||
|
|
||||||
|
for piece in board.getAllPieces(colour):
|
||||||
|
validMoves = board.getValidMoves(piece)
|
||||||
|
for move, skip in validMoves.items():
|
||||||
|
tempBoard = deepcopy(board)
|
||||||
|
tempPiece = tempBoard.getPiece(piece.row, piece.col)
|
||||||
|
newBoard = self._simulateMove(tempPiece, move, tempBoard, skip)
|
||||||
|
moves.append(newBoard)
|
||||||
|
return moves
|
4
readme.txt
Normal file
4
readme.txt
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
To run the project
|
||||||
|
open project folder in terminal or cmd
|
||||||
|
run pip install pygame or pip3 install pygame if running a mac or linux machine run with sudo for best chance of running the project
|
||||||
|
then run python main.py
|
96
reinforcementLearning/ReinforcementLearning.py
Normal file
96
reinforcementLearning/ReinforcementLearning.py
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
import random
|
||||||
|
from collections import deque
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import tensorflow as tf
|
||||||
|
from tensorflow.python.keras import Sequential, regularizers
|
||||||
|
from tensorflow.python.keras.layers import Dense
|
||||||
|
|
||||||
|
|
||||||
|
class ReinforcementLearning():
|
||||||
|
|
||||||
|
def __init__(self, action_space, state_space, env):
|
||||||
|
self.action_space = action_space
|
||||||
|
self.state_space = state_space
|
||||||
|
self.env = env
|
||||||
|
self.epsilon = 1
|
||||||
|
self.gamma = .95
|
||||||
|
self.batch_size = 64
|
||||||
|
self.epsilon_min = .01
|
||||||
|
self.epsilon_decay = .995
|
||||||
|
self.learning_rate = 0.001
|
||||||
|
self.memory = deque(maxlen=100000)
|
||||||
|
self.model = self._buildModel()
|
||||||
|
|
||||||
|
def AI(self, episode):
|
||||||
|
loss = []
|
||||||
|
|
||||||
|
max_steps = 1000
|
||||||
|
|
||||||
|
for e in range(episode):
|
||||||
|
state = self.env.reset()
|
||||||
|
state = np.reshape(state, (1, self.state_space))
|
||||||
|
score = 0
|
||||||
|
for i in range(max_steps):
|
||||||
|
action = self.act(state)
|
||||||
|
reward, next_state, done = self.env.step(action)
|
||||||
|
score += reward
|
||||||
|
next_state = np.reshape(next_state, (1, self.state_space))
|
||||||
|
self.remember(state, action, reward, next_state, done)
|
||||||
|
state = next_state
|
||||||
|
self.replay()
|
||||||
|
if done:
|
||||||
|
print("episode: {}/{}, score: {}".format(e, episode, score))
|
||||||
|
break
|
||||||
|
loss.append(score)
|
||||||
|
|
||||||
|
def _buildModel(self):
|
||||||
|
# Board model
|
||||||
|
board_model = Sequential()
|
||||||
|
|
||||||
|
# input dimensions is 32 board position values
|
||||||
|
board_model.add(Dense(64, activation='relu', input_dim=32))
|
||||||
|
|
||||||
|
# use regularizers, to prevent fitting noisy labels
|
||||||
|
board_model.add(Dense(32, activation='relu', kernel_regularizer=regularizers.l2(0.01)))
|
||||||
|
board_model.add(Dense(16, activation='relu', kernel_regularizer=regularizers.l2(0.01))) # 16
|
||||||
|
board_model.add(Dense(8, activation='relu', kernel_regularizer=regularizers.l2(0.01))) # 8
|
||||||
|
|
||||||
|
# output isn't squashed, because it might lose information
|
||||||
|
board_model.add(Dense(1, activation='linear', kernel_regularizer=regularizers.l2(0.01)))
|
||||||
|
board_model.compile(optimizer='nadam', loss='binary_crossentropy')
|
||||||
|
|
||||||
|
return board_model
|
||||||
|
|
||||||
|
def remember(self, state, action, reward, next_state, done):
|
||||||
|
self.memory.append((state, action, reward, next_state, done))
|
||||||
|
|
||||||
|
def replay(self):
|
||||||
|
if len(self.memory) < self.batch_size:
|
||||||
|
return
|
||||||
|
|
||||||
|
minibatch = random.sample(self.memory, self.batch_size)
|
||||||
|
states = np.array([i[0] for i in minibatch])
|
||||||
|
actions = np.array([i[1] for i in minibatch])
|
||||||
|
rewards = np.array([i[2] for i in minibatch])
|
||||||
|
next_states = np.array([i[3] for i in minibatch])
|
||||||
|
dones = np.array([i[4] for i in minibatch])
|
||||||
|
|
||||||
|
states = np.squeeze(states)
|
||||||
|
next_states = np.squeeze(next_states)
|
||||||
|
|
||||||
|
targets = rewards + self.gamma * (np.amax(self.model.predict_on_batch(next_states), axis=1)) * (1 - dones)
|
||||||
|
targets_full = self.model.predict_on_batch(states)
|
||||||
|
|
||||||
|
ind = np.array([i for i in range(self.batch_size)])
|
||||||
|
targets_full[[ind], [actions]] = targets
|
||||||
|
|
||||||
|
self.model.fit(states, targets_full, epochs=1, verbose=0)
|
||||||
|
if self.epsilon > self.epsilon_min:
|
||||||
|
self.epsilon *= self.epsilon_decay
|
||||||
|
|
||||||
|
def act(self, state):
|
||||||
|
if np.random.rand() <= self.epsilon:
|
||||||
|
return random.randrange(self.action_space)
|
||||||
|
act_values = self.model.predict(state)
|
||||||
|
return np.argmax(act_values[0])
|
0
reinforcementLearning/__init__.py
Normal file
0
reinforcementLearning/__init__.py
Normal file
0
utilities/__init__.py
Normal file
0
utilities/__init__.py
Normal file
BIN
utilities/assets/crown.png
Normal file
BIN
utilities/assets/crown.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.6 KiB |
185
utilities/board.py
Normal file
185
utilities/board.py
Normal file
@ -0,0 +1,185 @@
|
|||||||
|
import pygame
|
||||||
|
|
||||||
|
from .constants import BLACK, ROWS, GREEN, SQUARE_SIZE, COLS, WHITE
|
||||||
|
from .piece import Piece
|
||||||
|
|
||||||
|
|
||||||
|
class Board:
|
||||||
|
def __init__(self):
|
||||||
|
self.board = []
|
||||||
|
self.greenLeft = self.whiteLeft = 12
|
||||||
|
self.greenKings = self.whiteKings = 0
|
||||||
|
self.createBoard()
|
||||||
|
|
||||||
|
def drawSquares(self, win):
|
||||||
|
win.fill(BLACK)
|
||||||
|
for row in range(ROWS):
|
||||||
|
for col in range(row % 2, ROWS, 2):
|
||||||
|
pygame.draw.rect(win, GREEN, (row * SQUARE_SIZE, col * SQUARE_SIZE, SQUARE_SIZE, SQUARE_SIZE))
|
||||||
|
|
||||||
|
def createBoard(self):
|
||||||
|
for row in range(ROWS):
|
||||||
|
self.board.append([])
|
||||||
|
for col in range(COLS):
|
||||||
|
if col % 2 == ((row + 1) % 2):
|
||||||
|
if row < 3:
|
||||||
|
self.board[row].append(Piece(row, col, WHITE))
|
||||||
|
elif row > 4:
|
||||||
|
self.board[row].append(Piece(row, col, GREEN))
|
||||||
|
else:
|
||||||
|
self.board[row].append(None)
|
||||||
|
else:
|
||||||
|
self.board[row].append(None)
|
||||||
|
|
||||||
|
def draw(self, win):
|
||||||
|
self.drawSquares(win)
|
||||||
|
for row in range(ROWS):
|
||||||
|
for col in range(COLS):
|
||||||
|
piece = self.board[row][col]
|
||||||
|
if piece is not None:
|
||||||
|
piece.draw(win)
|
||||||
|
|
||||||
|
def move(self, piece, row, col):
|
||||||
|
self.board[piece.row][piece.col], self.board[row][col] = self.board[row][col], self.board[piece.row][piece.col]
|
||||||
|
piece.move(row, col)
|
||||||
|
|
||||||
|
if row == ROWS - 1 or row == 0:
|
||||||
|
piece.makeKing()
|
||||||
|
if piece.colour == WHITE:
|
||||||
|
self.whiteKings += 1
|
||||||
|
else:
|
||||||
|
self.greenKings += 1
|
||||||
|
|
||||||
|
def remove(self, skipped):
|
||||||
|
for piece in skipped:
|
||||||
|
self.board[piece.row][piece.col] = None
|
||||||
|
if piece is not None:
|
||||||
|
if piece.colour == GREEN:
|
||||||
|
self.greenLeft -= 1
|
||||||
|
else:
|
||||||
|
self.whiteLeft -= 1
|
||||||
|
|
||||||
|
def getPiece(self, row, col):
|
||||||
|
return self.board[row][col]
|
||||||
|
|
||||||
|
def winner(self):
|
||||||
|
if self.greenLeft <= 0:
|
||||||
|
return WHITE
|
||||||
|
elif self.whiteLeft <= 0:
|
||||||
|
return GREEN
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
def getValidMoves(self, piece):
|
||||||
|
moves = {}
|
||||||
|
forcedCapture = {}
|
||||||
|
left = piece.col - 1
|
||||||
|
right = piece.col + 1
|
||||||
|
row = piece.row
|
||||||
|
if piece.colour == GREEN:
|
||||||
|
moves.update(self._traverseLeft(row - 1, max(row - 3, -1), -1, piece.colour, left))
|
||||||
|
moves.update(self._traverseRight(row - 1, max(row - 3, -1), -1, piece.colour, right))
|
||||||
|
if piece.colour == WHITE:
|
||||||
|
moves.update(self._traverseLeft(row + 1, min(row + 3, ROWS), 1, piece.colour, left))
|
||||||
|
moves.update(self._traverseRight(row + 1, min(row + 3, ROWS), 1, piece.colour, right))
|
||||||
|
|
||||||
|
if piece.king:
|
||||||
|
moves.update(self._traverseLeft(row - 1, max(row - 3, -1), -1, piece.colour, left))
|
||||||
|
moves.update(self._traverseRight(row - 1, max(row - 3, -1), -1, piece.colour, right))
|
||||||
|
moves.update(self._traverseLeft(row + 1, min(row + 3, ROWS), 1, piece.colour, left))
|
||||||
|
moves.update(self._traverseRight(row + 1, min(row + 3, ROWS), 1, piece.colour, right))
|
||||||
|
|
||||||
|
if len(moves.values()) <= 1:
|
||||||
|
return moves
|
||||||
|
|
||||||
|
movesValues = list(moves.values())
|
||||||
|
movesKeys = list(moves.keys())
|
||||||
|
|
||||||
|
forced = {}
|
||||||
|
|
||||||
|
for i in range(len(movesKeys)):
|
||||||
|
if not movesValues[i]:
|
||||||
|
forced[movesKeys[i]] = moves[movesKeys[i]]
|
||||||
|
if len(forced) != len(moves):
|
||||||
|
forced.clear()
|
||||||
|
for i in range(len(movesKeys)):
|
||||||
|
if movesValues[i]:
|
||||||
|
forced[movesKeys[i]] = moves[movesKeys[i]]
|
||||||
|
if len(forced) != len(moves):
|
||||||
|
for i in range(len(movesKeys)):
|
||||||
|
if movesValues[i]:
|
||||||
|
forcedCapture[movesKeys[i]] = moves[movesKeys[i]]
|
||||||
|
else:
|
||||||
|
forcedCapture = forced
|
||||||
|
else:
|
||||||
|
forcedCapture = forced
|
||||||
|
return forcedCapture
|
||||||
|
|
||||||
|
def scoreOfTheBoard(self):
|
||||||
|
return self.whiteLeft - self.greenLeft
|
||||||
|
|
||||||
|
def getAllPieces(self, colour):
|
||||||
|
pieces = []
|
||||||
|
for row in self.board:
|
||||||
|
for piece in row:
|
||||||
|
if piece is not None and piece.colour == colour:
|
||||||
|
pieces.append(piece)
|
||||||
|
return pieces
|
||||||
|
|
||||||
|
def _traverseLeft(self, start, stop, step, colour, left, skipped=[]):
|
||||||
|
moves = {}
|
||||||
|
last = []
|
||||||
|
for row in range(start, stop, step):
|
||||||
|
if left < 0:
|
||||||
|
break
|
||||||
|
mvs = self._traverse(row, left, skipped, moves, step, last, colour)
|
||||||
|
if mvs is None:
|
||||||
|
break
|
||||||
|
elif isinstance(mvs, list):
|
||||||
|
last = mvs
|
||||||
|
else:
|
||||||
|
moves.update(mvs)
|
||||||
|
left -= 1
|
||||||
|
return moves
|
||||||
|
|
||||||
|
def _traverseRight(self, start, stop, step, colour, right, skipped=[]):
|
||||||
|
moves = {}
|
||||||
|
last = []
|
||||||
|
for row in range(start, stop, step):
|
||||||
|
if right >= COLS:
|
||||||
|
break
|
||||||
|
|
||||||
|
mvs = self._traverse(row, right, skipped, moves, step, last, colour)
|
||||||
|
if mvs is None:
|
||||||
|
break
|
||||||
|
elif isinstance(mvs, list):
|
||||||
|
last = mvs
|
||||||
|
else:
|
||||||
|
moves.update(mvs)
|
||||||
|
|
||||||
|
right += 1
|
||||||
|
return moves
|
||||||
|
|
||||||
|
def _traverse(self, row, col, skipped, moves, step, last, colour):
|
||||||
|
current = self.board[row][col]
|
||||||
|
if current is None:
|
||||||
|
if skipped and not last:
|
||||||
|
return None
|
||||||
|
elif skipped:
|
||||||
|
moves[(row, col)] = last + skipped
|
||||||
|
else:
|
||||||
|
moves[(row, col)] = last
|
||||||
|
|
||||||
|
if last:
|
||||||
|
if step == -1:
|
||||||
|
rowCalc = max(row - 3, 0)
|
||||||
|
else:
|
||||||
|
rowCalc = min(row + 3, ROWS)
|
||||||
|
moves.update(self._traverseLeft(row + step, rowCalc, step, colour, col - 1, skipped=last))
|
||||||
|
moves.update(self._traverseRight(row + step, rowCalc, step, colour, col + 1, skipped=last))
|
||||||
|
return None
|
||||||
|
elif current.colour == colour:
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
last = [current]
|
||||||
|
return last
|
15
utilities/constants.py
Normal file
15
utilities/constants.py
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import pygame
|
||||||
|
|
||||||
|
WIDTH, HEIGHT = 800, 800
|
||||||
|
ROWS, COLS = 8, 8
|
||||||
|
SQUARE_SIZE = WIDTH // COLS
|
||||||
|
|
||||||
|
# RGB color
|
||||||
|
|
||||||
|
GREEN = (144, 184, 59)
|
||||||
|
WHITE = (255, 255, 255)
|
||||||
|
BLACK = (0, 0, 0)
|
||||||
|
BLUE = (0, 0, 255)
|
||||||
|
GREY = (128, 128, 128)
|
||||||
|
|
||||||
|
CROWN = pygame.transform.scale(pygame.image.load("./utilities/assets/crown.png"), (45, 25))
|
82
utilities/gameManager.py
Normal file
82
utilities/gameManager.py
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
import pygame
|
||||||
|
from utilities.board import Board
|
||||||
|
from utilities.constants import GREEN, WHITE, BLUE, SQUARE_SIZE
|
||||||
|
|
||||||
|
class GameManager:
|
||||||
|
def __init__(self, win, colour):
|
||||||
|
self._init(colour)
|
||||||
|
self.win = win
|
||||||
|
|
||||||
|
def _init(self, colour):
|
||||||
|
self.selected = None
|
||||||
|
self.board = Board()
|
||||||
|
self.turn = colour
|
||||||
|
self.validMoves = {}
|
||||||
|
self.legCount = 0
|
||||||
|
|
||||||
|
def update(self):
|
||||||
|
self.board.draw(self.win)
|
||||||
|
self.drawValidMoves(self.validMoves)
|
||||||
|
pygame.display.update()
|
||||||
|
|
||||||
|
def reset(self):
|
||||||
|
self._init(self.turn)
|
||||||
|
|
||||||
|
def select(self, row, col):
|
||||||
|
if self.selected:
|
||||||
|
result = self._move(row, col)
|
||||||
|
if not result:
|
||||||
|
self.selected = None
|
||||||
|
self.select(row, col)
|
||||||
|
piece = self.board.getPiece(row, col)
|
||||||
|
if piece is not None and piece.colour == self.turn:
|
||||||
|
self.selected = piece
|
||||||
|
self.validMoves = self.board.getValidMoves(piece)
|
||||||
|
return True
|
||||||
|
|
||||||
|
def _move(self, row, col):
|
||||||
|
piece = self.board.getPiece(row, col)
|
||||||
|
if self.selected and piece is None and (row, col) in self.validMoves:
|
||||||
|
self.board.move(self.selected, row, col)
|
||||||
|
skipped = self.validMoves[row, col]
|
||||||
|
if self.validMoves[list(self.validMoves.keys())[0]]:
|
||||||
|
if self.validMoves[list(self.validMoves.keys())[0]][0].king:
|
||||||
|
self.selected.makeKing()
|
||||||
|
if skipped:
|
||||||
|
self.board.remove(skipped)
|
||||||
|
if len(self.validMoves) > 1:
|
||||||
|
del self.validMoves[list(self.validMoves.keys())[0]]
|
||||||
|
else:
|
||||||
|
self.changeTurn()
|
||||||
|
else:
|
||||||
|
self.changeTurn()
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
def changeTurn(self):
|
||||||
|
self.validMoves = {}
|
||||||
|
if self.turn == GREEN:
|
||||||
|
self.turn = WHITE
|
||||||
|
else:
|
||||||
|
self.turn = GREEN
|
||||||
|
|
||||||
|
def drawValidMoves(self, moves):
|
||||||
|
for row, col in moves:
|
||||||
|
pygame.draw.circle(self.win, BLUE,
|
||||||
|
(col * SQUARE_SIZE + SQUARE_SIZE // 2, row * SQUARE_SIZE + SQUARE_SIZE // 2), 15)
|
||||||
|
|
||||||
|
def winner(self):
|
||||||
|
return self.board.winner()
|
||||||
|
|
||||||
|
def getBoard(self):
|
||||||
|
return self.board
|
||||||
|
|
||||||
|
def aiMove(self, board):
|
||||||
|
if board is None:
|
||||||
|
# colour = "green" if self.turn == GREEN else "white"
|
||||||
|
# print("no move left for " + colour + " to make")
|
||||||
|
self.changeTurn()
|
||||||
|
return
|
||||||
|
self.board = board
|
||||||
|
self.changeTurn()
|
38
utilities/piece.py
Normal file
38
utilities/piece.py
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import pygame.draw
|
||||||
|
|
||||||
|
from utilities.constants import SQUARE_SIZE, GREY, CROWN
|
||||||
|
|
||||||
|
|
||||||
|
class Piece:
|
||||||
|
def __init__(self, row, col, colour):
|
||||||
|
self.row = row
|
||||||
|
self.col = col
|
||||||
|
self.colour = colour
|
||||||
|
self.king = False
|
||||||
|
self.x = 0
|
||||||
|
self.y = 0
|
||||||
|
self.calcPosition()
|
||||||
|
self.padding = 20
|
||||||
|
self.border = 2
|
||||||
|
|
||||||
|
def calcPosition(self):
|
||||||
|
self.x = SQUARE_SIZE * self.col + SQUARE_SIZE // 2
|
||||||
|
self.y = SQUARE_SIZE * self.row + SQUARE_SIZE // 2
|
||||||
|
|
||||||
|
def makeKing(self):
|
||||||
|
self.king = True
|
||||||
|
|
||||||
|
def draw(self, win):
|
||||||
|
radius = SQUARE_SIZE // 2 - self.padding
|
||||||
|
pygame.draw.circle(win, GREY, (self.x, self.y), radius + self.border)
|
||||||
|
pygame.draw.circle(win, self.colour, (self.x, self.y), radius)
|
||||||
|
if self.king:
|
||||||
|
win.blit(CROWN, (self.x - CROWN.get_width() // 2, self.y - CROWN.get_height() // 2))
|
||||||
|
|
||||||
|
def move(self, row, col):
|
||||||
|
self.row = row
|
||||||
|
self.col = col
|
||||||
|
self.calcPosition()
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return str(self.colour)
|
Loading…
Reference in New Issue
Block a user