commit 1eb0a04f30fddc4bcccbe913bd4574729539b346 Author: rodude123 Date: Fri Jul 28 19:34:53 2023 +0100 Created base game with working minimax algorithm, now working on reinforcement learning diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..287a2f0 --- /dev/null +++ b/.gitignore @@ -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/ + diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..13566b8 --- /dev/null +++ b/.idea/.gitignore @@ -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 diff --git a/.idea/GitLink.xml b/.idea/GitLink.xml new file mode 100644 index 0000000..5143819 --- /dev/null +++ b/.idea/GitLink.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml new file mode 100644 index 0000000..a55e7a1 --- /dev/null +++ b/.idea/codeStyles/codeStyleConfig.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/.idea/draughts.iml b/.idea/draughts.iml new file mode 100644 index 0000000..3c96bee --- /dev/null +++ b/.idea/draughts.iml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..aba48de --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,12 @@ + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 0000000..105ce2d --- /dev/null +++ b/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..d22234a --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..269c0d3 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/Report.pdf b/Report.pdf new file mode 100644 index 0000000..3f309d9 Binary files /dev/null and b/Report.pdf differ diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/main.py b/main.py new file mode 100644 index 0000000..921efef --- /dev/null +++ b/main.py @@ -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() diff --git a/minimax/__init__.py b/minimax/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/minimax/minimaxAlgo.py b/minimax/minimaxAlgo.py new file mode 100644 index 0000000..063b8cd --- /dev/null +++ b/minimax/minimaxAlgo.py @@ -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 diff --git a/readme.txt b/readme.txt new file mode 100644 index 0000000..5ecee58 --- /dev/null +++ b/readme.txt @@ -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 diff --git a/reinforcementLearning/ReinforcementLearning.py b/reinforcementLearning/ReinforcementLearning.py new file mode 100644 index 0000000..e554162 --- /dev/null +++ b/reinforcementLearning/ReinforcementLearning.py @@ -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]) diff --git a/reinforcementLearning/__init__.py b/reinforcementLearning/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/utilities/__init__.py b/utilities/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/utilities/assets/crown.png b/utilities/assets/crown.png new file mode 100644 index 0000000..4b11700 Binary files /dev/null and b/utilities/assets/crown.png differ diff --git a/utilities/board.py b/utilities/board.py new file mode 100644 index 0000000..a1bf435 --- /dev/null +++ b/utilities/board.py @@ -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 diff --git a/utilities/constants.py b/utilities/constants.py new file mode 100644 index 0000000..526c64e --- /dev/null +++ b/utilities/constants.py @@ -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)) diff --git a/utilities/gameManager.py b/utilities/gameManager.py new file mode 100644 index 0000000..93db2a1 --- /dev/null +++ b/utilities/gameManager.py @@ -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() diff --git a/utilities/piece.py b/utilities/piece.py new file mode 100644 index 0000000..c808fd9 --- /dev/null +++ b/utilities/piece.py @@ -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)