diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2cf3df8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +# Python +.ipynb_checkpoints +*_cache +__pycache__ +.venv/ + +# Hydra +outputs/ + +# Workspace +.vscode diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..d8e6b72 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,39 @@ +# Commands to use pre-commit +# pip install pre-commit +# pre-commit install +# pre-commit run --all-files +# git commit -m "" --no-verify + +# See https://pre-commit.com for more information +# See https://pre-commit.com/hooks.html for more hooks +repos: +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v3.2.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + exclude: 'replays/' + - id: check-yaml + - id: check-added-large-files + args: ['--maxkb=1024'] + - id: check-docstring-first + - id: check-executables-have-shebangs + - id: requirements-txt-fixer +- repo: https://github.com/asottile/add-trailing-comma + rev: v3.1.0 + hooks: + - id: add-trailing-comma +- repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.1.11 + hooks: + - id: ruff + args: [ --fix ] + types_or: [ python, pyi, jupyter ] + exclude: 'constants.py' + - id: ruff-format + types_or: [ python, pyi, jupyter ] +# - repo: https://github.com/pre-commit/mirrors-mypy +# rev: v1.8.0 +# hooks: +# - id: mypy +# additional_dependencies: [types-all] diff --git a/README.md b/README.md index ac00767..0755040 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ The following image is an example of a hop. The highlighted green piece may hop - A game interface that supports 2 to 3 players. Any of them can be a human player. - Export replay to file - Load an existing replay file to watch the game (you may click the buttons or press the left and right arrow keys to navigate through the game) -- You may create **custom bots** under the `custom_bots` folder! It'll automatically be added into the game. For more information, check out the [custom bot guide](https://github.com/henrychess/pygame-chinese-checkers/blob/main/custom_bots/README.md). +- You may create **custom bots** under the `custom_bots` folder! It'll automatically be added into the game. For more information, check out the [custom bot guide](bots/README.md). ## Showcase video diff --git a/custom_bots/CustomBotTemplate.py b/bots/BotTemplate.py similarity index 57% rename from custom_bots/CustomBotTemplate.py rename to bots/BotTemplate.py index bf79af4..f4fc86a 100644 --- a/custom_bots/CustomBotTemplate.py +++ b/bots/BotTemplate.py @@ -1,28 +1,33 @@ from game_logic.player import Player -from game_logic.game import * -from game_logic.helpers import add, mult +from game_logic.game import Game +from game_logic.helpers import subj_to_obj_coor + class CustomBotTemplate(Player): def __init__(self): super().__init__() - - def pickMove(self, g:Game): + + def pickMove(self, g: Game): moves = g.allMovesDict(self.playerNum) - #board_state = g.getBoardState(self.playerNum) - #bool_board_state = g.getBoolBoardState(self.playerNum) + # board_state = g.getBoardState(self.playerNum) + # bool_board_state = g.getBoolBoardState(self.playerNum) """ The following code section is a simple example: it randomly picks a valid move and return it. """ from random import choice + l = [] for coor in moves: - if moves[coor] != []: l.append(coor) + if moves[coor] != []: + l.append(coor) start = choice(l) end = choice(moves[start]) """ This is the return section. `start` and `end` are the starting and ending subjective coordinates. """ - return [subj_to_obj_coor(start, self.playerNum), - subj_to_obj_coor(end, self.playerNum)] + return [ + subj_to_obj_coor(start, self.playerNum), + subj_to_obj_coor(end, self.playerNum), + ] diff --git a/bots/GreedyBot0.py b/bots/GreedyBot0.py new file mode 100644 index 0000000..c30cda7 --- /dev/null +++ b/bots/GreedyBot0.py @@ -0,0 +1,64 @@ +import random +from game_logic.game import Game +from game_logic.helpers import subj_to_obj_coor +from game_logic.player import Player + + +class GreedyBot0(Player): + """ + Choose a forward move randomly. Else, choose a sideway move randomly. + """ + + def __init__(self): + super().__init__() + + def pickMove(self, g: Game): + """ + Choose a forward move randomly. Else, choose a sideway move randomly. + + Returns: + [start_coor, end_coor] : in objective coordinates + """ + print(f"[GreedyBot0] is player {self.playerNum}") + moves = g.allMovesDict(self.playerNum) + forwardMoves = dict() + sidewaysMoves = dict() + (start_coor, end_coor) = ((), ()) + + # Split moves into forward and sideways + for coor in moves: + # If there are moves + if moves[coor] != []: + forwardMoves[coor] = [] + sidewaysMoves[coor] = [] + + # Check y-coordinate of destination + for dest in moves[coor]: + if dest[1] > coor[1]: + forwardMoves[coor].append(dest) + if dest[1] == coor[1]: + sidewaysMoves[coor].append(dest) + + # Remove empty keys + for coor in list(forwardMoves): + if forwardMoves[coor] == []: + del forwardMoves[coor] + for coor in list(sidewaysMoves): + if sidewaysMoves[coor] == []: + del sidewaysMoves[coor] + + # Choose a forward move randomly + if len(forwardMoves) != 0: + start_coor = random.choice(list(forwardMoves)) + end_coor = random.choice(forwardMoves[start_coor]) + # Else, choose a sideway move randomly + else: + start_coor = random.choice(list(sidewaysMoves)) + end_coor = random.choice(sidewaysMoves[start_coor]) + + move = [ + subj_to_obj_coor(start_coor, self.playerNum, g.layout), + subj_to_obj_coor(end_coor, self.playerNum, g.layout), + ] + print(f"[GreedyBot0] Move: {move}\n") + return move diff --git a/bots/GreedyBot1.py b/bots/GreedyBot1.py new file mode 100644 index 0000000..640baec --- /dev/null +++ b/bots/GreedyBot1.py @@ -0,0 +1,86 @@ +import random +from game_logic.player import Player +from game_logic.game import Game +from game_logic.helpers import subj_to_obj_coor + + +class GreedyBot1(Player): + """ + Choose the move that moves a piece to the topmost cell. + """ + + def __init__(self): + super().__init__() + + def pickMove(self, g: Game): + """ + Choose the forward move with the greatest y value. If there are no + forward moves, choose a sideway move randomly. + + Returns: + [startCoor, endCoor] : in objective coordinates + """ + print(f"[GreedyBot1] is player {self.playerNum}") + moves = g.allMovesDict(self.playerNum) + forwardMoves = dict() + sidewaysMoves = dict() + (startCoor, endCoor) = ((), ()) + + # Split moves into forward and sideways + for startCoor in moves: + # Check y-coordinate of destination + for dest in moves[startCoor]: + if dest[1] > startCoor[1]: + forwardMoves[startCoor] = dest + if dest[1] == startCoor[1]: + sidewaysMoves[startCoor] = dest + # BUG: moves return int when there are no forward moves + + # If there are no forward moves, move sideways randomly. + if len(forwardMoves) == 0: + print("[GreedyBot1] No forward moves") + print(sidewaysMoves) + startCoor = random.choice(list(sidewaysMoves)) + endCoor = random.choice(sidewaysMoves[startCoor]) + + move = [ + subj_to_obj_coor(startCoor, self.playerNum, g.layout), + subj_to_obj_coor(endCoor, self.playerNum, g.layout), + ] + print(f"[GreedyBot1] Move: {move}\n") + return move + + # Choose the furthest destination (biggest y value in dest), + # then backmost piece (smallest y value in coor) + biggestDestY = -8 + smallestStartY = 8 + for coor in forwardMoves: + dest = forwardMoves[coor] + # Find forward move with biggest y dest value + if dest[1] > biggestDestY: + (startCoor, endCoor) = (coor, dest) + biggestDestY = dest[1] + smallestStartY = coor[1] + + elif dest[1] == biggestDestY: + startY = coor[1] + + # If tiebreakers, + # choose forward move with smallest y start value + if startY < smallestStartY: + (startCoor, endCoor) = (coor, dest) + biggestDestY = dest[1] + smallestStartY = coor[1] + elif startY == smallestStartY: + startCoor, endCoor = random.choice( + [[startCoor, endCoor], [coor, dest]], + ) + biggestDestY = endCoor[1] + smallestStartY = startCoor[1] + + move = [ + subj_to_obj_coor(startCoor, self.playerNum, g.layout), + subj_to_obj_coor(endCoor, self.playerNum, g.layout), + ] + print(f"[GreedyBot1] Move: {move}\n") + return move diff --git a/bots/GreedyBot2.py b/bots/GreedyBot2.py new file mode 100644 index 0000000..2910770 --- /dev/null +++ b/bots/GreedyBot2.py @@ -0,0 +1,75 @@ +import random +from game_logic.player import Player +from game_logic.game import Game +from game_logic.helpers import subj_to_obj_coor + + +class GreedyBot2(Player): + """Always finds a move that jumps through the maximum distance (dest[1] - coor[1])""" + + def __init__(self): + super().__init__() + + def pickMove(self, g: Game): + """ + Choose a forward move with the greatest distance travelled. If there + are no forward moves, choose a sideway move randomly. + + Returns: + [start_coor, end_coor] : in objective coordinates + """ + print(f"[GreedyBot2] is player {self.playerNum}") + + forwardMoves = dict() + sidewaysMoves = dict() + (start_coor, end_coor) = (None, None) + + # Split moves into forward and sideways + moves = g.allMovesDict(self.playerNum) + for coor in moves: + if moves == []: + continue + forwardMoves[coor] = [] + sidewaysMoves[coor] = [] + for dest in moves[coor]: + if dest[1] > coor[1]: + forwardMoves[coor].append(dest) + if dest[1] == coor[1]: + sidewaysMoves[coor].append(dest) + + # If forward is empty, move sideways + if len(forwardMoves) == 0: + start_coor = random.choice(list(sidewaysMoves)) + end_coor = random.choice(sidewaysMoves[start_coor]) + move = [ + subj_to_obj_coor(start_coor, self.playerNum, g.layout), + subj_to_obj_coor(end_coor, self.playerNum, g.layout), + ] + print(f"[GreedyBot2] Move: {move}\n") + return move + + # Find forward with the max distance travelled + max_dist = 0 + for coor in forwardMoves: + for dest in forwardMoves[coor]: + dist = dest[1] - coor[1] + if dist > max_dist: + max_dist = dist + (start_coor, end_coor) = (coor, dest) + elif dist == max_dist: + # Prefer to move the piece that is more backwards + if dest[1] < end_coor[1]: + max_dist = dist + (start_coor, end_coor) = (coor, dest) + + if start_coor is None or end_coor is None: + print("[GreedyBot2] Error: No move found") + start_coor = random.choice(list(moves)) + end_coor = random.choice(moves[start_coor]) + + move = [ + subj_to_obj_coor(start_coor, self.playerNum, g.layout), + subj_to_obj_coor(end_coor, self.playerNum, g.layout), + ] + print(f"[GreedyBot2] Move: {move}\n") + return move diff --git a/custom_bots/README.md b/bots/README.md similarity index 100% rename from custom_bots/README.md rename to bots/README.md diff --git a/bots/RandomBot.py b/bots/RandomBot.py new file mode 100644 index 0000000..ad9785a --- /dev/null +++ b/bots/RandomBot.py @@ -0,0 +1,33 @@ +import random +from game_logic.player import Player +from game_logic.game import Game +from game_logic.helpers import subj_to_obj_coor + + +class RandomBot(Player): + def __init__(self): + super().__init__() + + def pickMove(self, g: Game): + """ + Returns: + [start_coor, end_coor] : in objective coordinates + """ + print(f"[RandomBot] is player {self.playerNum}") + moves = g.allMovesDict(self.playerNum) + + start_coords = [] + for coor in moves: + if moves[coor] != []: + start_coords.append(coor) + + # Choose a random start_coor + start_coor = random.choice(start_coords) + end_coor = random.choice(moves[start_coor]) + + move = [ + subj_to_obj_coor(start_coor, self.playerNum, g.layout), + subj_to_obj_coor(end_coor, self.playerNum, g.layout), + ] + print(f"[RandomBot] Move: {move}\n") + return move diff --git a/bots/__init__.py b/bots/__init__.py new file mode 100644 index 0000000..4048ea4 --- /dev/null +++ b/bots/__init__.py @@ -0,0 +1,13 @@ +import importlib +from glob import glob + +for module in glob("custom_bots/*py"): + if not module.endswith("__init__.py"): + importlib.import_module( + module.replace( + "/", + ".", + ) + .replace("\\", ".") + .removesuffix(".py"), + ) diff --git a/coor_display.py b/coor_display.py index 1968e5e..449344c 100644 --- a/coor_display.py +++ b/coor_display.py @@ -1,32 +1,61 @@ -from game_logic.game import * -from game_logic.helpers import * -from game_logic.literals import * -import pygame, sys +# usr/bin/python3 +""" +Script to display the coordinates of the Chinese Checkers +board as an image. +""" -pygame.init() -window = pygame.display.set_mode((WIDTH, HEIGHT), pygame.SCALED | pygame.RESIZABLE) -pygame.display.set_caption('Chinese Checkers Coordinates') -g = Game(3) -window.fill(WHITE) -g.drawPolygons(window) -g.drawLines(window) -for coor in g.board: - c = add(g.centerCoor, mult(h2c(coor),g.unitLength)) #absolute coordinates on screen - pygame.draw.circle(window, WHITE, c, g.circleRadius) - pygame.draw.circle(window, BLACK, c, g.circleRadius, g.lineWidth) - if isinstance(g.board[coor], Piece): - pygame.draw.circle(window, PLAYER_COLORS[g.board[coor].getPlayerNum()-1], c, g.circleRadius-2) - coor_str = f"{coor[0]}, {coor[1]}" - text = pygame.font.Font(size=int(WIDTH*0.0175)).render(coor_str, True, BLACK, None) - textRect = text.get_rect() - textRect.center = c - window.blit(text, textRect) -pygame.display.update() -# pygame.image.save(window, "screenshot.png") +import pygame +import sys +from game_logic.game import Game +from game_logic.piece import Piece +from gui.constants import WIDTH, HEIGHT, WHITE, BLACK, PLAYER_COLORS +from gui.gui_helpers import add, mult, h2c -while True: - for event in pygame.event.get(): - if event.type == pygame.QUIT: - pygame.quit() - sys.exit() - + +def main(): + pygame.init() + window = pygame.display.set_mode( + (WIDTH, HEIGHT), + pygame.SCALED | pygame.RESIZABLE, + ) + pygame.display.set_caption("Chinese Checkers Coordinates") + g = Game(3) + window.fill(WHITE) + g.drawPolygons(window) + g.drawLines(window) + for coor in g.board: + c = add( + g.centerCoor, + mult(h2c(coor), g.unitLength), + ) # absolute coordinates on screen + pygame.draw.circle(window, WHITE, c, g.circleRadius) + pygame.draw.circle(window, BLACK, c, g.circleRadius, g.lineWidth) + if isinstance(g.board[coor], Piece): + pygame.draw.circle( + window, + PLAYER_COLORS[g.board[coor].getPlayerNum() - 1], + c, + g.circleRadius - 2, + ) + coor_str = f"{coor[0]}, {coor[1]}" + text = pygame.font.Font(size=int(WIDTH * 0.0175)).render( + coor_str, + True, + BLACK, + None, + ) + textRect = text.get_rect() + textRect.center = c + window.blit(text, textRect) + pygame.display.update() + # pygame.image.save(window, "screenshot.png") + + while True: + for event in pygame.event.get(): + if event.type == pygame.QUIT: + pygame.quit() + sys.exit() + + +if __name__ == "__main__": + main() diff --git a/custom_bots/__init__.py b/custom_bots/__init__.py deleted file mode 100644 index 38abd7b..0000000 --- a/custom_bots/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -import importlib -from glob import glob - -for module in glob("custom_bots/*py"): - if not module.endswith("__init__.py"): - importlib.import_module(module.replace( - "/", ".").replace("\\", ".").removesuffix(".py")) diff --git a/game_logic/game.py b/game_logic/game.py index 6534143..57eab00 100644 --- a/game_logic/game.py +++ b/game_logic/game.py @@ -1,171 +1,326 @@ -from .literals import * -from .helpers import * -from .piece import * -import pygame, copy +""" +Game Class to represent the game state and logic. +""" +import copy +from game_logic.helpers import add, checkJump, obj_to_subj_coor, mult +from game_logic.layout import ( + DIRECTIONS, + ALL_COOR, + Layout, +) +from game_logic.piece import Piece +from gui.constants import HEIGHT, WIDTH +from typing import List + + +class Move: + """ + Class to backtrace the path of a move. + """ + + def __init__(self, coord: tuple): + self.parent: Move = None + self.coord: tuple = coord + self.children: List[Move] = [] + + def addChild(self, child): + self.children.append(child) + child.parent = self + + def getPath(self) -> List[tuple]: + """ + Recursively add the parent to the path. + """ + path = [self.coord] + if self.parent is self: # reached the root node + return path + else: # recurse + return self.parent.getPath() + path + class Game: - def __init__(self, playerCount=3): - if playerCount in (2,3): self.playerCount = playerCount - else: self.playerCount = 3 - self.pieces: dict[int, set[Piece]] = {1:set(), 2:set(), 3:set()} - self.board = self.createBoard(playerCount) - #for drawing board - self.unitLength = int(WIDTH * 0.05) #unitLength length in pixels - self.lineWidth = int(self.unitLength * 0.05) #line width - self.circleRadius = int(HEIGHT * 0.025) #board square (circle) radius - self.centerCoor = (WIDTH/2, HEIGHT/2) #window size is 800*600 - - def createBoard(self, playerCount: int): - Board = {} - #player 1 end zone - for p in range(-4,1): - for q in range(4,9): - if p + q > 4: continue - else: - if (p,q) not in Board: Board[(p,q)] = None - #player 1 start zone - for p in range(0,5): - for q in range(-8,-3): - if p + q < -4: continue - else: - Board[(p,q)] = Piece(1, p, q) - self.pieces[1].add(Board[p, q]) - #player 2 end zone - for p in range(4,9): - for q in range(-4,1): - if p + q > 4: continue - else: - if (p,q) not in Board: Board[(p,q)] = None - #player 2 start zone - for p in range(-8,-3): - for q in range(0,5): - if p + q < -4: continue - else: - Board[(p,q)] = Piece(2, p, q) - self.pieces[2].add(Board[p, q]) - #player 3 end zone - for p in range(-4,1): - for q in range(-4,1): - if p + q > -4: continue - else: - if (p,q) not in Board: Board[(p,q)] = None - #player 3 start zone - for p in range(0,5): - for q in range(0,5): - if p + q < 4: continue - else: - Board[(p,q)] = None if playerCount == 2 else Piece(3, p, q) - if playerCount == 3: self.pieces[3].add(Board[p, q]) - #neutral zone - for p in range(-3,4): - for q in range(-3,4): - if p + q <= 3 and p + q >= -3: Board[(p,q)] = None - return Board + def __init__( + self, + playerList, + playerNum: int, + playerNames: list[str], + layout: str, + n_pieces: int, + ): + # Gameplay variables + self.turnCount = 1 # current turn number + self.playerNum = playerNum # current player number (1->6) + self.playerNames = playerNames # e.g. ["Human", "GreedyBot1"] + # e.g. ["Human", "GreedyBot1"], used in gui_helpers.drawTurnCount() + self.playerList = playerList # list of player objects + # e.g. [, + # ] + + # Instantiate pieces and board + self.pieces: dict[int, set[Piece]] = {} + self.board: dict[tuple, Piece | None] = {} + self.layout = layout + self.coor = Layout(layout, n_pieces) + self.createBoard() + + # Parameters for drawing board + self.unitLength = int(WIDTH * 0.05) # unitLength length in pixels + self.lineWidth = int(self.unitLength * 0.05) + self.circleRadius = int(HEIGHT * 0.025) # board square (circle) radius + self.centerCoor = (WIDTH / 2, HEIGHT / 2) + + def createBoard(self): + """ + Returns a dict of the board. Adds pieces to starting zones. + """ + playerCount = len(self.playerList) + + for i in range(1, playerCount + 1): + self.pieces[i] = set() + + # Initialize all possible positions + for x, y in ALL_COOR: + self.board[(x, y)] = None + # Add empty spaces first because a player's start zones overlaps with + # another player's end zone + + # Add pieces + for playerNum in range(1, playerCount + 1): + for p, q in self.coor.START_COOR[playerNum]: + # Add piece to board + self.board[(p, q)] = Piece(playerNum, p, q) + # Add piece to player's set + self.pieces[playerNum].add(self.board[p, q]) def getValidMoves(self, startPos: tuple, playerNum: int): - '''Inputs the piece's starting position, playing player's number, and `self.board`. - Returns a `list` of objective coordinates of valid moves (end coordinates) that piece can make.''' + """ + Compute all valid moves for a piece. + + Args: + startPos (tuple): objective coordinates of the piece. + playerNum (int): the player number. + + Returns: + list of tuples: objective coordinates of the dest valid moves. + """ moves = [] + + # Try all 6 directions for direction in DIRECTIONS: destination = add(startPos, direction) - if destination not in self.board: continue #out of bounds - elif self.board[destination] == None: moves.append(destination) #walk - else: #self.board[destination] != None + # Step is out of bounds + if destination not in self.board: + continue + + # Single step into open space + if self.board[destination] is None: + moves.append(destination) # walk + + # Single step into occupied space, check for skips + else: # self.board[destination] is not None destination = add(destination, direction) - if destination not in self.board or self.board[destination] != None: continue #out of bounds or can't jump + if destination not in self.board or self.board[destination] is not None: + continue # out of bounds or can't jump moves.append(destination) checkJump(moves, self.board, destination, direction, playerNum) + + # You can move past other player's territory, but you can't stay there. for i in copy.deepcopy(moves): - #You can move past other player's territory, but you can't stay there. - if (i not in START_COOR[playerNum]) and (i not in END_COOR[playerNum]) and (i not in NEUTRAL_COOR): + if ( + (i not in self.coor.START_COOR[playerNum]) + and (i not in self.coor.END_COOR[playerNum]) + and (i not in self.coor.NEUTRAL_COOR) + ): while i in moves: moves.remove(i) return list(set(moves)) + def checkValidStepDest(self, playerNum: int, dest: tuple): + """ + Check if the destination is valid single step for the player. + + Args: + playerNum (int): the player number. + dest (tuple): the objective coordinates of the destination. + + Returns: + bool: True if the destination is valid. + """ + if dest not in self.board: # out of bounds + # print("out of bounds") + return False + if self.board[dest] is not None: # occupied cell + # print("occupied cell") + return False + if ( + dest in self.coor.NEUTRAL_COOR # neutral territory + or dest in self.coor.START_COOR[playerNum] # own start zone + or dest in self.coor.END_COOR[playerNum] + ): # own end zone + return True + return False + # if ( + # dest not in NEUTRAL_COOR # other player's territory + # and dest not in END_COOR[playerNum] + # and dest not in START_COOR[playerNum] + # ): + # # print("other player's territory") + # return False + # return True + + def getMovePath(self, playerNum: int, start: tuple, end: tuple): + """ + Find the path for the move using breadth-first search. + + Note: + This function should be called before moving the piece, + otherwise the end cell will be occupied. + + Args: + playerNum (int): the player number. + start (tuple): objective coordinates of the starting cell. + end (tuple): objective coordinates of the ending cell. + + Returns: + path (list(tuples)): objective coordinates of cells along the path. + """ + start_m = Move(start) + start_m.parent = start_m + path = [] + + # Single step + for dir in DIRECTIONS: + dest = add(start, dir) + dest_m = Move(dest) + if not self.checkValidStepDest(playerNum, dest): + continue + start_m.addChild(dest_m) + # Found end cell, return path + if dest == end: + path += dest_m.getPath() + return path + + # Jump steps using BFS. Note that a jump can be made through + # opponent's territory. + queue = [start_m] + while queue: + current = queue.pop(0) + for dir in DIRECTIONS: + # print(f"dir: {dir}") + stepDest = add(current.coord, dir) + if stepDest not in self.board: # out of bounds + # print("step: out of bounds") + continue + if self.board[stepDest] is None: # no piece to skip + # print("step: no piece to skip") + continue + jumpDir = mult(dir, 2) + jumpDest = add(current.coord, jumpDir) + jumpDest_m = Move(jumpDest) # create Move object + if jumpDest not in self.board: # out of bounds + # print("jump: out of bounds") + continue + if self.board[jumpDest] is not None: # occupied cell + # print("jump: occupied cell", jumpDest) + continue + if jumpDest == current.parent.coord: + # print("jump: back to parent") + continue # prevents endless loops + + jumpDest_m.parent = current + current.addChild(jumpDest_m) + if jumpDest == end: + path += jumpDest_m.getPath() + return path + queue.append(jumpDest_m) + + # Assumes that a path will be found eventually. + if path == []: + raise ValueError("No path found") + def checkWin(self, playerNum: int): - for i in END_COOR[playerNum]: - if self.board[i] == None: return False - if isinstance(self.board[i], Piece) and self.board[i].getPlayerNum() != playerNum: return False + """ + Check if all of the player's pieces are in their end zone. + """ + for i in self.coor.END_COOR[playerNum]: + # if there are no pieces + if self.board[i] is None: + return False + # if the piece does not belong to the player + if ( + isinstance(self.board[i], Piece) + and self.board[i].getPlayerNum() != playerNum + ): + return False return True + def isOver(self): + """ + Check if the game is over. + """ + for i in range(1, len(self.playerList) + 1): + if self.checkWin(i): + return True + return False + def getBoardState(self, playerNum: int): - '''Key: subjective coordinates\nValue: piece's player number, or 0 if it's vacant''' + """ + Key: subjective coordinates + Value: piece's player number, + or 0 if it's vacant + """ state = dict() for i in self.board: - state[obj_to_subj_coor(i, playerNum)] = (0 if self.board[i] == None else int(self.board[i].getPlayerNum())) + state[obj_to_subj_coor(i, playerNum, self.layout)] = ( + 0 if self.board[i] is None else int(self.board[i].getPlayerNum()) + ) return state - + def getBoolBoardState(self, playerNum: int): - '''Key: subjective coordinates\nValue: `true`, or `false` if it's vacant''' + """ + Returns a dict of the board in subjective coordinates. + + Key: subjective coordinates + Value: true if occupied, false if vacant + """ state = dict() for i in self.board: - state[obj_to_subj_coor(i, playerNum)] = (self.board[i] != None) + state[obj_to_subj_coor(i, playerNum, self.layout)] = ( + self.board[i] is not None + ) return state def allMovesDict(self, playerNum: int): - '''Returns a dict of all valid moves, in subjective coordinates. - The key is the coordinates of a piece (`tuple`), and the value is a `list` of destination coordinates.''' + """ + Returns a dict of all valid moves, in subjective coordinates. + + Key: coordinates of a piece (`tuple`), + Value: list of destination coordinates. + + e.g. {(1, -5): [(1, -4), (0, -4)], (3, -5): [(4, -6), (2, -4)]} + """ moves = dict() for p in self.pieces[playerNum]: p_moves_list = self.getValidMoves(p.getCoor(), playerNum) - if p_moves_list == []: continue - p_subj_coor = obj_to_subj_coor(p.getCoor(), playerNum) - moves[p_subj_coor] = [obj_to_subj_coor(i, playerNum) for i in p_moves_list] + if p_moves_list == []: + continue + p_subj_coor = obj_to_subj_coor(p.getCoor(), playerNum, self.layout) + moves[p_subj_coor] = [ + obj_to_subj_coor(i, playerNum, self.layout) for i in p_moves_list + ] + # print(f"[Game] All moves (sub): {moves}") return moves def movePiece(self, start: tuple, end: tuple): - assert self.board[start] != None and self.board[end] == None, "AssertionError at movePiece()" + """ + Moves a piece from start coord to end coord. in objective coordinates. + """ + assert self.board[start] is not None, "startCoord is empty" + assert self.board[end] is None, "endCoord is occupied" + + # Update piece attribute self.board[start].setCoor(end) + + # Change piece's location in g.board self.board[end] = self.board[start] self.board[start] = None - - def drawBoard(self, window: pygame.Surface, playerNum: int=1): - '''inputs Surface object''' - self.drawPolygons(window, playerNum) - self.drawLines(window) - self.drawCircles(window, playerNum) - - def drawCircles(self, window:pygame.Surface, playerNum: int): - for obj_coor in self.board: - coor = obj_to_subj_coor(obj_coor, playerNum) - c = add(self.centerCoor, mult(h2c(coor),self.unitLength)) #absolute coordinates on screen - pygame.draw.circle(window, WHITE, c, self.circleRadius) - pygame.draw.circle(window, BLACK, c, self.circleRadius, self.lineWidth) - if isinstance(self.board[obj_coor], Piece): - pygame.draw.circle(window, PLAYER_COLORS[self.board[obj_coor].getPlayerNum()-1], c, self.circleRadius-2) - # coor_str = str(coor) - # text = pygame.font.Font(size=14).render(coor_str, True, BLACK, None) - # textRect = text.get_rect() - # textRect.center = c - # window.blit(text, textRect) - - def drawLines(self, window: pygame.Surface): - '''Draws the black lines of the board. Doesn't need playerNum''' - visited = set() - neighbors = set() - for coor in self.board: - for dir in DIRECTIONS: - n_coor = add(coor,dir) - if n_coor not in visited and n_coor in self.board: - neighbors.add(n_coor) - for n_coor in neighbors: - c = add(self.centerCoor, mult(h2c(coor),self.unitLength)) - n = add(self.centerCoor, mult(h2c(n_coor),self.unitLength)) - pygame.draw.line(window, BLACK, c, n, self.lineWidth) - neighbors.clear() - # self.screen_is_altered = False - - def drawPolygons(self, window: pygame.Surface, playerNum: int=1): - #center hexagon - pygame.draw.polygon(window, WHITE, (abs_coors(self.centerCoor, (-4,4), self.unitLength), abs_coors(self.centerCoor, (0,4), self.unitLength), abs_coors(self.centerCoor, (4,0), self.unitLength), abs_coors(self.centerCoor, (4,-4), self.unitLength), abs_coors(self.centerCoor, (0,-4), self.unitLength), abs_coors(self.centerCoor, (-4,0), self.unitLength))) - #triangles - if playerNum == 1: colors = (YELLOW, RED, GREEN) - elif playerNum == 2: colors = (RED, GREEN, YELLOW) - elif playerNum == 3: colors = (GREEN, YELLOW, RED) - pygame.draw.polygon(window, colors[0], (add(self.centerCoor,mult(h2c((-4,8)), self.unitLength)), add(self.centerCoor,mult(h2c((-4,4)), self.unitLength)), add(self.centerCoor,mult(h2c((0,4)), self.unitLength)))) - pygame.draw.polygon(window, colors[0], (add(self.centerCoor,mult(h2c((0,-4)), self.unitLength)), add(self.centerCoor,mult(h2c((4,-4)), self.unitLength)), add(self.centerCoor,mult(h2c((4,-8)), self.unitLength)))) - pygame.draw.polygon(window, colors[2], (add(self.centerCoor,mult(h2c((-4,0)), self.unitLength)), add(self.centerCoor,mult(h2c((-4,-4)), self.unitLength)), add(self.centerCoor,mult(h2c((0,-4)), self.unitLength)))) - pygame.draw.polygon(window, colors[2], (add(self.centerCoor,mult(h2c((0,4)), self.unitLength)), add(self.centerCoor,mult(h2c((4,4)), self.unitLength)), add(self.centerCoor,mult(h2c((4,0)), self.unitLength)))) - pygame.draw.polygon(window, colors[1], (add(self.centerCoor,mult(h2c((4,0)), self.unitLength)), add(self.centerCoor,mult(h2c((8,-4)), self.unitLength)), add(self.centerCoor,mult(h2c((4,-4)), self.unitLength)))) - pygame.draw.polygon(window, colors[1], (add(self.centerCoor,mult(h2c((-8,4)), self.unitLength)), add(self.centerCoor,mult(h2c((-4,4)), self.unitLength)), add(self.centerCoor,mult(h2c((-4,0)), self.unitLength)))) - diff --git a/game_logic/helpers.py b/game_logic/helpers.py index a02074a..1992230 100644 --- a/game_logic/helpers.py +++ b/game_logic/helpers.py @@ -1,146 +1,205 @@ -from .literals import * +""" +Helper functions to determine moves in the game. +""" import math -import pygame -from colorsys import rgb_to_hls, hls_to_rgb +from game_logic.layout import DIRECTIONS + def add(a: tuple, b: tuple): - '''Insert two tuples, return them added as if they were vectors.''' - if len(a) != len(b): raise TypeError('tuples of different length') - return tuple(a[i]+b[i] for i in range(len(a))) + """Insert two tuples, return them added as if they were vectors.""" + if len(a) != len(b): + raise TypeError("tuples of different length") + return tuple(a[i] + b[i] for i in range(len(a))) + + def mult(a: tuple, n: int): - '''Insert a tuple and an int, return them multiplied as if the tuple were a vector.''' - return tuple(a[i]*n for i in range(len(a))) + """Insert a tuple and an int, return them multiplied as if the tuple were a vector.""" + return tuple(a[i] * n for i in range(len(a))) + + def absValues(iterable): """Inputs an iterable with all ints. Returns a list of their absolute values.""" return [abs[int(i)] for i in iterable] -def checkJump(moves: list, board: dict, destination: tuple, direction: tuple, playerNum: int): - '''Recursively checks if you can jump further. Helper function of getValidMoves(). - \nInputs and outputs are in objective coordinates''' + + +def checkJump( + moves: list, + board: dict, + destination: tuple, + direction: tuple, + playerNum: int, +): + """ + Recursively checks if you can jump further. Helper function of getValidMoves(). + Inputs and outputs are in objective coordinates + """ for dir in DIRECTIONS: jumpDir = mult(dir, 2) - if dir == mult(direction, -1) or add(destination, dir) not in board or add(destination, jumpDir) not in board or add(destination, jumpDir) in moves: continue - elif board[add(destination, dir)] == None: continue - else: dest = add(destination, jumpDir) - if dest in moves: continue #prevents endless loops - elif dest not in board or board[dest] != None: continue #out of bounds or two pieces in a line - else: + if ( + dir == mult(direction, -1) + or add(destination, dir) not in board + or add(destination, jumpDir) not in board + or add(destination, jumpDir) in moves + ): + continue + elif board[add(destination, dir)] is None: + continue + else: + dest = add(destination, jumpDir) + if dest in moves: + continue # prevents endless loops + elif dest not in board or board[dest] is not None: + continue # out of bounds or two pieces in a line + else: moves.append(dest) try: - checkJump(moves, board, dest, dir, playerNum) #recursively checks available squares + checkJump( + moves, + board, + dest, + dir, + playerNum, + ) # recursively checks available squares except RecursionError: - print("RecursionError from " + str(destination) + " to " + str(dest)) - -def setItem(listt, index, item): - listt[index] = item - -def obj_to_subj_coor(c: tuple, playerNum: int): - p, q, r = c[0], c[1], 0-c[0]-c[1] - if playerNum == 1 or playerNum not in (1,2,3): return c - if playerNum == 2: return (r, p) - if playerNum == 3: return (q, r) -def subj_to_obj_coor(c: tuple, playerNum: int): - p, q, r = c[0], c[1], 0-c[0]-c[1] - if playerNum == 1 or playerNum not in (1,2,3): return c - if playerNum == 2: return (q, r) - if playerNum == 3: return (r, p) + print( + "RecursionError from " + str(destination) + " to " + str(dest), + ) + + +def setItem(list_, index, item): + list_[index] = item + + +def obj_to_subj_coor(c: tuple, playerNum: int, layout: str): + p, q, r = c[0], c[1], 0 - c[0] - c[1] + # c: (0, -4) -> pqr: (0, -4, 4) + # c: (1, -4) -> pqr: (1, -4, 3) + # c: (2, -4) -> pqr: (2, -4, 2) + # c: (3, -4) -> pqr: (3, -4, 1) + # c: (-2, 4) -> pqr: (-2, 4, -2) + if layout == "MIRROR": + if playerNum == 1: + return c + if playerNum == 2: # triangle player 5 + return (-p, -q) + if playerNum == 3: # triangle player 4 + return (-q, -r) + if playerNum == 4: # triangle player 3 + return (q, r) + if playerNum == 5: # triangle player 2 + return (r, p) + if playerNum == 6: # triangle player 6 + return (-r, -p) + elif layout == "TRIANGLE": + if playerNum == 1: + return c + if playerNum == 2: + return (r, p) + if playerNum == 3: + return (q, r) + if playerNum == 4: + return (-q, -r) + if playerNum == 5: + return (-p, -q) + if playerNum == 6: + return (-r, -p) + else: + raise ValueError("Invalid layout") + + +def subj_to_obj_coor(c: tuple, playerNum: int, layout: str): + p, q, r = c[0], c[1], 0 - c[0] - c[1] + # c: (0, -4) -> pqr: (0, -4, 4) + # c: (1, -4) -> pqr: (1, -4, 3) + # c: (2, -4) -> pqr: (2, -4, 2) + # c: (3, -4) -> pqr: (3, -4, 1) + # c: (-2, 4) -> pqr: (-2, 4, -2) + # c: (-3, -1) -> pqr: (-3, -1, 4) + if layout == "MIRROR": + if playerNum == 1: + return c + if playerNum == 2: # triangle player 5 + return (-p, -q) + if playerNum == 3: # triangle player 4 + return (-r, -p) + if playerNum == 4: # triangle player 3 + return (r, p) + if playerNum == 5: # triangle player 2 + return (q, r) + if playerNum == 6: # triangle player 6 + return (-q, -r) + elif layout == "TRIANGLE": + if playerNum == 1: + return c + if playerNum == 2: + return (q, r) + if playerNum == 3: + return (r, p) + if playerNum == 4: + return (-r, -p) + if playerNum == 5: + return (-p, -q) + if playerNum == 6: + return (-q, -r) + else: + raise ValueError("Invalid layout") + + def sign_func(i: int): - if i > 0: return 1 - if i < 0: return -1 - else: return 0 + if i > 0: + return 1 + if i < 0: + return -1 + else: + return 0 + + def distance(start: tuple, end: tuple): - i = 0; c = start; p2 = end[0]; q2 = end[1] + i = 0 + c = start + p2 = end[0] + q2 = end[1] while c != end: - p1 = c[0]; q1 = c[1]; dp = sign_func(p2 - p1); dq = sign_func(q2 - q1) - if (dp == 1 and dq == 1) or (dp == -1 and dq == -1): dq = 0 + p1 = c[0] + q1 = c[1] + dp = sign_func(p2 - p1) + dq = sign_func(q2 - q1) + if (dp == 1 and dq == 1) or (dp == -1 and dq == -1): + dq = 0 c = add(c, (dp, dq)) i += 1 return i + + def rotate(coor: tuple, angleDegrees): - x = coor[0]; y = coor[1] + x = coor[0] + y = coor[1] angle = math.radians(angleDegrees) - return (math.cos(angle) * x - math.sin(angle) * y, math.sin(angle) * x + math.cos(angle) * y) + return ( + math.cos(angle) * x - math.sin(angle) * y, + math.sin(angle) * x + math.cos(angle) * y, + ) + + def h2c(coor: tuple): - '''hexagonal to cartesian for pygame''' - x = coor[0]; y = coor[1] - x2 = x+0.5*y; y2 = -1 * 0.5 * (math.sqrt(3) * y) - return(x2, y2) -def abs_coors(center:tuple, coor:tuple, unit:int): - '''absolute coordinates on screen''' - return add(center, mult(h2c(coor),unit)) - -def adjust_color_brightness(rgbTuple:tuple, factor): - r, g, b = rgbTuple[0], rgbTuple[1], rgbTuple[2] - h, l, s = rgb_to_hls(r / 255.0, g / 255.0, b / 255.0) - l = max(min(l * factor, 1.0), 0.0) - r, g, b = hls_to_rgb(h, l, s) - return int(r * 255), int(g * 255), int(b * 255) -def brighten_color(rgbTuple: tuple, factor=0.25): - '''To darken, use a negative value for factor.\n - Factor is float with absolute value between 0 and 1, representing percentage.''' - return adjust_color_brightness(rgbTuple, 1 + factor) + """hexagonal to cartesian for pygame""" + x = coor[0] + y = coor[1] + x2 = x + 0.5 * y + y2 = -1 * 0.5 * (math.sqrt(3) * y) + return (x2, y2) + + +def abs_coors(center: tuple, coor: tuple, unit: int): + """absolute coordinates on screen""" + return add(center, mult(h2c(coor), unit)) + + def ints(s): l = [int(i) for i in s] - if isinstance(s,tuple): return tuple(l) - if isinstance(s, list): return l - if isinstance(s, set): return set(l) - -class Button: - def __init__(self, x:int=0, y:int=0, centerx:int=0, centery:int=0, width:int=200, height:int=100, enabled:bool=True, button_color:tuple=ORANGE) -> None: - """ self.x=x; self.y=y; self.width = width; self.height = height """ - self.enabled=enabled; self.button_color=button_color - if centerx and centery: - self.buttonRect = pygame.Rect( - centerx - width / 2, - centery - height / 2, - width, height) - else: - self.buttonRect = pygame.Rect(x, y, width, height) - - def draw(self, window: pygame.Surface, mouse_pos): - if self.enabled: - if self.isHovering(mouse_pos) and self.enabled: - pygame.draw.rect(window, brighten_color(self.button_color, 0.25), self.buttonRect, 0, 5) - else: pygame.draw.rect(window, self.button_color, self.buttonRect, 0, 5) - pygame.draw.rect(window, BLACK, self.buttonRect, 2, 5) - else: - pygame.draw.rect(window, GRAY, self.buttonRect, 0, 5) - - def isClicked(self, mouse_pos, mouse_left_click): - if mouse_left_click and self.buttonRect.collidepoint(mouse_pos) and self.enabled: - return True - else: return False - - def isHovering(self, mouse_pos): - if self.buttonRect.collidepoint(mouse_pos): - return True - else: return False - -class TextButton(Button): - def __init__(self, text: str, x:int=0, y:int=0, centerx:int=0, centery:int=0, width:int=200, height:int=100, enabled:bool=True, font=None, font_size=16, text_color:tuple=BLACK, button_color:tuple=ORANGE): - #super().__init__() - self.enabled=enabled; self.button_color=button_color - if centerx and centery: - self.buttonRect = pygame.Rect( - centerx - width / 2, - centery - height / 2, - width, height) - else: - self.buttonRect = pygame.Rect(x, y, width, height) - self.text = text - self.font = font; self.font_size = font_size; self.text_color = text_color; self.button_color = button_color - - def draw(self, window:pygame.Surface, mouse_pos): - text = pygame.font.SysFont(self.font, self.font_size).render(self.text, True, self.text_color) - textRect = text.get_rect() - textRect.center = self.buttonRect.center - #color = self.button_color - if not self.enabled: - color = GRAY - else: - color = self.button_color - pygame.draw.rect(window, color, self.buttonRect, 0, 5) - if self.isHovering(mouse_pos) and self.enabled: - pygame.draw.rect(window, brighten_color(color, 0.25), self.buttonRect, 0, 5) - pygame.draw.rect(window, BLACK, self.buttonRect, 2, 5) - window.blit(text, textRect) - + if isinstance(s, tuple): + return tuple(l) + if isinstance(s, list): + return l + if isinstance(s, set): + return set(l) diff --git a/game_logic/human.py b/game_logic/human.py new file mode 100644 index 0000000..a5c3158 --- /dev/null +++ b/game_logic/human.py @@ -0,0 +1,200 @@ +import math +import sys +import pygame +from pygame import QUIT, MOUSEBUTTONDOWN +from game_logic.game import Game +from game_logic.player import Player +from game_logic.piece import Piece +from game_logic.helpers import ints +from gui.constants import ( + WIDTH, + HEIGHT, + WHITE, + LIGHT_GRAY, + PLAYER_COLORS, + DARK_GRAY, +) +from gui.gui_helpers import ( + abs_coors, + brighten_color, + TextButton, + obj_to_subj_coor, + drawBoard, +) + + +class Human(Player): + def __init__(self): + super().__init__() + + def updatePieceColour( + self, + g: Game, + window: pygame.Surface, + mousePos: tuple, + piece: Piece, + ): + """ + Brightens the color of the piece if the mouse is hovering over it. + """ + coor = ( + obj_to_subj_coor(piece.getCoor(), self.playerNum, g.layout) + if self.playerNum != 0 + else piece.getCoor() + ) + absCoor = abs_coors(g.centerCoor, coor, g.unitLength) + # Case 1: Mouse is hovering over piece, brighten its color + if ( + math.dist(mousePos, absCoor) <= g.circleRadius + and piece.mouse_hovering is False + ): + # Change the piece's color + pygame.draw.circle( + window, + brighten_color(PLAYER_COLORS[piece.getPlayerNum() - 1], 0.75), + absCoor, + g.circleRadius - 2, + ) + piece.mouse_hovering = True + # Case 2: Mouse is not hovering over piece, return to original color + elif ( + math.dist(mousePos, absCoor) > g.circleRadius + and piece.mouse_hovering is True + and tuple(window.get_at(ints(absCoor))) != WHITE + ): + pygame.draw.circle( + window, + PLAYER_COLORS[piece.getPlayerNum() - 1], + absCoor, + g.circleRadius - 2, + ) + piece.mouse_hovering = False + + def pickMove( + self, + g: Game, + window: pygame.Surface, + humanPlayerNum: int = 0, + ): + """ + Returns the start and end coordinates of the selected move. + """ + print(f"[Human] is player {self.playerNum}") + pieceSet: set[Piece] = g.pieces[self.playerNum] + validMoves = [] + clicking = False + selected_piece_coor = () + prev_selected_piece_coor = () + while True: + ev = pygame.event.wait() + + # Quit the game + if ev.type == QUIT: + pygame.quit() + sys.exit() + + # Wait for a click. If mouse hovers on a piece, highlight it + mousePos = pygame.mouse.get_pos() + clicking = ev.type == MOUSEBUTTONDOWN + + # Return to main menu + backButton = TextButton( + "Back to Menu", + width=int(HEIGHT * 0.25), + height=int(HEIGHT * 0.0833), + font_size=int(WIDTH * 0.04), + ) + if backButton.isClicked(mousePos, clicking): + return (False, False) + + backButton.draw(window, mousePos) + + for piece in pieceSet: + coor = ( + obj_to_subj_coor(piece.getCoor(), self.playerNum, g.layout) + if humanPlayerNum != 0 + else piece.getCoor() + ) + absCoor = abs_coors(g.centerCoor, coor, g.unitLength) + + # Update the color of the piece based on mouse hover position + self.updatePieceColour(g, window, mousePos, piece) + + # If the selected piece is the current piece, + # and there are valid moves, + # draw a gray circle around the possible destinations. + if selected_piece_coor == piece.getCoor() and validMoves != []: + for d in validMoves: + destCoor = ( + abs_coors( + g.centerCoor, + obj_to_subj_coor(d, self.playerNum, g.layout), + g.unitLength, + ) + if humanPlayerNum != 0 + else abs_coors(g.centerCoor, d, g.unitLength) + ) + + # Gray circle if mouse is hovering over it + if math.dist(mousePos, destCoor) <= g.circleRadius: + if clicking: + move = [selected_piece_coor, d] + print(f"[Human] Move: {move}\n") + return [selected_piece_coor, d] + else: + pygame.draw.circle( + window, + LIGHT_GRAY, + destCoor, + g.circleRadius - 2, + ) + # White circle if mouse is not hovering over it + else: + pygame.draw.circle( + window, + WHITE, + destCoor, + g.circleRadius - 2, + ) + + # Check if the piece is selected + if math.dist(mousePos, absCoor) <= g.circleRadius and clicking is True: + selected_piece_coor = piece.getCoor() + if ( + prev_selected_piece_coor != () + and selected_piece_coor != prev_selected_piece_coor + ): + if humanPlayerNum != 0: + drawBoard(g, window, self.playerNum) + else: + drawBoard(g, window) + prev_selected_piece_coor = selected_piece_coor + + pygame.draw.circle( + window, + DARK_GRAY + (50,), + absCoor, + g.circleRadius, + g.lineWidth + 1, + ) + validMoves = g.getValidMoves( + selected_piece_coor, + self.playerNum, + ) + + # Draw gray circles around valid moves + for c in validMoves: + c2 = ( + obj_to_subj_coor(c, self.playerNum, g.layout) + if humanPlayerNum != 0 + else c + ) + pygame.draw.circle( + window, + DARK_GRAY, + abs_coors(g.centerCoor, c2, g.unitLength), + g.circleRadius, + g.lineWidth + 2, + ) + + pygame.display.update() diff --git a/game_logic/layout.py b/game_logic/layout.py new file mode 100644 index 0000000..501b5ef --- /dev/null +++ b/game_logic/layout.py @@ -0,0 +1,371 @@ +""" +This file contains literals used in the game logic. + +The following literals are defined: +- START_COOR: dict +- END_COOR: dict +- NEUTRAL_COOR: set +- ALL_COOR: set +- DIRECTIONS: set +- POINTS: set +""" + +# Unit vectors for the 6 directions from a cell +DIRECTIONS = {(1, 0), (0, 1), (-1, 1), (-1, 0), (0, -1), (1, -1)} + +# Zones start clockwise from the bottom-most middle yellow triangle. +# 10 piece configuration +ZONE_1_10 = { + (4, -8), + (3, -7), + (4, -7), + (2, -6), + (3, -6), + (4, -6), + (1, -5), + (2, -5), + (3, -5), + (4, -5), +} +ZONE_2_10 = { + (-4, -2), + (-4, -1), + (-3, -2), + (-4, -3), + (-3, -3), + (-2, -3), + (-1, -4), + (-4, -4), + (-3, -4), + (-2, -4), +} +ZONE_3_10 = { + (-5, 1), + (-5, 4), + (-7, 4), + (-6, 4), + (-5, 3), + (-5, 2), + (-7, 3), + (-6, 3), + (-8, 4), + (-6, 2), +} +ZONE_4_10 = { + (-4, 7), + (-3, 7), + (-1, 5), + (-4, 6), + (-3, 6), + (-2, 6), + (-4, 5), + (-3, 5), + (-4, 8), + (-2, 5), +} +ZONE_5_10 = { + (4, 4), + (2, 4), + (3, 4), + (4, 1), + (4, 3), + (1, 4), + (4, 2), + (2, 3), + (3, 3), + (3, 2), +} +ZONE_6_10 = { + (6, -4), + (7, -3), + (5, -2), + (5, -1), + (6, -2), + (5, -3), + (6, -3), + (7, -4), + (8, -4), + (5, -4), +} + +# Zones start clockwise from the bottom-most middle yellow triangle. +# 15 piece configuration +ZONE_1_15 = { + (4, -8), + (3, -7), + (4, -7), + (2, -6), + (3, -6), + (4, -6), + (1, -5), + (2, -5), + (3, -5), + (4, -5), + (0, -4), + (1, -4), + (2, -4), + (3, -4), + (4, -4), +} +ZONE_2_15 = { + (-4, -2), + (-4, -1), + (-3, -2), + (-3, -1), + (-2, -2), + (-4, -3), + (-3, -3), + (-4, 0), + (-2, -3), + (-1, -3), + (0, -4), + (-1, -4), + (-4, -4), + (-3, -4), + (-2, -4), +} +ZONE_3_15 = { + (-4, 4), + (-5, 1), + (-4, 1), + (-5, 4), + (-7, 4), + (-6, 4), + (-4, 0), + (-5, 3), + (-4, 3), + (-5, 2), + (-7, 3), + (-4, 2), + (-6, 3), + (-8, 4), + (-6, 2), +} +ZONE_4_15 = { + (-4, 4), + (-3, 4), + (0, 4), + (-4, 7), + (-2, 4), + (-3, 7), + (-1, 4), + (-1, 5), + (-4, 6), + (-3, 6), + (-2, 6), + (-4, 5), + (-3, 5), + (-4, 8), + (-2, 5), +} +ZONE_5_15 = { + (4, 4), + (2, 4), + (4, 0), + (0, 4), + (3, 4), + (4, 1), + (3, 1), + (4, 3), + (1, 4), + (4, 2), + (2, 3), + (3, 3), + (2, 2), + (3, 2), + (1, 3), +} +ZONE_6_15 = { + (6, -4), + (4, 0), + (4, -3), + (7, -3), + (5, -2), + (5, -1), + (6, -2), + (4, -4), + (5, -3), + (6, -3), + (7, -4), + (8, -4), + (4, -1), + (4, -2), + (5, -4), +} + +# Set of neutral coordinates in the center of the board +NEUTRAL_ZONE = { + (3, -2), + (3, -1), + (-3, 0), + (-3, 3), + (0, 2), + (1, -3), + (1, 0), + (-2, -1), + (-1, -2), + (-1, -1), + (-2, 1), + (-1, 1), + (3, -3), + (3, 0), + (-3, 2), + (0, -1), + (0, -2), + (0, 1), + (2, -2), + (2, -1), + (1, 2), + (2, 1), + (-2, 0), + (-1, 0), + (-2, 3), + (-1, 3), + (-2, 2), + (0, -3), + (-3, 1), + (0, 0), + (2, -3), + (1, 1), + (0, 3), + (2, 0), + (1, -2), + (1, -1), + (-1, 2), +} + +# Outer ring of neutral zone in the center +NEUTRAL_RING = { + (0, -4), + (1, -4), + (2, -4), + (3, -4), + (4, -4), + (4, -3), + (4, -2), + (4, -1), + (4, 0), + (3, 1), + (2, 2), + (1, 3), + (0, 4), + (-1, 4), + (-2, 4), + (-3, 4), + (-4, 4), + (-4, 3), + (-4, 2), + (-4, 1), + (-4, 0), + (-3, -1), + (-2, -2), + (-1, -3), +} + +# All possible coordinates on the board +ALL_COOR = ( + ZONE_1_10 + | ZONE_2_10 + | ZONE_3_10 + | ZONE_4_10 + | ZONE_5_10 + | ZONE_6_10 + | NEUTRAL_ZONE + | NEUTRAL_RING +) + + +class Layout: + def __init__(self, layout: str, n_pieces: int): + self.layout = layout # "MIRROR" or "TRIANGLE" + self.n_pieces = n_pieces # 10 or 15 + + self.START_COOR = {} + self.END_COOR = {} + self.NEUTRAL_COOR = set() + + self.set_layout() + + def set_layout(self): + if self.n_pieces == 10: + self.NEUTRAL_COOR = NEUTRAL_ZONE | NEUTRAL_RING + if self.layout == "MIRROR": + # Key: playerNum, Value: set of start coordinates + self.START_COOR = { + 1: ZONE_1_10, + 2: ZONE_4_10, + 3: ZONE_2_10, + 4: ZONE_5_10, + 5: ZONE_3_10, + 6: ZONE_6_10, + } + self.END_COOR = { + 1: ZONE_4_10, + 2: ZONE_1_10, + 3: ZONE_5_10, + 4: ZONE_2_10, + 5: ZONE_6_10, + 6: ZONE_3_10, + } + + elif self.layout == "TRIANGLE": + self.START_COOR = { + 1: ZONE_1_10, + 2: ZONE_3_10, + 3: ZONE_5_10, + 4: ZONE_2_10, + 5: ZONE_4_10, + 6: ZONE_6_10, + } + self.END_COOR = { + 1: ZONE_4_10, + 2: ZONE_6_10, + 3: ZONE_2_10, + 4: ZONE_5_10, + 5: ZONE_1_10, + 6: ZONE_3_10, + } + else: + raise ValueError(f"Invalid layout: {self.layout}") + + elif self.n_pieces == 15: + self.NEUTRAL_COOR = NEUTRAL_ZONE + if self.layout == "MIRROR": + self.START_COOR = { + 1: ZONE_1_15, + 2: ZONE_4_15, + 3: ZONE_2_15, + 4: ZONE_5_15, + 5: ZONE_3_15, + 6: ZONE_6_15, + } + self.END_COOR = { + 1: ZONE_4_15, + 2: ZONE_1_15, + 3: ZONE_5_15, + 4: ZONE_2_15, + 5: ZONE_6_15, + 6: ZONE_3_15, + } + + elif self.layout == "TRIANGLE": + self.START_COOR = { + 1: ZONE_1_15, + 2: ZONE_3_15, + 3: ZONE_5_15, + 4: ZONE_2_15, + 5: ZONE_4_15, + 6: ZONE_6_15, + } + self.END_COOR = { + 1: ZONE_4_15, + 2: ZONE_6_15, + 3: ZONE_2_15, + 4: ZONE_5_15, + 5: ZONE_1_15, + 6: ZONE_3_15, + } + else: + raise ValueError(f"Invalid layout: {self.layout}") + else: + raise ValueError(f"Invalid number of pieces: {self.n_pieces}") diff --git a/game_logic/literals.py b/game_logic/literals.py deleted file mode 100644 index 8019708..0000000 --- a/game_logic/literals.py +++ /dev/null @@ -1,93 +0,0 @@ -# from tkinter import Tk -# root = Tk() -# screen_w = int(root.winfo_screenwidth() * 0.9) -# screen_h = int(root.winfo_screenheight() * 0.9) -# root.destroy() -# del Tk -from PySide6 import QtWidgets -import sys -app = QtWidgets.QApplication(sys.argv) -screen = app.primaryScreen() -size = screen.size() -screen_w = size.width() -screen_h = size.height() -print(f"width: {screen_w}, height: {screen_h}") -if int(screen_w * (3/4)) <= screen_h: - WIDTH = screen_w; HEIGHT = int(screen_w * (3/4)) -else: - HEIGHT = screen_h; WIDTH = int(screen_h * (4/3)) -del screen_w, screen_h - - -END_COOR = { - 3: {(-4, -2), (-4, -1), (-3, -2), (-3, -1), (-2, -2), (-4, -3), (-3, -3), (-4, 0), (-2, -3), (-1, -3), (0, -4), (-1, -4), (-4, -4), (-3, -4), (-2, -4)}, - 2: {(6, -4), (4, 0), (4, -3), (7, -3), (5, -2), (5, -1), (6, -2), (4, -4), (5, -3), (6, -3), (7, -4), (8, -4), (4, -1), (4, -2), (5, -4)}, - 1: {(-4, 4), (-3, 4), (0, 4), (-4, 7), (-2, 4), (-3, 7), (-1, 4), (-1, 5), (-4, 6), (-3, 6), (-2, 6), (-4, 5), (-3, 5), (-4, 8), (-2, 5)} -} -START_COOR = { - 1: {(3, -5), (1, -4), (4, -6), (1, -5), (4, -7), (2, -6), (4, -4), (3, -6), (0, -4), (4, -5), (3, -7), (2, -4), (4, -8), (3, -4), (2, -5)}, - 2: {(-4, 4), (-5, 1), (-4, 1), (-5, 4), (-7, 4), (-6, 4), (-4, 0), (-5, 3), (-4, 3), (-5, 2), (-7, 3), (-4, 2), (-6, 3), (-8, 4), (-6, 2)}, - 3: {(4, 4), (2, 4), (4, 0), (0, 4), (3, 4), (4, 1), (3, 1), (4, 3), (1, 4), (4, 2), (2, 3), (3, 3), (2, 2), (3, 2), (1, 3)} -} -NEUTRAL_COOR = {(3, -2), (3, -1), (-3, 0), (-3, 3), (0, 2), (1, -3), (1, 0), (-2, -1), (-1, -2), (-1, -1), (-2, 1), (-1, 1), (3, -3), (3, 0), (-3, 2), (0, -1), (0, -2), (0, 1), (2, -2), (2, -1), (1, 2), (2, 1), (-2, 0), (-1, 0), (-2, 3), (-1, 3), (-2, 2), (0, -3), (-3, 1), (0, 0), (2, -3), (1, 1), (0, 3), (2, 0), (1, -2), (1, -1), (-1, 2)} -ALL_COOR = END_COOR[1]|END_COOR[2]|END_COOR[3]|START_COOR[1]|START_COOR[2]|START_COOR[3]|NEUTRAL_COOR -DIRECTIONS = {(1,0),(0,1),(-1,1),(-1,0),(0,-1),(1,-1)} -POINTS = ( - (-4,7),(-3,7), -(-4,6),(-2,6), -(-4,5),(-1,5), -(-8,4),(4,4), -(-7,3),(4,3), -(-6,2),(4,2), -(-5,1),(4,1), -(-4,0),(4,0), -(-4,-1),(5,-1), -(-4,-2),(6,-2), -(-4,-3),(7,-3), -(-4,-4),(8,-4), -(1,-5),(4,-5), -(2,-6),(4,-6), -(3,-7),(4,-7), -(-7,3),(-7,4), -(-6,2),(-6,4), -(-5,1),(-5,4), -(-4,-4),(-4,8), -(-3,-4),(-3,7), -(-2,-4),(-2,6), -(-1,-4),(-1,5), -(0,-4),(0,4), -(1,-5),(1,4), -(2,-6),(2,4), -(3,-7),(3,4), -(4,-8),(4,4), -(5,-4),(5,-1), -(6,-4),(6,-2), -(7,-4),(7,-3), -(3,4),(4,3), -(2,4),(4,2), -(1,4),(4,1), -(-4,8),(8,-4), -(-4,7),(7,-4), -(-4,6),(6,-4), -(-4,5),(5,-4), -(-4,4),(4,-4), -(-5,4),(4,-5), -(-6,4),(4,-6), -(-7,4),(7,-4), -(-8,4),(4,-8), -(-4,-1),(-1,-4), -(-4,-2),(-2,-4), -(-4,-3),(-3,-4) -) -WHITE = (255,255,255) -BLACK = (0,0,0) -RED = (210,43,43) -GREEN = (0,128,0) -YELLOW = (255,215,0) -ORANGE = (252,147,3) -GRAY = (189,189,189) -LIGHT_GRAY = (228,230,231) -PLAYER_COLORS = (YELLOW, RED, GREEN) -BG_RED = RED#(235,160,160) -BG_GREEN = GREEN#(0,200,0) -BG_YELLOW = YELLOW#(255,238,144) diff --git a/game_logic/loops.py b/game_logic/loops.py deleted file mode 100644 index 3bd353d..0000000 --- a/game_logic/loops.py +++ /dev/null @@ -1,469 +0,0 @@ -from .game import * -from .player import * -from .helpers import * -import sys, os.path -import pygame -from pygame.locals import * -from PySide6 import QtWidgets -from time import strftime -from custom_bots import * - -class LoopController: - - def __init__(self) -> None: - self.loopNum = 0 - self.winnerList = list() - self.replayRecord = list() - self.playerTypes = {} - self.filePath = '' - # key: class name strings - # value: class without () - for i in PlayerMeta.playerTypes: - self.playerTypes[i.__name__] = i - self.playerList = [ - HumanPlayer(), - Greedy1BotPlayer(), - Greedy2BotPlayer() - ] - pygame.event.set_allowed([QUIT, MOUSEBUTTONDOWN, MOUSEBUTTONUP]) - - def mainLoop(self, window: pygame.Surface): - # print(f"Loop goes on with loopNum {self.loopNum}") - if self.loopNum == 0: - self.playerList = [ - HumanPlayer(), - Greedy1BotPlayer(), - Greedy2BotPlayer() - ] - pygame.event.set_allowed([QUIT, MOUSEBUTTONDOWN, MOUSEBUTTONUP]) - self.filePath = False - self.replayRecord = [] - self.mainMenuLoop(window) - elif self.loopNum == 1: - self.loadPlayerLoop() - elif self.loopNum == 2: - self.winnerList, self.replayRecord = self.gameplayLoop( - window, self.playerList) - elif self.loopNum == 3: - self.gameOverLoop(window, self.winnerList, self.replayRecord) - elif self.loopNum == 4: - pygame.event.set_allowed([QUIT, MOUSEBUTTONDOWN, MOUSEBUTTONUP, KEYDOWN]) - pygame.key.set_repeat(100) - self.replayLoop(window, self.filePath) - elif self.loopNum == 5: - self.filePath = self.loadReplayLoop() - - def gameplayLoop(self, window: pygame.Surface, playerss: list[Player]): - playingPlayerIndex = 0 - humanPlayerNum = 0 - #returnStuff[0] is the winning player number, - #or -1 if it's a draw - #returnStuff[1] is replayRecord - #if there are two players, len(returnStuff[0]) is 1 - #otherwise, it is 2, with the first winner at index 0 - returnStuff = [[],[]] - replayRecord = [] - #replayRecord[0] marks the number of players - players = copy.deepcopy(playerss) - while None in players: players.remove(None) - if len(players) > 3: players = players[:3] - players[0].setPlayerNum(1) - players[1].setPlayerNum(2) - if len(players) == 3: players[2].setPlayerNum(3) - #generate the Game - g = Game(len(players)) - #some other settings - replayRecord.append(str(len(players))) - oneHuman = exactly_one_is_human(players) - if oneHuman: - for player in players: - if isinstance(player, HumanPlayer): - humanPlayerNum = player.getPlayerNum() - highlight = [] - #start the game loop - while True: - playingPlayer = players[playingPlayerIndex] - # If 100 milliseconds (0.1 seconds) have passed - # and there is no event, ev will be NOEVENT and - # the bot player will make a move. - # Otherwise, the bot player won't move until you - # move your mouse. - ev = pygame.event.wait(100) - if ev.type == QUIT: pygame.quit(); sys.exit() - window.fill(GRAY) - if humanPlayerNum != 0: - g.drawBoard(window, humanPlayerNum) - else: - g.drawBoard(window) - if highlight: - pygame.draw.circle(window, (117,10,199), abs_coors(g.centerCoor, highlight[0], g.unitLength), g.circleRadius, g.lineWidth+2) - pygame.draw.circle(window, (117,10,199), abs_coors(g.centerCoor, highlight[1], g.unitLength), g.circleRadius, g.lineWidth+2) - backButton = TextButton('Back to Menu', width=int(HEIGHT*0.25), height=int(HEIGHT*0.0833), font_size=int(WIDTH*0.04)) - mouse_pos = pygame.mouse.get_pos() - mouse_left_click = ev.type == MOUSEBUTTONDOWN - if backButton.isClicked(mouse_pos, mouse_left_click): - self.loopNum = 0 - return ([], []) - backButton.draw(window, mouse_pos) - pygame.display.update() - if isinstance(playingPlayer, HumanPlayer): - start_coor, end_coor = playingPlayer.pickMove(g, window, humanPlayerNum, highlight) - if (not start_coor) and (not end_coor): - self.loopNum = 0 - return ([], []) - else: - start_coor, end_coor = playingPlayer.pickMove(g) - g.movePiece(start_coor, end_coor) - if oneHuman: highlight = [obj_to_subj_coor(start_coor, humanPlayerNum), obj_to_subj_coor(end_coor, humanPlayerNum)] - else: highlight = [start_coor, end_coor] - replayRecord.append(str(start_coor)+'to'+str(end_coor)) - winning = g.checkWin(playingPlayer.getPlayerNum()) - if winning and len(players) == 2: - if humanPlayerNum != 0: - g.drawBoard(window, humanPlayerNum) - else: - g.drawBoard(window) - playingPlayer.has_won = True - returnStuff[0].append(playingPlayer.getPlayerNum()) - # print('The winner is Player %d' % playingPlayer.getPlayerNum()) - returnStuff[1] = replayRecord - self.loopNum = 3 - #print(returnStuff) - return returnStuff - elif winning and len(players) == 3: - playingPlayer.has_won = True - returnStuff[0].append(playingPlayer.getPlayerNum()) - players.remove(playingPlayer) - #TODO: show the message on screen - # print("The first winner is Player %d" % playingPlayer.getPlayerNum()) - if playingPlayerIndex >= len(players) - 1: playingPlayerIndex = 0 - else: playingPlayerIndex += 1 - - def replayLoop(self, window: pygame.Surface, filePath: str = None): - if not filePath: - print("File Path is void!") - self.loopNum = 0 - if (not self.replayRecord) and filePath: - isValidReplay = True - move_list = [] - with open(filePath) as f: - text = f.read() - move_list = text.split('\n') - playerCount = move_list.pop(0) - if not eval(playerCount) in (2, 3): - self.showNotValidReplay() - isValidReplay = False - else: - playerCount = eval(playerCount) - for i in range(len(move_list)): - move_list[i] = move_list[i].split("to") - if (len(move_list[i]) != 2): - self.showNotValidReplay() - isValidReplay = False - break - else: - for j in range(len(move_list[i])): - move_list[i][j] = eval(move_list[i][j]) - if not isinstance(move_list[i][j], tuple): - self.showNotValidReplay() - isValidReplay = False - break - for i in range(len(move_list)): - if move_list[i][0] not in ALL_COOR or move_list[i][1] not in ALL_COOR: - self.showNotValidReplay() - isValidReplay = False - break - if isValidReplay: self.replayRecord = [playerCount] + move_list - if self.replayRecord: - if f: del f - if text: del text - playerCount = self.replayRecord.pop(0) - g = Game(playerCount) - prevButton = TextButton('<', centerx=WIDTH*0.125, centery=HEIGHT*0.5, width=int(WIDTH/8), height=int(HEIGHT/6), font_size=int(WIDTH*0.04)) - nextButton = TextButton('>', centerx=WIDTH*0.875, centery=HEIGHT*0.5, width=int(WIDTH/8), height=int(HEIGHT/6), font_size=int(WIDTH*0.04)) - backButton = TextButton('Back to Menu', width=int(HEIGHT*0.25), height=int(HEIGHT*0.0833), font_size=int(WIDTH*0.04)) - moveListIndex = -1 - left = False; right = False - highlight = [] - window.fill(WHITE) - hintText = pygame.font.Font(size=int(HEIGHT*0.05)).render( - "Use the buttons or the left and right arrow keys to navigate through the game", - antialias=True, color=BLACK, wraplength=int(WIDTH*0.375)) - hintTextRect = hintText.get_rect() - hintTextRect.topright = (WIDTH, 1) - window.blit(hintText, hintTextRect) - while True: - ev = pygame.event.wait() - if ev.type == QUIT: - pygame.quit() - sys.exit() - if moveListIndex == -1: prevButton.enabled = False - else: prevButton.enabled = True - if moveListIndex == len(move_list) - 1: nextButton.enabled = False - else: nextButton.enabled = True - mouse_pos = pygame.mouse.get_pos() - mouse_left_click = ev.type == MOUSEBUTTONDOWN - left = ev.type == KEYDOWN and ev.key == K_LEFT and prevButton.enabled - right = ev.type == KEYDOWN and ev.key == K_RIGHT and nextButton.enabled - if backButton.isClicked(mouse_pos, mouse_left_click): - self.loopNum = 0 - break - if prevButton.isClicked(mouse_pos, mouse_left_click) or left: - moveListIndex -= 1 - # reverse-move move_list[moveListIndex + 1] - g.movePiece(move_list[moveListIndex + 1][1], move_list[moveListIndex + 1][0]) - highlight = move_list[moveListIndex] if moveListIndex >= 0 else [] - if nextButton.isClicked(mouse_pos, mouse_left_click) or right: - moveListIndex += 1 - # move move_list[moveListIndex] - g.movePiece(move_list[moveListIndex][0], move_list[moveListIndex][1]) - highlight = move_list[moveListIndex] - prevButton.draw(window, mouse_pos) - nextButton.draw(window, mouse_pos) - backButton.draw(window, mouse_pos) - g.drawBoard(window) - if highlight: - pygame.draw.circle(window, (117,10,199), abs_coors(g.centerCoor, highlight[0], g.unitLength), g.circleRadius, g.lineWidth+2) - pygame.draw.circle(window, (117,10,199), abs_coors(g.centerCoor, highlight[1], g.unitLength), g.circleRadius, g.lineWidth+2) - pygame.display.update() - - def loadReplayLoop(self): - if not QtWidgets.QApplication.instance(): - app = QtWidgets.QApplication(sys.argv) - else: - app = QtWidgets.QApplication.instance() - if not os.path.isdir("./replays"): os.mkdir("./replays") - filePath = QtWidgets.QFileDialog.getOpenFileName(dir="./replays", filter="*.txt")[0] - if filePath: - # print(filePath) - self.loopNum = 4 - return filePath - else: - # print("cancelled") - self.loopNum = 0 - return False - - def gameOverLoop(self, window: pygame.Surface, winnerList: list, replayRecord: list): - #print(winnerList); print(replayRecord) - #winner announcement text - if len(winnerList) == 1: - winnerString = 'Player %d wins' % winnerList[0] - elif len(winnerList) == 2: - winnerString = 'Player %d wins, then Player %d wins' % (winnerList[0], winnerList[1]) - else: - winnerString = 'len(winnerList) is %d' % len(winnerList) - font = pygame.font.SysFont('Arial', int(WIDTH*0.04)) - text = font.render(winnerString, True, BLACK, WHITE) - textRect = text.get_rect() - textRect.center = (int(WIDTH*0.5),int(HEIGHT/6)) - window.blit(text, textRect) - #buttons - menuButton = TextButton("Back to menu", centerx=int(WIDTH*0.25), centery=int(HEIGHT*2/3)) - exportReplayButton = TextButton("Export replay", centerx=int(WIDTH*0.75), centery=int(HEIGHT*2/3)) - while True: - for event in pygame.event.get(): - if event.type == QUIT: - pygame.quit() - sys.exit() - mouse_pos = pygame.mouse.get_pos() - mouse_left_click = pygame.mouse.get_pressed()[0] - if menuButton.isClicked(mouse_pos, mouse_left_click): - self.loopNum = 0 - break - if exportReplayButton.isClicked(mouse_pos, mouse_left_click): - curTime = strftime("%Y%m%d-%H%M%S") - if not os.path.isdir("./replays"): os.mkdir("./replays") - with open(f"./replays/replay-{curTime}.txt", mode="w+") as f: - for i in range(len(replayRecord)): - if i < len(replayRecord) - 1: f.write(str(replayRecord[i])+'\n') - else: f.write(str(replayRecord[i])) - exportReplayButton.text = "Replay exported!" - exportReplayButton.enabled = False - menuButton.draw(window, mouse_pos) - exportReplayButton.draw(window, mouse_pos) - pygame.display.update() - - def loadPlayerLoop(self): - loaded = False - appModifier = 0.75 - appWidth = WIDTH * appModifier - appHeight = HEIGHT * appModifier - # - if not QtWidgets.QApplication.instance(): - app = QtWidgets.QApplication(sys.argv) - else: - app = QtWidgets.QApplication.instance() - app.aboutToQuit.connect(self.closing) - Form = QtWidgets.QWidget() - Form.setWindowTitle("Game Settings") - Form.resize(appWidth, appHeight) - # - box = QtWidgets.QWidget(Form) - box.setGeometry( - appWidth * 0.0625, appHeight * 0.0625, - appWidth * 0.875, appHeight * 0.625) - grid = QtWidgets.QGridLayout(box) - # - label_pNum = QtWidgets.QLabel(Form) - label_pNum.setText("Number of Players") - rButton_2P = QtWidgets.QRadioButton(Form) - rButton_2P.setText('2') - rButton_2P.toggled.connect( - lambda: label_p3Type.setStyleSheet("color: #878787;")) - rButton_2P.toggled.connect( - lambda: cBox_p3.setDisabled(True)) - rButton_2P.toggled.connect( - lambda: setItem(self.playerList, 2, None)) - # rButton_2P.toggled.connect( - # lambda: print(self.playerList)) - rButton_3P = QtWidgets.QRadioButton(Form) - rButton_3P.setText('3') - rButton_3P.setChecked(True) - rButton_3P.toggled.connect( - lambda: label_p3Type.setStyleSheet("color: #000000;")) - rButton_3P.toggled.connect( - lambda: cBox_p3.setDisabled(False)) - rButton_3P.toggled.connect( - lambda: setItem(self.playerList, 2, - self.playerTypes[cBox_p3.currentText()]())) - label_p1Type = QtWidgets.QLabel(Form) - label_p1Type.setText("Player 1:") - label_p2Type = QtWidgets.QLabel(Form) - label_p2Type.setText("Player 2:") - label_p3Type = QtWidgets.QLabel(Form) - label_p3Type.setText("Player 3:") - cBox_p1 = QtWidgets.QComboBox(Form) - cBox_p2 = QtWidgets.QComboBox(Form) - cBox_p3 = QtWidgets.QComboBox(Form) - cBoxes = (cBox_p1, cBox_p2, cBox_p3) - - if not loaded: - initialPlayerList = [HumanPlayer, Greedy1BotPlayer, Greedy2BotPlayer] - for i in range(3): - grid.addWidget(cBoxes[i], i+1, 2, 1, 2) - cBoxes[i].addItems(list(self.playerTypes)) - cBoxes[i].setCurrentIndex(list(self.playerTypes.values()).index(initialPlayerList[i])) - loaded = True - del initialPlayerList - - cBox_p1.currentIndexChanged.connect( - lambda: setItem(self.playerList, 0, self.playerTypes[cBox_p1.currentText()]())) - # cBox_p1.currentIndexChanged.connect( - # lambda: print(self.playerList)) - cBox_p2.currentIndexChanged.connect( - lambda: setItem(self.playerList, 1, self.playerTypes[cBox_p2.currentText()]())) - # cBox_p2.currentIndexChanged.connect( - # lambda: print(self.playerList)) - cBox_p3.currentIndexChanged.connect( - lambda: setItem(self.playerList, 2, self.playerTypes[cBox_p3.currentText()]())) - # cBox_p3.currentIndexChanged.connect( - # lambda: print(self.playerList)) - # - grid.addWidget(label_pNum, 0, 0, 1, 2) - grid.addWidget(rButton_2P, 0, 2) - grid.addWidget(rButton_3P, 0, 3) - grid.addWidget(label_p1Type, 1, 0, 1, 2) - grid.addWidget(label_p2Type, 2, 0, 1, 2) - grid.addWidget(label_p3Type, 3, 0, 1, 2) - # - startButton = QtWidgets.QPushButton(Form) - startButton.setText("Start Game") - startButton.setGeometry( - appWidth * 0.625, appHeight * 0.8125, - appWidth * 0.25, appHeight * 0.125 - ) - startButton.clicked.connect(self.startGame) - # - cancelButton = QtWidgets.QPushButton(Form) - cancelButton.setText("Back to Menu") - cancelButton.setGeometry( - appWidth * 0.125, appHeight * 0.8125, - appWidth * 0.25, appHeight * 0.125 - ) - cancelButton.clicked.connect(self.backToMenu) - # - Form.show() - app.exec() - - #helpers for loadPlayerLoop and replayLoop - def startGame(self): - # print(self.playerList) - self.loopNum = 2 #go to gameplay - QtWidgets.QApplication.closeAllWindows() - def backToMenu(self): - self.loopNum = 0 #go to main menu - QtWidgets.QApplication.closeAllWindows() - def closing(self): - if self.loopNum == 0 or self.loopNum == 1: self.backToMenu() - elif self.loopNum == 2: self.startGame() - def showNotValidReplay(self): - print("This is not a valid replay!") - self.loopNum = 0 - - def mainMenuLoop(self, window:pygame.Surface): - window.fill(WHITE) - titleText = pygame.font.Font(size=int(WIDTH*0.08)).render( - "Chinese Checkers", True, BLACK) - titleTextRect = titleText.get_rect() - titleTextRect.center = (WIDTH*0.5, HEIGHT*0.25) - window.blit(titleText, titleTextRect) - playButton = TextButton( - "Play", centerx=int(WIDTH*0.5), centery=int(HEIGHT*0.375), width=WIDTH*0.25, height=HEIGHT*0.125, font_size=32) - loadReplayButton = TextButton( - "Load replay", centerx=int(WIDTH*0.5), centery=int(HEIGHT*0.625), width=WIDTH*0.25, height=HEIGHT*0.125, font_size=32) - while True: - ev = pygame.event.wait() - if ev.type == QUIT: - pygame.quit() - sys.exit() - mouse_pos = pygame.mouse.get_pos() - mouse_left_click = ev.type == MOUSEBUTTONDOWN - if playButton.isClicked(mouse_pos, mouse_left_click): - # print("play") - self.loopNum = 1 - break - if loadReplayButton.isClicked(mouse_pos, mouse_left_click): - # print('load-replay') - self.loopNum = 5 - break - - playButton.draw(window, mouse_pos) - loadReplayButton.draw(window, mouse_pos) - pygame.display.update() - -def exactly_one_is_human(players: list[Player]): - b = False - for player in players: - if b == False and isinstance(player, HumanPlayer): - b = True - elif b == True and isinstance(player, HumanPlayer): - return False - return b - -def trainingLoop(g: Game, players: list[Player], recordReplay: bool=False): - playingPlayerIndex = 0 - replayRecord = [] - if recordReplay: - replayRecord.append(str(len(players))) - for player in players: - assert not isinstance(player, HumanPlayer), "Can't have humans during training! Human at player %d" % players.index(player) + 1 - for i in range(len(players)): - players[i].setPlayerNum(i+1) - while True: - playingPlayer = players[playingPlayerIndex] - start_coor, end_coor = playingPlayer.pickMove(g) - g.movePiece(start_coor, end_coor) - if recordReplay: - replayRecord.append(str(start_coor)+' '+str(end_coor)) - winning = g.checkWin(playingPlayer.getPlayerNum()) - if winning and len(players) == 2: - playingPlayer.has_won = True - print('The winner is Player %d' % playingPlayer.getPlayerNum()) - print(f"{len(replayRecord)} moves") - break #TODO: return stuff? - elif winning and len(players) == 3: - playingPlayer.has_won = True - players.remove(playingPlayer) - print("The first winner is Player %d" % playingPlayer.getPlayerNum()) - if playingPlayerIndex >= len(players) - 1: playingPlayerIndex = 0 - else: playingPlayerIndex += 1 diff --git a/game_logic/piece.py b/game_logic/piece.py index 6f0ff3e..5ed33d6 100644 --- a/game_logic/piece.py +++ b/game_logic/piece.py @@ -1,3 +1,9 @@ +""" +Class to represent a piece on the board. Each piece has a player number, +and a position on the board. +""" + + class Piece: def __init__(self, playerNum: int, p: int, q: int): self.playerNum = playerNum @@ -5,12 +11,15 @@ def __init__(self, playerNum: int, p: int, q: int): self.q = q self.mouse_hovering = False self.selected = False + def __hash__(self) -> int: return id(self) - - def getPlayerNum(self): return self.playerNum - def getCoor(self): return (self.p, self.q) + def getPlayerNum(self): + return self.playerNum + + def getCoor(self): + return (self.p, self.q) def setCoor(self, new_coor: tuple): self.p = new_coor[0] diff --git a/game_logic/player.py b/game_logic/player.py index 1be981e..e0b797a 100644 --- a/game_logic/player.py +++ b/game_logic/player.py @@ -1,13 +1,9 @@ -from .game import * -from .piece import * -from .literals import * -from .helpers import * -import random -import pygame -import math -from pygame.locals import * -import sys +""" +Abstract class for human players and bots in the game. +""" from abc import ABC, ABCMeta, abstractmethod +from game_logic.game import Game + class PlayerMeta(ABCMeta): playerTypes = [] @@ -17,232 +13,25 @@ def __init__(cls, name, bases, attrs): PlayerMeta.playerTypes.append(cls) super().__init__(name, bases, attrs) + class Player(ABC, metaclass=PlayerMeta): def __init__(self): - self.playerNum = 0 + self.playerNum = 0 # Starting from 1 self.has_won = False + def getPlayerNum(self): return self.playerNum + def setPlayerNum(self, num: int): + """ + Starting from 1. + """ self.playerNum = num - - @abstractmethod - def pickMove(self, g:Game): - ... - -class RandomBotPlayer(Player): - def __init__(self): - super().__init__() - - def pickMove(self, g: Game): - '''returns [start_coor, end_coor]''' - moves = g.allMovesDict(self.playerNum) - l = [] - for coor in moves: - if moves[coor] != []: l.append(coor) - coor = random.choice(l) - move = random.choice(moves[coor]) - return [subj_to_obj_coor(coor, self.playerNum), subj_to_obj_coor(move, self.playerNum)] - -class GreedyRandomBotPlayer(Player): - def __init__(self): - super().__init__() - - def pickMove(self, g: Game): - '''returns [start_coor, end_coor]''' - moves = g.allMovesDict(self.playerNum) - tempMoves = dict() - #forward - for coor in moves: - if moves[coor] != []: tempMoves[coor] = [] - else: continue - for dest in moves[coor]: - if dest[1] > coor[1]: tempMoves[coor].append(dest) - for coor in list(tempMoves): - if tempMoves[coor] == []: del tempMoves[coor] - if len(tempMoves) > 0: - coor = random.choice(list(tempMoves)) - move = random.choice(tempMoves[coor]) - else: - #sideways - tempMoves.clear() - for coor in moves: - if moves[coor] != []: tempMoves[coor] = [] - else: continue - for dest in moves[coor]: - if dest[1] == coor[1]: tempMoves[coor].append(dest) - for coor in list(tempMoves): - if tempMoves[coor] == []: del tempMoves[coor] - coor = random.choice(list(tempMoves)) - move = random.choice(tempMoves[coor]) - return [subj_to_obj_coor(coor, self.playerNum), subj_to_obj_coor(move, self.playerNum)] - -class Greedy1BotPlayer(Player): - '''Always finds the move that moves a piece to the topmost square''' - def __init__(self): - super().__init__() - - def pickMove(self, g: Game): - '''returns [start_coor, end_coor] in objective coordinates''' - moves = g.allMovesDict(self.playerNum) - #state = g.boardState(self.playerNum) - forwardMoves = dict() - sidewaysMoves = dict() - start_coor = () - end_coor = () - #split moves into forward and sideways - for coor in moves: - if moves[coor] != []: forwardMoves[coor] = []; sidewaysMoves[coor] = [] - else: continue - for dest in moves[coor]: - if dest[1] > coor[1]: forwardMoves[coor].append(dest) - if dest[1] == coor[1]: sidewaysMoves[coor].append(dest) - for coor in list(forwardMoves): - if forwardMoves[coor] == []: del forwardMoves[coor] - for coor in list(sidewaysMoves): - if sidewaysMoves[coor] == []: del sidewaysMoves[coor] - - #choose the furthest destination (biggest y value in dest), - #then backmost piece (smallest y value in coor) - biggestDestY = -8 - smallestStartY = 8 - if len(forwardMoves) == 0: - start_coor = random.choice(list(sidewaysMoves)) - end_coor = random.choice(sidewaysMoves[start_coor]) - else: - for coor in forwardMoves: - for i in range(len(forwardMoves[coor])): - dest = forwardMoves[coor][i] - if dest[1] > biggestDestY: - start_coor = coor; end_coor = dest - biggestDestY = dest[1] - smallestStartY = coor[1] - elif dest[1] == biggestDestY: - startY = coor[1] - if startY < smallestStartY: - start_coor = coor; end_coor = dest - biggestDestY = dest[1] - smallestStartY = coor[1] - elif startY == smallestStartY: - start_coor, end_coor = random.choice([[start_coor, end_coor], [coor, dest]]) - biggestDestY = end_coor[1] - smallestStartY = start_coor[1] - return [subj_to_obj_coor(start_coor, self.playerNum), subj_to_obj_coor(end_coor, self.playerNum)] - -class Greedy2BotPlayer(Player): - '''Always finds a move that jumps through the maximum distance (dest[1] - coor[1])''' - def __init__(self): - super().__init__() + @abstractmethod def pickMove(self, g: Game): - '''returns [start_coor, end_coor] in objective coordinates\n - return [subj_to_obj_coor(start_coor, self.playerNum), subj_to_obj_coor(end_coor, self.playerNum)]''' - moves = g.allMovesDict(self.playerNum) - #state = g.boardState(self.playerNum) - forwardMoves = dict() - sidewaysMoves = dict() - start_coor = () - end_coor = () - max_dist = 0 - #split moves into forward and sideways - for coor in moves: - if moves[coor] != []: forwardMoves[coor] = []; sidewaysMoves[coor] = [] - else: continue - for dest in moves[coor]: - if dest[1] > coor[1]: forwardMoves[coor].append(dest) - if dest[1] == coor[1]: sidewaysMoves[coor].append(dest) - for coor in list(forwardMoves): - if forwardMoves[coor] == []: del forwardMoves[coor] - for coor in list(sidewaysMoves): - if sidewaysMoves[coor] == []: del sidewaysMoves[coor] - #if forward is empty, move sideways - if len(forwardMoves) == 0: - start_coor = random.choice(list(sidewaysMoves)) - end_coor = random.choice(sidewaysMoves[start_coor]) - return [subj_to_obj_coor(start_coor, self.playerNum), subj_to_obj_coor(end_coor, self.playerNum)] - #forward: max distance - for coor in forwardMoves: - for dest in forwardMoves[coor]: - if start_coor == () and end_coor == (): - start_coor = coor; end_coor = dest - max_dist = end_coor[1] - start_coor[1] - else: - dist = dest[1] - coor[1] - if dist > max_dist: - max_dist = dist; start_coor = coor; end_coor = dest - elif dist == max_dist: - #prefers to move the piece that is more backwards - if dest[1] < end_coor[1]: max_dist = dist; start_coor = coor; end_coor = dest - return [subj_to_obj_coor(start_coor, self.playerNum), subj_to_obj_coor(end_coor, self.playerNum)] - -class HumanPlayer(Player): - def __init__(self): - super().__init__() - - def pickMove(self, g:Game, window:pygame.Surface, humanPlayerNum: int=0, highlight=None): - pieceSet:set[Piece] = g.pieces[self.playerNum] - validmoves = [] - clicking = False - selected_piece_coor = () - prev_selected_piece_coor = () - #pygame.event.set_allowed([QUIT, MOUSEBUTTONDOWN, MOUSEBUTTONUP]) - while True: - ev = pygame.event.wait() - if ev.type == QUIT: - pygame.quit() - sys.exit() - #wait for a click, - #if mouse hovers on a piece, highlight it - mouse_pos = pygame.mouse.get_pos() - clicking = ev.type == MOUSEBUTTONDOWN - # - if highlight: - pygame.draw.circle(window, (117,10,199), abs_coors(g.centerCoor, highlight[0], g.unitLength), g.circleRadius, g.lineWidth+2) - pygame.draw.circle(window, (117,10,199), abs_coors(g.centerCoor, highlight[1], g.unitLength), g.circleRadius, g.lineWidth+2) - - backButton = TextButton('Back to Menu', width=int(HEIGHT*0.25), height=int(HEIGHT*0.0833), font_size=int(WIDTH*0.04)) - if backButton.isClicked(mouse_pos, clicking): - return (False, False) - backButton.draw(window, mouse_pos) - - for piece in pieceSet: - coor = obj_to_subj_coor(piece.getCoor(), self.playerNum) if humanPlayerNum != 0 else piece.getCoor() - absCoor = abs_coors(g.centerCoor, coor, g.unitLength) - if math.dist(mouse_pos, absCoor) <= g.circleRadius and piece.mouse_hovering == False: - #change the piece's color - pygame.draw.circle(window, brighten_color(PLAYER_COLORS[piece.getPlayerNum()-1], 0.75), absCoor, g.circleRadius-2) - piece.mouse_hovering = True - elif math.dist(mouse_pos, absCoor) > g.circleRadius and piece.mouse_hovering == True and tuple(window.get_at(ints(absCoor))) != WHITE: - #draw a circle of the original color - pygame.draw.circle(window, PLAYER_COLORS[piece.getPlayerNum()-1], absCoor, g.circleRadius-2) - piece.mouse_hovering = False - #when a piece is selected, and you click any of the valid destinations, - #you will move that piece to the destination - if selected_piece_coor == piece.getCoor() and validmoves != []: - for d in validmoves: - destCoor = abs_coors(g.centerCoor, obj_to_subj_coor(d, self.playerNum), g.unitLength) if humanPlayerNum != 0 else abs_coors(g.centerCoor, d, g.unitLength) - if math.dist(mouse_pos, destCoor) <= g.circleRadius: - if clicking: - return [selected_piece_coor, d] - #draw a gray circle - else: pygame.draw.circle(window, LIGHT_GRAY, destCoor, g.circleRadius-2) - elif math.dist(mouse_pos, destCoor) > g.circleRadius: - #draw a white circle - pygame.draw.circle(window, WHITE, destCoor, g.circleRadius-2) - #clicking the piece - if math.dist(mouse_pos, absCoor) <= g.circleRadius and clicking == True: - selected_piece_coor = piece.getCoor() - if prev_selected_piece_coor != () and selected_piece_coor != prev_selected_piece_coor: - if humanPlayerNum != 0: g.drawBoard(window, self.playerNum) - else: g.drawBoard(window) - prev_selected_piece_coor = selected_piece_coor - #draw a semi-transparent gray circle outside the piece - pygame.draw.circle(window, (161,166,196,50), absCoor, g.circleRadius, g.lineWidth+1) - #draw semi-transparent circles around all coordinates in getValidMoves() - validmoves = g.getValidMoves(selected_piece_coor, self.playerNum) - for c in validmoves: - c2 = obj_to_subj_coor(c, self.playerNum) if humanPlayerNum != 0 else c - pygame.draw.circle(window, (161,166,196), abs_coors(g.centerCoor, c2, g.unitLength), g.circleRadius, g.lineWidth+2) - - pygame.display.update() - #return [start_coor, end_coor] + """ + Returns: + [start_coor, end_coor] : in objective coordinates + """ + ... diff --git a/gui/__init__.py b/gui/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/gui/constants.py b/gui/constants.py new file mode 100644 index 0000000..e57935a --- /dev/null +++ b/gui/constants.py @@ -0,0 +1,68 @@ +""" +This file contains literals used in to create the GUI. + +The following literals are defined: +- WIDTH: int +- HEIGHT: int +and other colours used. +""" + +# from tkinter import Tk +# root = Tk() +# screen_w = int(root.winfo_screenwidth() * 0.9) +# screen_h = int(root.winfo_screenheight() * 0.9) +# root.destroy() +# del Tk + +# Dynamic window size based on user's screen +import sys +from PySide6 import QtWidgets + +app = QtWidgets.QApplication(sys.argv) +screen = app.primaryScreen() +size = screen.size() + +screen_w = size.width() * 0.9 +screen_h = size.height() * 0.9 + +# Enforce aspect ratio of 4:3 +if int(screen_w * (3 / 4)) <= screen_h: + WIDTH = screen_w + HEIGHT = int(screen_w * (3 / 4)) +else: + HEIGHT = screen_h + WIDTH = int(screen_h * (4 / 3)) +del screen_w, screen_h + +# HEIGHT = 800 +# WIDTH = HEIGHT * 4 // 3 +print(f"[gui.constants] Creating GUI window of size ({WIDTH},{HEIGHT})") + +# Colors +WHITE = (255, 255, 255) +BLACK = (0, 0, 0) +RED = (210, 43, 43) +GREEN = (0, 128, 0) +BLUE = (0, 0, 128) +LIGHT_BLUE = (173, 216, 230) +YELLOW = (255, 215, 0) +ORANGE = (252, 147, 3) +GRAY = (189, 189, 189) +TEAL = (0, 128, 128) +LIGHT_GRAY = (228, 230, 231) +DARK_GRAY = (161, 166, 196) +PURPLE = (117, 10, 199) +BG_RED = RED # (235,160,160) +BG_GREEN = GREEN # (0,200,0) +BG_YELLOW = YELLOW # (255,238,144) + +PLAYER_COLORS = (YELLOW, RED, GREEN, LIGHT_BLUE, ORANGE, TEAL) +# coordinates to draw text +PLAYER_LABELS = [ + (7, -7), + (-6, 7), + (-4, -5), + (4, 5), + (-8, 5), + (8, -5), +] diff --git a/gui/gui_helpers.py b/gui/gui_helpers.py new file mode 100644 index 0000000..77bfce6 --- /dev/null +++ b/gui/gui_helpers.py @@ -0,0 +1,397 @@ +import pygame +from colorsys import rgb_to_hls, hls_to_rgb +from game_logic.layout import DIRECTIONS +from game_logic.game import Game +from game_logic.helpers import add, abs_coors, h2c, mult, obj_to_subj_coor +from game_logic.piece import Piece +from gui.constants import ( + BLACK, + GRAY, + ORANGE, + RED, + WHITE, + YELLOW, + GREEN, + PLAYER_COLORS, + PURPLE, + WIDTH, + PLAYER_LABELS, +) + + +def highlightMove(g: Game, window: pygame.Surface, move): + """ + Highlights the start and end coordinates of a move. + """ + if move == []: + return + # Highlight start coordinate + pygame.draw.circle( + window, + PURPLE, + abs_coors(g.centerCoor, move[0], g.unitLength), + g.circleRadius, + g.lineWidth + 2, + ) + + # Highlight end coordinate + pygame.draw.circle( + window, + PURPLE, + abs_coors(g.centerCoor, move[1], g.unitLength), + g.circleRadius, + g.lineWidth + 2, + ) + + +def adjust_color_brightness(rgbTuple: tuple, factor): + r, g, b = rgbTuple[0], rgbTuple[1], rgbTuple[2] + h, l, s = rgb_to_hls(r / 255.0, g / 255.0, b / 255.0) + l = max(min(l * factor, 1.0), 0.0) + r, g, b = hls_to_rgb(h, l, s) + return int(r * 255), int(g * 255), int(b * 255) + + +def brighten_color(rgbTuple: tuple, factor=0.25): + """To darken, use a negative value for factor.\n + Factor is float with absolute value between 0 and 1, representing percentage.""" + return adjust_color_brightness(rgbTuple, 1 + factor) + + +def drawPath(g: Game, window: pygame.Surface, path: list): + """ + Draws dots in the cells along the path of a move. + """ + if path is None: + return + for i in range(len(path) - 1): + cell = abs_coors(g.centerCoor, path[i], g.unitLength) + nextCell = abs_coors(g.centerCoor, path[i + 1], g.unitLength) + pygame.draw.circle(window, PURPLE, cell, int(0.3 * g.circleRadius)) + pygame.draw.circle(window, PURPLE, nextCell, int(0.3 * g.circleRadius)) + + +def drawBoard(g: Game, window: pygame.Surface, playerNum: int = 1): + """ + Draws the board polygon, lines and circles. + """ + drawPolygons(g, window) + drawTriangles(g, window, playerNum) + drawLines(g, window) + drawCircles(g, window, playerNum) + # drawCoordinates(g, window) + drawPlayerTypes(g, window) + drawTurnCount(g, window) + + +def drawTurnCount(g: Game, window: pygame.Surface): + """ + Adds the turn count to the window. + """ + playerName = g.playerNames[g.playerNum - 1] + n_players = len(g.playerNames) + turn_cycle = (g.turnCount - 1) // n_players + 1 + subturn = (g.turnCount - 1) % n_players + 1 + text = pygame.font.Font(size=int(WIDTH * 0.04)).render( + f"Turn: {turn_cycle}.{subturn}, {playerName}", + True, + BLACK, + None, + ) + textRect = text.get_rect() + textRect.center = add( + g.centerCoor, + mult(h2c((-3, -7)), g.unitLength), + ) + window.blit(text, textRect) + + +def drawPlayerTypes(g: Game, window: pygame.Surface): + """ + Adds the player types to the window. + """ + + for p, coor in zip(g.playerList, PLAYER_LABELS): + c = add( + g.centerCoor, + mult(h2c(coor), g.unitLength), + ) # absolute coordinates on screen + playerStr = type(p).__name__ + text = pygame.font.Font(size=int(WIDTH * 0.035)).render( + playerStr, + True, + PLAYER_COLORS[p.getPlayerNum() - 1], + None, + ) + textRect = text.get_rect() + textRect.center = c + window.blit(text, textRect) + + +def drawCircles(g: Game, window: pygame.Surface, playerNum: int): + for obj_coor in g.board: + # Draw an empty cell + coor = obj_to_subj_coor(obj_coor, playerNum, g.layout) + c = add( + g.centerCoor, + mult(h2c(coor), g.unitLength), + ) # absolute coordinates on screen + pygame.draw.circle(window, WHITE, c, g.circleRadius) + pygame.draw.circle(window, BLACK, c, g.circleRadius, g.lineWidth) + + # Draw player's piece if the cell is occupied + if isinstance(g.board[obj_coor], Piece): + pygame.draw.circle( + window, + PLAYER_COLORS[g.board[obj_coor].getPlayerNum() - 1], + c, + g.circleRadius - 2, + ) + + +def drawLines(g: Game, window: pygame.Surface): + """ + Draws the black lines to connect the cells. + """ + visited = set() + neighbors = set() + for coor in g.board: + for dir in DIRECTIONS: + n_coor = add(coor, dir) + if n_coor not in visited and n_coor in g.board: + neighbors.add(n_coor) + for n_coor in neighbors: + c = add(g.centerCoor, mult(h2c(coor), g.unitLength)) + n = add(g.centerCoor, mult(h2c(n_coor), g.unitLength)) + pygame.draw.line(window, BLACK, c, n, g.lineWidth) + neighbors.clear() + + +def drawPolygons(g: Game, window: pygame.Surface): + # center hexagon + pygame.draw.polygon( + window, + GRAY, + ( + abs_coors(g.centerCoor, (-4, 4), g.unitLength), + abs_coors(g.centerCoor, (0, 4), g.unitLength), + abs_coors(g.centerCoor, (4, 0), g.unitLength), + abs_coors(g.centerCoor, (4, -4), g.unitLength), + abs_coors(g.centerCoor, (0, -4), g.unitLength), + abs_coors(g.centerCoor, (-4, 0), g.unitLength), + ), + ) + + +def drawTriangles(g: Game, window: pygame.Surface, playerNum: int = 1): + # Set sequence of player colours. + if playerNum == 1: + colors = (YELLOW, RED, GREEN) + elif playerNum == 2: + colors = (RED, GREEN, YELLOW) + elif playerNum == 3: + colors = (GREEN, YELLOW, RED) + + # Draw the 6 triangles + pygame.draw.polygon( + window, + colors[0], + ( + add(g.centerCoor, mult(h2c((-4, 8)), g.unitLength)), + add(g.centerCoor, mult(h2c((-4, 4)), g.unitLength)), + add(g.centerCoor, mult(h2c((0, 4)), g.unitLength)), + ), + ) + pygame.draw.polygon( + window, + colors[0], + ( + add(g.centerCoor, mult(h2c((0, -4)), g.unitLength)), + add(g.centerCoor, mult(h2c((4, -4)), g.unitLength)), + add(g.centerCoor, mult(h2c((4, -8)), g.unitLength)), + ), + ) + pygame.draw.polygon( + window, + colors[2], + ( + add(g.centerCoor, mult(h2c((-4, 0)), g.unitLength)), + add(g.centerCoor, mult(h2c((-4, -4)), g.unitLength)), + add(g.centerCoor, mult(h2c((0, -4)), g.unitLength)), + ), + ) + pygame.draw.polygon( + window, + colors[2], + ( + add(g.centerCoor, mult(h2c((0, 4)), g.unitLength)), + add(g.centerCoor, mult(h2c((4, 4)), g.unitLength)), + add(g.centerCoor, mult(h2c((4, 0)), g.unitLength)), + ), + ) + pygame.draw.polygon( + window, + colors[1], + ( + add(g.centerCoor, mult(h2c((4, 0)), g.unitLength)), + add(g.centerCoor, mult(h2c((8, -4)), g.unitLength)), + add(g.centerCoor, mult(h2c((4, -4)), g.unitLength)), + ), + ) + pygame.draw.polygon( + window, + colors[1], + ( + add(g.centerCoor, mult(h2c((-8, 4)), g.unitLength)), + add(g.centerCoor, mult(h2c((-4, 4)), g.unitLength)), + add(g.centerCoor, mult(h2c((-4, 0)), g.unitLength)), + ), + ) + + +def drawCoordinates(g: Game, window: pygame.Surface): + """ + Adds the coordinates of each cell to the window. + """ + for coor in g.board: + c = add( + g.centerCoor, + mult(h2c(coor), g.unitLength), + ) # absolute coordinates on screen + coor_str = f"{coor[0]}, {coor[1]}" + text = pygame.font.Font(size=int(WIDTH * 0.0175)).render( + coor_str, + True, + BLACK, + None, + ) + textRect = text.get_rect() + textRect.center = c + window.blit(text, textRect) + + +class Button: + def __init__( + self, + x: int = 0, + y: int = 0, + centerx: int = 0, + centery: int = 0, + width: int = 200, + height: int = 100, + enabled: bool = True, + button_color: tuple = ORANGE, + ) -> None: + """self.x=x; self.y=y; self.width = width; self.height = height""" + self.enabled = enabled + self.button_color = button_color + if centerx and centery: + self.buttonRect = pygame.Rect( + centerx - width / 2, + centery - height / 2, + width, + height, + ) + else: + self.buttonRect = pygame.Rect(x, y, width, height) + + def draw(self, window: pygame.Surface, mouse_pos): + if self.enabled: + if self.isHovering(mouse_pos) and self.enabled: + pygame.draw.rect( + window, + brighten_color(self.button_color, 0.25), + self.buttonRect, + 0, + 5, + ) + else: + pygame.draw.rect( + window, + self.button_color, + self.buttonRect, + 0, + 5, + ) + pygame.draw.rect(window, BLACK, self.buttonRect, 2, 5) + else: + pygame.draw.rect(window, GRAY, self.buttonRect, 0, 5) + + def isClicked(self, mouse_pos, mouse_left_click): + if ( + mouse_left_click + and self.buttonRect.collidepoint(mouse_pos) + and self.enabled + ): + return True + else: + return False + + def isHovering(self, mouse_pos): + if self.buttonRect.collidepoint(mouse_pos): + return True + else: + return False + + +class TextButton(Button): + def __init__( + self, + text: str, + x: int = 0, + y: int = 0, + centerx: int = 0, + centery: int = 0, + width: int = 200, + height: int = 100, + enabled: bool = True, + font=None, + font_size=16, + text_color: tuple = BLACK, + button_color: tuple = ORANGE, + ): + # super().__init__() + self.enabled = enabled + self.button_color = button_color + if centerx and centery: + self.buttonRect = pygame.Rect( + centerx - width / 2, + centery - height / 2, + width, + height, + ) + else: + self.buttonRect = pygame.Rect(x, y, width, height) + self.text = text + self.font = font + self.font_size = font_size + self.text_color = text_color + self.button_color = button_color + + def draw(self, window: pygame.Surface, mouse_pos): + """ + Fades the button if the mouse is hovering over it. + """ + text = pygame.font.SysFont(self.font, self.font_size).render( + self.text, + True, + self.text_color, + ) + textRect = text.get_rect() + textRect.center = self.buttonRect.center + # color = self.button_color + if not self.enabled: + color = GRAY + else: + color = self.button_color + pygame.draw.rect(window, color, self.buttonRect, 0, 5) + if self.isHovering(mouse_pos) and self.enabled: + pygame.draw.rect( + window, + brighten_color(color, 0.25), + self.buttonRect, + 0, + 5, + ) + pygame.draw.rect(window, BLACK, self.buttonRect, 2, 5) + window.blit(text, textRect) diff --git a/gui/loops.py b/gui/loops.py new file mode 100644 index 0000000..f8e49b5 --- /dev/null +++ b/gui/loops.py @@ -0,0 +1,872 @@ +""" +LoopController class is used to control the game windows displayed. +""" +import os.path +import pygame +import sys +from copy import deepcopy +from bots.GreedyBot0 import GreedyBot0 +from bots.GreedyBot1 import GreedyBot1 +from bots.GreedyBot2 import GreedyBot2 +from game_logic.layout import ALL_COOR +from game_logic.game import Game +from game_logic.helpers import obj_to_subj_coor, setItem +from game_logic.human import Human +from game_logic.player import Player, PlayerMeta +from gui.constants import WIDTH, HEIGHT, WHITE, BLACK, GRAY +from gui.gui_helpers import TextButton, drawBoard, drawPath, highlightMove +from pygame import ( + QUIT, + MOUSEBUTTONDOWN, + MOUSEBUTTONUP, + KEYDOWN, + K_LEFT, + K_RIGHT, +) +from PySide6 import QtWidgets +from time import strftime + +# The following is necessary for playerObject = eval(playerClass)() to work +# _ = [ +# GreedyBot0, +# GreedyBot1, +# GreedyBot2, +# RandomBot, +# ] + + +class LoopController: + """ + Methods consist of MainLoop, MainMenuLoop, LoadPlayerLoop, GameplayLoop, + GameOverLoop, ReplayLoop, LoadReplayLoop. + """ + + def __init__(self, waitBot=True, layout="MIRROR", n_pieces=10) -> None: + # Initialize variables + self.waitBot = waitBot + self.n_pieces = n_pieces + self.layout = layout + + self.loopNum = 0 + self.winnerList = list() + self.replayRecord = list() + self.filePath = "" + self.playerTypes = {} # e.g. {"GreedyBot1": } + self.playerNames = None # e.g. ["Human", "GreedyBot1"] + + # Create a dictionary of player types with PlayerMeta as parent class + for i in PlayerMeta.playerTypes: + # key: class name strings, value: class without () + self.playerTypes[i.__name__] = i + + # List of all possible player objects + self.playerList = [ + GreedyBot0(), + GreedyBot1(), + GreedyBot2(), + ] + + # Block all pygame events + for c_str in pygame.constants.__all__: + try: + c_id = eval(f"pygame.{c_str}") + pygame.event.set_blocked(c_id) + except (ValueError, TypeError): + pass + + # Allow only these events + pygame.event.set_allowed( + [QUIT, MOUSEBUTTONDOWN, MOUSEBUTTONUP, KEYDOWN], + ) + + def mainLoop(self, window: pygame.Surface): + """ + Controls the flow to enter mainMenuLoop (0), loadPlayerLoop (1), + gameplayLoop (2), gameOverLoop (3), replayLoop (4), loadReplayLoop (5). + """ + # print(f"Loop goes on with loopNum {self.loopNum}") + + # First loop to display the main menu + if self.loopNum == 0: + self.filePath = False + self.replayRecord = [] + self.mainMenuLoop(window) + + elif self.loopNum == 1: + # from playButton in mainMenuLoop + # enters loadPlayerLoop to choose player types + self.loadPlayerLoop() + + elif self.loopNum == 2: + # from startButton in loadPlayerLoop + # enters gameplayLoop to play the game + self.winnerList, self.replayRecord = self.gameplayLoop(window) + + elif self.loopNum == 3: + # from gameplayLoop after a player wins + self.gameOverLoop(window, self.winnerList, self.replayRecord) + + elif self.loopNum == 4: + # to view a replay + # pygame.key.set_repeat(100) + self.replayLoop(window, self.filePath) + + elif self.loopNum == 5: + # to select a replay file + self.filePath = self.loadReplayLoop() + + def mainMenuLoop(self, window: pygame.Surface): + """ + Display the main menu and register events from the buttons. + """ + window.fill(WHITE) + titleText = pygame.font.Font(size=int(WIDTH * 0.08)).render( + "Chinese Checkers", + True, + BLACK, + ) + titleTextRect = titleText.get_rect() + titleTextRect.center = (WIDTH * 0.5, HEIGHT * 0.25) + window.blit(titleText, titleTextRect) + playButton = TextButton( + "Play", + centerx=int(WIDTH * 0.5), + centery=int(HEIGHT * 0.375), + width=WIDTH * 0.25, + height=HEIGHT * 0.125, + font_size=32, + ) + loadReplayButton = TextButton( + "Load replay", + centerx=int(WIDTH * 0.5), + centery=int(HEIGHT * 0.625), + width=WIDTH * 0.25, + height=HEIGHT * 0.125, + font_size=32, + ) + while True: + # Register close button event + ev = pygame.event.wait() + if ev.type == QUIT: + pygame.quit() + sys.exit() + + # Control flow to the next state + mouse_pos = pygame.mouse.get_pos() + mouse_left_click = ev.type == MOUSEBUTTONDOWN + if playButton.isClicked(mouse_pos, mouse_left_click): + # Go to the gamePlayLoop + self.loopNum = 1 + break + if loadReplayButton.isClicked(mouse_pos, mouse_left_click): + # Go to the loadReplayLoop + self.loopNum = 5 + break + + playButton.draw(window, mouse_pos) + loadReplayButton.draw(window, mouse_pos) + pygame.display.update() + + def loadPlayerLoop(self): + """ + Display a smaller window to select number of players and player types. + """ + loaded = False + appModifier = 0.75 + appWidth = WIDTH * appModifier + appHeight = HEIGHT * appModifier + + if not QtWidgets.QApplication.instance(): + app = QtWidgets.QApplication(sys.argv) + else: + app = QtWidgets.QApplication.instance() + app.aboutToQuit.connect(self.closing) + + Form = QtWidgets.QWidget() + Form.setWindowTitle("Game Settings") + Form.resize(appWidth, appHeight) + + box = QtWidgets.QWidget(Form) + box.setGeometry( + appWidth * 0.0625, + appHeight * 0.0625, + appWidth * 0.875, + appHeight * 0.625, + ) + grid = QtWidgets.QGridLayout(box) + + # Choose number of players + label_pNum = QtWidgets.QLabel(Form) + label_pNum.setText("No. of Players") + + # Button for 1 player + rButton_1P = QtWidgets.QRadioButton(Form) + rButton_1P.setText("1") + rButton_1P.toggled.connect( + lambda: label_p2Type.setStyleSheet("color: #878787;"), + ) # grey out p2 label + rButton_1P.toggled.connect( + lambda: label_p3Type.setStyleSheet("color: #878787;"), + ) # grey out p3 label + rButton_1P.toggled.connect(lambda: cBox_p2.setDisabled(True)) + rButton_1P.toggled.connect(lambda: cBox_p3.setDisabled(True)) + rButton_1P.toggled.connect(lambda: setItem(self.playerList, 1, None)) + rButton_1P.toggled.connect(lambda: setItem(self.playerList, 2, None)) + + # Button for 2 players + rButton_2P = QtWidgets.QRadioButton(Form) + rButton_2P.setText("2") + rButton_2P.toggled.connect( + lambda: label_p3Type.setStyleSheet("color: #878787;"), + ) # grey out p3 label + rButton_2P.toggled.connect( + lambda: label_p2Type.setStyleSheet("color: #000000;"), + ) # restore p2 label + rButton_2P.toggled.connect(lambda: cBox_p2.setDisabled(False)) + rButton_2P.toggled.connect(lambda: cBox_p3.setDisabled(True)) + rButton_2P.toggled.connect(lambda: setItem(self.playerList, 2, None)) + + # Button for 3 players + rButton_3P = QtWidgets.QRadioButton(Form) + rButton_3P.setText("3") + rButton_3P.setChecked(True) + rButton_3P.toggled.connect( + lambda: label_p2Type.setStyleSheet("color: #000000;"), + ) # restore p3 label + rButton_3P.toggled.connect( + lambda: label_p3Type.setStyleSheet("color: #000000;"), + ) # restore p3 label + rButton_3P.toggled.connect(lambda: cBox_p2.setDisabled(False)) + rButton_3P.toggled.connect(lambda: cBox_p3.setDisabled(False)) + rButton_3P.toggled.connect( + lambda: setItem( + self.playerList, + 2, + self.playerTypes[cBox_p3.currentText()](), + ), + ) + + # Combo boxes for player types + label_p1Type = QtWidgets.QLabel(Form) + label_p1Type.setText("Player 1:") + label_p2Type = QtWidgets.QLabel(Form) + label_p2Type.setText("Player 2:") + label_p3Type = QtWidgets.QLabel(Form) + label_p3Type.setText("Player 3:") + cBox_p1 = QtWidgets.QComboBox(Form) + cBox_p2 = QtWidgets.QComboBox(Form) + cBox_p3 = QtWidgets.QComboBox(Form) + cBoxes = (cBox_p1, cBox_p2, cBox_p3) + + # Set initial player types for the 3 combo boxes + if not loaded: + for i in range(3): + grid.addWidget(cBoxes[i], i + 1, 2, 1, 2) + cBoxes[i].addItems(list(self.playerTypes)) + cBoxes[i].setCurrentIndex(i) + loaded = True + + # Modify playerList when player types are selected + cBox_p1.currentIndexChanged.connect( + lambda: setItem( + self.playerList, + 0, + self.playerTypes[cBox_p1.currentText()](), + ), + ) + cBox_p2.currentIndexChanged.connect( + lambda: setItem( + self.playerList, + 1, + self.playerTypes[cBox_p2.currentText()](), + ), + ) + cBox_p3.currentIndexChanged.connect( + lambda: setItem( + self.playerList, + 2, + self.playerTypes[cBox_p3.currentText()](), + ), + ) + + # Print playerlist for debugging + rButton_1P.toggled.connect(lambda: print(self.playerList)) + rButton_2P.toggled.connect(lambda: print(self.playerList)) + rButton_3P.toggled.connect(lambda: print(self.playerList)) + cBox_p1.currentIndexChanged.connect( + lambda: print(self.playerList), + ) + cBox_p2.currentIndexChanged.connect( + lambda: print(self.playerList), + ) + cBox_p3.currentIndexChanged.connect( + lambda: print(self.playerList), + ) + + # Add widgets to the grid + grid.addWidget(label_pNum, 0, 0) + grid.addWidget(rButton_1P, 0, 1) + grid.addWidget(rButton_2P, 0, 2) + grid.addWidget(rButton_3P, 0, 3) + grid.addWidget(label_p1Type, 1, 0, 1, 2) + grid.addWidget(label_p2Type, 2, 0, 1, 2) + grid.addWidget(label_p3Type, 3, 0, 1, 2) + + startButton = QtWidgets.QPushButton(Form) + startButton.setText("Start Game") + startButton.setGeometry( + appWidth * 0.625, + appHeight * 0.8125, + appWidth * 0.25, + appHeight * 0.125, + ) + startButton.clicked.connect(self.startGame) + + cancelButton = QtWidgets.QPushButton(Form) + cancelButton.setText("Back to Menu") + cancelButton.setGeometry( + appWidth * 0.125, + appHeight * 0.8125, + appWidth * 0.25, + appHeight * 0.125, + ) + cancelButton.clicked.connect(self.backToMenu) + + Form.show() + app.exec() + + # helpers for loadPlayerLoop and replayLoop + def startGame(self): + self.loopNum = 2 # go to gameplay + while None in self.playerList: + self.playerList.remove(None) + self.playerNames = [i.__class__.__name__ for i in self.playerList] + QtWidgets.QApplication.closeAllWindows() + + # helpers for loadPlayerLoop and replayLoop + def backToMenu(self): + self.loopNum = 0 # go to main menu + QtWidgets.QApplication.closeAllWindows() + + def gameplayLoop(self, window: pygame.Surface): + """ + Returns: + [results, replayRecord] + result : len(n_players-1), list of winning player numbers with + the 1st winnder at index 0. -1 if draw. + replayRecord : list of moves in the game. + """ + # Initialize variables + playerIndex = 0 + humanPlayerNum = 0 + result = [] + replayRecord = [] + + # Set player numbers + players: list[Player] = deepcopy(self.playerList) + while None in players: + players.remove(None) + for i in range(len(players)): + players[i].setPlayerNum(i + 1) + # players: list of player objects selected + + # 1st line: no. of players + # 2nd line: player names + # 3rd line: game config + replayRecord.append(str(len(players))) + replayRecord.append(",".join(self.playerNames)) + replayRecord.append(f"{self.layout},{self.n_pieces}") + + # Generate the game + g = Game(players, 1, self.playerNames, self.layout, self.n_pieces) + oneHuman = exactly_one_is_human(players) + if oneHuman: + for player in players: + if isinstance(player, Human): + humanPlayerNum = player.getPlayerNum() + + # Start the game loop + selectedMove = [] # list of start and end coordinates of picked move + path = [] + while True: + currentPlayer = players[playerIndex] + + if self.waitBot: # wait for user to press a key + ev = pygame.event.wait() + else: # bot moves after waiting + duration = 500 # milliseconds + ev = pygame.event.wait(duration) + + # Register close button event + if ev.type == QUIT: + pygame.quit() + sys.exit() + + # Draw the board + window.fill(GRAY) + drawBoard(g, window) + + # Bot Text + botText = pygame.font.Font(size=int(HEIGHT * 0.035)).render( + "Press any key for the bot to make a move", + antialias=True, + color=BLACK, + wraplength=int(WIDTH * 0.2), + ) + botTextRect = botText.get_rect() + botTextRect.topright = (WIDTH, 1) + window.blit(botText, botTextRect) + + # Highlight the 2 coordinates of the move + highlightMove(g, window, selectedMove) + drawPath(g, window, path) + selectedMove = [] + + backButton = TextButton( + "Back to Menu", + width=int(HEIGHT * 0.25), + height=int(HEIGHT * 0.0833), + font_size=int(WIDTH * 0.04), + ) + + mouse_pos = pygame.mouse.get_pos() + mouse_left_click = ev.type == MOUSEBUTTONDOWN + + # Return to main menu if the back button is clicked + if backButton.isClicked(mouse_pos, mouse_left_click): + self.loopNum = 0 + return ([], []) + backButton.draw(window, mouse_pos) + pygame.display.update() + + # Playing player makes a move + if isinstance(currentPlayer, Human): + # Human player makes a move + start_coor, end_coor = currentPlayer.pickMove( + g, + window, + humanPlayerNum, + ) + if (not start_coor) and (not end_coor): + # Return to main menu + self.loopNum = 0 + return ([], []) + else: + # Bot player makes a move + start_coor, end_coor = currentPlayer.pickMove(g) + + path = g.getMovePath(g.playerNum, start_coor, end_coor) + + g.movePiece(start_coor, end_coor) + + if oneHuman: + selectedMove = [ + obj_to_subj_coor(start_coor, humanPlayerNum, self.layout), + obj_to_subj_coor(end_coor, humanPlayerNum, self.layout), + ] + else: + selectedMove = [start_coor, end_coor] + + replayRecord.append(str(start_coor) + "to" + str(end_coor)) + + # Check if the playing player has won + winning = g.checkWin(currentPlayer.getPlayerNum()) + + if winning: # and len(players) == 2: + drawBoard(g, window) + currentPlayer.has_won = True + result.append(currentPlayer.getPlayerNum()) + # replayRecord.append(str(currentPlayer.getPlayerNum())) + + # Go to the game over loop + self.loopNum = 3 + return [result, replayRecord] + + elif winning and len(players) >= 3: + currentPlayer.has_won = True + result.append(currentPlayer.getPlayerNum()) + players.remove(currentPlayer) + + # Switch to the next player + playerIndex = (playerIndex + 1) % len(players) + g.turnCount += 1 + g.playerNum = playerIndex + 1 + + def replayLoop(self, window: pygame.Surface, filePath: str = None): + # Check if a path has been selected + if not filePath: + print("File Path is void!") + self.loopNum = 0 + + # Check validity of replay file + if (not self.replayRecord) and filePath: + isValidReplay = True + move_list = [] + with open(filePath) as f: + # Parse the file + text = f.read() + move_list = text.split("\n") + playerCount = int(move_list.pop(0)) + playerNames = move_list.pop(0).split(",") + self.layout, self.n_pieces = move_list.pop(0).split(",") + self.n_pieces = int(self.n_pieces) + playerList: list[Player] = [] + + # Create player objects + for i, className in enumerate(playerNames): + playerList.append(eval(className)()) + playerList[-1].setPlayerNum(i + 1) + + # Removed the check for total number of players + + # Check each move is valid + # Empty line at the end of the file results in an invalid replay + for i in range(len(move_list)): + move_list[i] = move_list[i].split("to") + + # Check there are 2 sets of coordinates for each move + if len(move_list[i]) != 2: + print(i, move_list[i]) + self.showNotValidReplay() + isValidReplay = False + break + for j in range(len(move_list[i])): + move_list[i][j] = eval(move_list[i][j]) + + # Check coordinates are tuples + if not isinstance(move_list[i][j], tuple): + print(f"Invalid coordinates: {move_list[i][j]}") + self.showNotValidReplay() + isValidReplay = False + break + # Check if the coordinates exists on the board + if move_list[i][j] not in ALL_COOR: + print(f"Invalid coordinates: {move_list[i][j]}") + self.showNotValidReplay() + isValidReplay = False + break + + if isValidReplay: + self.replayRecord = move_list + + # Start the replay if it is valid + if self.replayRecord: + if f: + del f + if text: + del text + + # Initialise game + path = None + g = Game(playerList, 1, playerNames, self.layout, self.n_pieces) + g.playerNum = 0 + g.turnCount = 0 + + # Set up UI buttons + prevButton = TextButton( + "<", + centerx=WIDTH * 0.125, + centery=HEIGHT * 0.5, + width=int(WIDTH / 8), + height=int(HEIGHT / 6), + font_size=int(WIDTH * 0.04), + ) + nextButton = TextButton( + ">", + centerx=WIDTH * 0.875, + centery=HEIGHT * 0.5, + width=int(WIDTH / 8), + height=int(HEIGHT / 6), + font_size=int(WIDTH * 0.04), + ) + backButton = TextButton( + "Back to Menu", + width=int(HEIGHT * 0.25), + height=int(HEIGHT * 0.0833), + font_size=int(WIDTH * 0.04), + ) + autoPlayButton = TextButton( + "Auto Play", + centerx=WIDTH * 0.875, + centery=HEIGHT * 0.875, + width=int(WIDTH / 9), + height=int(HEIGHT / 10), + font_size=int(WIDTH * 0.03), + ) + # BUG: requires double click to actiate autoPlay + hintText = pygame.font.Font(size=int(HEIGHT * 0.03)).render( + "Use the buttons or the left and right arrow keys to navigate through the game", + antialias=True, + color=BLACK, + wraplength=int(WIDTH * 0.375), + ) + hintTextRect = hintText.get_rect() + hintTextRect.topright = (WIDTH, 1) + + def previousMove(): + nonlocal g, moveListIndex, selectedMove, path + g.playerNum = g.turnCount % playerCount + 1 + g.turnCount -= 1 + moveListIndex -= 1 + start_coor = move_list[moveListIndex + 1][1] + end_coor = move_list[moveListIndex + 1][0] + path = g.getMovePath(g.playerNum, start_coor, end_coor) + g.movePiece(start_coor, end_coor) + selectedMove = move_list[moveListIndex] if moveListIndex >= 0 else [] + + def nextMove(): + nonlocal g, moveListIndex, selectedMove, path + g.playerNum = g.turnCount % playerCount + 1 + g.turnCount += 1 + moveListIndex += 1 + start_coor = move_list[moveListIndex][0] + end_coor = move_list[moveListIndex][1] + path = g.getMovePath(g.playerNum, start_coor, end_coor) + g.movePiece(start_coor, end_coor) + selectedMove = move_list[moveListIndex] + + # Initialise replay variables + moveListIndex = -1 + selectedMove = [] + left = False + right = False + mouse_left_click = False + autoPlay = False + autoPlayButton.enabled = True + + # Iterate through all the moves + while True: + # Register mouse left click event + mouse_pos = pygame.mouse.get_pos() + + # Wait for user to press a key + if not autoPlay: + ev = pygame.event.wait() + # Register mouse click and key press events + mouse_left_click = ev.type == MOUSEBUTTONDOWN + left = ( + ev.type == KEYDOWN and ev.key == K_LEFT and prevButton.enabled + ) + right = ( + ev.type == KEYDOWN and ev.key == K_RIGHT and nextButton.enabled + ) + autoPlay = autoPlayButton.isClicked( + mouse_pos, + mouse_left_click, + ) + if autoPlay: + print("[gui.loops] Automatically replaying...") + # Replay automatically after waiting + if autoPlay: + duration = 500 # milliseconds + ev = pygame.event.wait(duration) + mouse_left_click = ev.type == MOUSEBUTTONDOWN + right = True + + # Register close button event + if ev.type == QUIT: + pygame.quit() + sys.exit() + + # Enable/disable buttons at beginning/end of replay + if moveListIndex == -1: + prevButton.enabled = False + else: + prevButton.enabled = True + if moveListIndex == len(move_list) - 1: + # Stop automatic replay + autoPlay = False + nextButton.enabled = False + right = False + print("[gui.loops] End of replay") + else: + nextButton.enabled = True + + # Exit replay mode + if backButton.isClicked(mouse_pos, mouse_left_click): + self.loopNum = 0 + break + # Reverse move + if prevButton.isClicked(mouse_pos, mouse_left_click) or left: + previousMove() + # Move to next move + if nextButton.isClicked(mouse_pos, mouse_left_click) or right: + nextMove() + + # Draw buttons and board + window.fill(GRAY) + window.blit(hintText, hintTextRect) + drawBoard(g, window) + highlightMove(g, window, selectedMove) + drawPath(g, window, path) + prevButton.draw(window, mouse_pos) + nextButton.draw(window, mouse_pos) + backButton.draw(window, mouse_pos) + autoPlayButton.draw(window, mouse_pos) + pygame.display.update() + + def loadReplayLoop(self): + """ + Display a smaller window to select a replay file. + """ + if not QtWidgets.QApplication.instance(): + app = QtWidgets.QApplication(sys.argv) + else: + app = QtWidgets.QApplication.instance() + app.aboutToQuit.connect(self.closing) + + if not os.path.isdir("./replays"): + os.mkdir("./replays") + filePath = QtWidgets.QFileDialog.getOpenFileName( + dir="./replays", + filter="*.txt", + )[0] + if filePath: + self.loopNum = 4 + return filePath + else: # User cancelled + self.loopNum = 0 + return False + + def gameOverLoop( + self, + window: pygame.Surface, + winnerList: list, + replayRecord: list, + ): + # print(winnerList); print(replayRecord) + # winner announcement text + if len(winnerList) == 1: + winnerString = "Player %d wins" % winnerList[0] + elif len(winnerList) == 2: + winnerString = "Player %d wins, then Player %d wins" % ( + winnerList[0], + winnerList[1], + ) + else: + winnerString = "len(winnerList) is %d" % len(winnerList) + font = pygame.font.SysFont("Arial", int(WIDTH * 0.04)) + text = font.render(winnerString, True, BLACK, WHITE) + textRect = text.get_rect() + textRect.center = (int(WIDTH * 0.5), int(HEIGHT / 6)) + window.blit(text, textRect) + + # Create buttons + menuButton = TextButton( + "Back to menu", + centerx=int(WIDTH * 0.25), + centery=int(HEIGHT * 2 / 3), + font_size=32, + ) + exportReplayButton = TextButton( + "Export replay", + centerx=int(WIDTH * 0.75), + centery=int(HEIGHT * 2 / 3), + font_size=32, + ) + + while True: + # Register events + for event in pygame.event.get(): + if event.type == QUIT: + pygame.quit() + sys.exit() + mouse_pos = pygame.mouse.get_pos() + mouse_left_click = pygame.mouse.get_pressed()[0] + + # Return to main menu + if menuButton.isClicked(mouse_pos, mouse_left_click): + self.loopNum = 0 + break + + # Export replay + if exportReplayButton.isClicked(mouse_pos, mouse_left_click): + curTime = strftime("%Y%m%d-%H%M%S") + if not os.path.isdir("./replays"): + os.mkdir("./replays") + with open(f"./replays/replay-{curTime}.txt", mode="w+") as f: + for i in range(len(replayRecord)): + if i < len(replayRecord) - 1: + f.write(str(replayRecord[i]) + "\n") + else: + f.write(str(replayRecord[i])) + exportReplayButton.text = "Replay exported!" + exportReplayButton.enabled = False + + # Draw buttons + menuButton.draw(window, mouse_pos) + exportReplayButton.draw(window, mouse_pos) + pygame.display.update() + + def closing(self): + if self.loopNum == 0 or self.loopNum == 1: + self.backToMenu() + elif self.loopNum == 2: + self.startGame() + + def showNotValidReplay(self): + print("This is not a valid replay!") + self.loopNum = 0 + + +def exactly_one_is_human(players: list[Player]): + """ + Checks through the list of players to see if exactly one of them is human. + """ + only_one = False + for player in players: + if only_one is False and isinstance(player, Human): + only_one = True + elif only_one is True and isinstance(player, Human): + return False + return only_one + + +def trainingLoop(g: Game, players: list[Player], recordReplay: bool = False): + """ + Not sure what this does. Currently not used. + """ + playerIndex = 0 + replayRecord = [] + if recordReplay: + replayRecord.append(str(len(players))) + + # Ensure no humans are playing + for player in players: + assert not isinstance(player, Human), ( + "Can't have humans during training! Human at player %d" + % players.index(player) + + 1 + ) + + for i in range(len(players)): + players[i].setPlayerNum(i + 1) + + # Main training game loop + while True: + currentPlayer = players[playerIndex] + + # Playing player makes a move + start_coor, end_coor = currentPlayer.pickMove(g) + g.movePiece(start_coor, end_coor) + + if recordReplay: + replayRecord.append(str(start_coor) + " " + str(end_coor)) + + winning = g.checkWin(currentPlayer.getPlayerNum()) + + if winning and len(players) == 2: + currentPlayer.has_won = True + print("The winner is Player %d" % currentPlayer.getPlayerNum()) + print(f"{len(replayRecord)} moves") + break # TODO: return stuff? + elif winning and len(players) == 3: + currentPlayer.has_won = True + players.remove(currentPlayer) + print( + "The first winner is Player %d" % currentPlayer.getPlayerNum(), + ) + if playerIndex >= len(players) - 1: + playerIndex = 0 + else: + playerIndex += 1 diff --git a/main.py b/main.py index 59312ac..e1aa502 100644 --- a/main.py +++ b/main.py @@ -1,19 +1,30 @@ -from game_logic.loops import * -from game_logic.game import * -from game_logic.player import * -from game_logic.literals import * import pygame +from gui.loops import LoopController +from gui.constants import WIDTH, HEIGHT pygame.init() window = pygame.display.set_mode((WIDTH, HEIGHT), pygame.SCALED | pygame.SRCALPHA) -pygame.display.set_caption('Chinese Checkers') +pygame.display.set_caption("Chinese Checkers") -lc = LoopController() -while True: - """ - for event in pygame.event.get(): - if event.type == QUIT: - pygame.quit() - sys.exit() """ - lc.mainLoop(window) +def main(): + # Initialize pygame window + pygame.init() + window = pygame.display.set_mode( + (WIDTH, HEIGHT), + pygame.SCALED | pygame.SRCALPHA, + ) + pygame.display.set_caption("Chinese Checkers") + + waitBot = False # True: bot waits for a key press before making a move + layout = "TRIANGLE" # "MIRROR" or "TRIANGLE" + n_pieces = 15 # 10 or 15 + + # Enter game control loop + lc = LoopController(waitBot, layout, n_pieces) + while True: + lc.mainLoop(window) + + +if __name__ == "__main__": + main() diff --git a/replays/2_player_replay.txt b/replays/2_player_replay.txt new file mode 100644 index 0000000..7225770 --- /dev/null +++ b/replays/2_player_replay.txt @@ -0,0 +1,126 @@ +2 +GreedyBot2,GreedyBot1 +TRIANGLE,15 +(3, -5)to(3, -3) +(-5, 1)to(-3, 1) +(3, -7)to(1, -3) +(-4, 2)to(-2, 0) +(2, -5)to(2, -3) +(-2, 0)to(-1, 0) +(4, -7)to(0, -3) +(-6, 2)to(0, 0) +(4, -4)to(2, -2) +(0, 0)to(1, 0) +(3, -3)to(1, 1) +(1, 0)to(2, 0) +(1, -5)to(1, -1) +(2, 0)to(3, 0) +(1, -4)to(1, 2) +(3, 0)to(4, -1) +(3, -4)to(1, 0) +(4, -1)to(5, -2) +(2, -3)to(0, 3) +(5, -2)to(6, -3) +(4, -5)to(0, 1) +(6, -3)to(7, -4) +(2, -2)to(0, 4) +(7, -4)to(8, -4) +(1, 1)to(-1, 5) +(-4, 1)to(0, -1) +(0, -4)to(0, 2) +(-1, 0)to(1, -2) +(2, -4)to(0, 0) +(0, -1)to(2, -3) +(1, -1)to(-1, 3) +(2, -3)to(3, -3) +(1, -3)to(-1, 1) +(3, -3)to(4, -4) +(1, 0)to(-1, 4) +(4, -4)to(5, -4) +(0, 0)to(-2, 2) +(5, -4)to(6, -4) +(-1, 1)to(-3, 3) +(6, -4)to(7, -4) +(0, 2)to(-2, 4) +(1, -2)to(2, -3) +(0, 3)to(-2, 5) +(2, -3)to(3, -3) +(-1, 3)to(-3, 5) +(3, -3)to(4, -4) +(0, 4)to(-2, 6) +(4, -4)to(5, -4) +(-2, 4)to(-4, 6) +(5, -4)to(6, -4) +(-1, 4)to(-3, 6) +(-4, 4)to(-2, 0) +(-2, 2)to(-4, 4) +(-2, 0)to(-1, 0) +(-1, 5)to(-3, 7) +(-6, 3)to(0, -1) +(-2, 5)to(-4, 7) +(-1, 0)to(1, -2) +(-2, 6)to(-4, 8) +(1, -2)to(2, -3) +(4, -8)to(4, -7) +(2, -3)to(3, -3) +(4, -7)to(2, -5) +(3, -3)to(4, -4) +(3, -6)to(-1, -2) +(4, -4)to(5, -4) +(2, -6)to(2, -4) +(0, -1)to(1, -2) +(2, -5)to(0, -1) +(1, -2)to(2, -3) +(2, -4)to(2, -2) +(2, -3)to(3, -3) +(0, -3)to(-2, -1) +(3, -3)to(4, -4) +(-1, -2)to(-3, 4) +(-4, 3)to(-2, 3) +(4, -6)to(3, -5) +(-2, 3)to(-1, 2) +(0, 1)to(-4, 5) +(-1, 2)to(0, 2) +(3, -5)to(2, -4) +(0, 2)to(1, 1) +(2, -4)to(2, -3) +(1, 1)to(2, 0) +(2, -3)to(0, 3) +(2, 0)to(3, 0) +(1, 2)to(-1, 4) +(3, 0)to(4, -1) +(0, 3)to(-2, 5) +(4, -1)to(5, -2) +(-4, 4)to(-2, 6) +(5, -2)to(6, -3) +(2, -2)to(2, -1) +(6, -3)to(7, -3) +(-2, -1)to(-2, 0) +(-3, 1)to(1, -1) +(0, -1)to(0, 0) +(1, -1)to(3, -1) +(2, -1)to(2, 0) +(3, -1)to(4, -1) +(-2, 0)to(-3, 1) +(4, -1)to(5, -2) +(0, 0)to(0, 1) +(5, -2)to(6, -3) +(2, 0)to(2, 1) +(-6, 4)to(-2, 2) +(-3, 1)to(-3, 2) +(-2, 2)to(-1, 2) +(0, 1)to(-2, 3) +(-1, 2)to(0, 2) +(-3, 3)to(-1, 5) +(0, 2)to(1, 1) +(2, 1)to(1, 2) +(1, 1)to(2, 0) +(-3, 2)to(-3, 3) +(2, 0)to(3, 0) +(1, 2)to(0, 3) +(3, 0)to(4, -1) +(-3, 3)to(-4, 4) +(4, -1)to(5, -2) +(-2, 3)to(-2, 4) +(5, -2)to(6, -2) +(0, 3)to(0, 4) \ No newline at end of file diff --git a/replays/3_player_replay.txt b/replays/3_player_replay.txt new file mode 100644 index 0000000..5cffc00 --- /dev/null +++ b/replays/3_player_replay.txt @@ -0,0 +1,177 @@ +3 +GreedyBot0,GreedyBot1,GreedyBot2 +MIRROR,10 +(3, -6)to(1, -4) +(-2, 6)to(-2, 4) +(-2, -4)to(2, -4) +(1, -4)to(0, -3) +(-3, 5)to(-1, 3) +(-4, -4)to(0, -2) +(4, -5)to(4, -4) +(-1, 3)to(-1, 2) +(-3, -3)to(-1, -1) +(1, -5)to(1, -4) +(-1, 2)to(-1, 1) +(-4, -2)to(-2, -2) +(3, -5)to(-1, -3) +(-1, 1)to(-1, 0) +(-3, -2)to(1, -2) +(4, -4)to(3, -3) +(-1, 0)to(0, -1) +(-1, -4)to(-1, 0) +(-1, -3)to(1, -1) +(0, -1)to(2, -3) +(-2, -3)to(2, -1) +(2, -6)to(1, -5) +(2, -3)to(3, -4) +(0, -2)to(2, 0) +(0, -3)to(0, -2) +(3, -4)to(4, -5) +(1, -2)to(3, 0) +(4, -8)to(2, -6) +(-1, 5)to(-1, 3) +(-1, -1)to(-1, 1) +(0, -2)to(0, -1) +(-1, 3)to(-1, 2) +(-1, 0)to(3, -2) +(4, -6)to(4, -2) +(4, -5)to(4, -6) +(-1, 1)to(-1, 3) +(1, -1)to(3, 1) +(4, -6)to(4, -8) +(2, -1)to(4, 1) +(0, -1)to(0, 0) +(-1, 2)to(-1, 1) +(2, 0)to(4, 2) +(1, -5)to(1, -3) +(-1, 1)to(-1, 0) +(3, 0)to(3, 2) +(3, -3)to(3, -1) +(-1, 0)to(0, -1) +(3, -2)to(3, 0) +(4, -7)to(4, -6) +(0, -1)to(0, -2) +(4, 1)to(4, 3) +(1, -4)to(-1, -2) +(0, -2)to(0, -3) +(-2, -2)to(0, -2) +(4, -2)to(2, 2) +(0, -3)to(0, -4) +(4, 2)to(4, 4) +(3, -1)to(2, 0) +(0, -4)to(1, -5) +(-3, -4)to(-3, -3) +(3, 1)to(1, 3) +(-2, 5)to(4, -1) +(-4, -3)to(-2, -3) +(1, -3)to(1, -2) +(4, -1)to(4, -2) +(-3, -3)to(-1, 1) +(2, 0)to(2, 1) +(4, -2)to(4, -3) +(0, -2)to(2, -2) +(4, -6)to(4, -5) +(4, -3)to(4, -4) +(3, 0)to(1, 4) +(-1, -2)to(-2, -1) +(4, -4)to(4, -6) +(-4, -1)to(-3, -1) +(-2, -1)to(-2, 0) +(4, -6)to(4, -7) +(-2, -3)to(-2, -2) +(3, -7)to(3, -6) +(1, -5)to(3, -7) +(-3, -1)to(-2, -1) +(1, 3)to(0, 4) +(-2, 4)to(-2, 3) +(-2, -1)to(0, 1) +(1, -2)to(1, -1) +(-2, 3)to(-2, 2) +(-1, 1)to(3, 3) +(0, 4)to(-1, 5) +(-2, 2)to(-1, 1) +(3, 2)to(3, 4) +(2, -6)to(4, -4) +(-1, 1)to(-1, 0) +(-2, -2)to(-1, -2) +(1, -1)to(1, 3) +(-1, 0)to(0, -1) +(-1, -2)to(-1, -1) +(4, -5)to(4, -3) +(0, -1)to(0, -2) +(-1, -1)to(-1, 0) +(1, 3)to(0, 4) +(0, -2)to(1, -3) +(-1, 0)to(-1, 4) +(-1, 5)to(-2, 6) +(1, -3)to(1, -5) +(-1, 3)to(3, 1) +(2, -5)to(2, -3) +(1, -5)to(2, -6) +(2, -4)to(3, -4) +(-2, 0)to(-3, 1) +(-4, 5)to(-3, 4) +(3, -4)to(3, -2) +(-3, 1)to(-3, 2) +(-3, 4)to(-2, 3) +(2, -2)to(4, -2) +(-3, 2)to(-4, 3) +(-2, 3)to(-2, 2) +(0, 1)to(1, 1) +(3, -6)to(3, -5) +(-2, 2)to(-1, 1) +(3, -2)to(3, -1) +(0, 0)to(0, 1) +(-1, 1)to(-1, 0) +(4, -2)to(4, -1) +(0, 1)to(0, 2) +(-1, 0)to(0, -1) +(1, 1)to(1, 3) +(2, -3)to(1, -2) +(0, -1)to(0, -2) +(3, -1)to(3, 0) +(2, 1)to(1, 2) +(0, -2)to(1, -3) +(4, -1)to(2, 3) +(3, -5)to(2, -4) +(1, -3)to(1, -4) +(3, 0)to(3, 2) +(1, -2)to(0, -1) +(1, -4)to(2, -5) +(3, 1)to(4, 1) +(0, 4)to(-1, 5) +(2, -5)to(3, -6) +(-1, 4)to(0, 4) +(4, -4)to(3, -3) +(-3, 7)to(-3, 5) +(0, 4)to(2, 4) +(-4, 3)to(-4, 4) +(-4, 7)to(-4, 3) +(4, 1)to(4, 2) +(4, -3)to(3, -2) +(-4, 3)to(-3, 2) +(2, 4)to(0, 4) +(-4, 4)to(-4, 5) +(-3, 2)to(-2, 1) +(0, 4)to(2, 4) +(2, 2)to(0, 4) +(-2, 1)to(-1, 0) +(3, 2)to(2, 2) +(1, 2)to(0, 3) +(-1, 0)to(1, -2) +(2, 2)to(3, 2) +(2, -4)to(1, -3) +(1, -2)to(2, -3) +(1, 3)to(-1, 3) +(0, 2)to(-2, 4) +(2, -3)to(3, -4) +(-1, 3)to(1, 3) +(1, -3)to(1, -2) +(3, -4)to(4, -5) +(3, 2)to(4, 1) +(0, -1)to(-1, 0) +(4, -5)to(4, -6) +(3, 3)to(3, 2) +(-2, 6)to(-3, 7) +(-3, 5)to(-1, 3) +(1, 3)to(3, 3) \ No newline at end of file diff --git a/replays/replay-20231029-213732.txt b/replays/replay-20231029-213732.txt deleted file mode 100644 index 971f908..0000000 --- a/replays/replay-20231029-213732.txt +++ /dev/null @@ -1,188 +0,0 @@ -3 -(4, -4)to(3, -3) -(-5, 1)to(-3, 1) -(2, 3)to(0, 3) -(4, -6)to(2, -2) -(-7, 3)to(-5, 1) -(4, 3)to(2, 1) -(2, -2)to(1, -1) -(-5, 2)to(-3, 2) -(1, 4)to(1, 2) -(4, -8)to(0, 0) -(-7, 4)to(-3, 0) -(3, 4)to(1, 4) -(0, 0)to(0, 1) -(-5, 3)to(-3, 3) -(0, 4)to(2, -2) -(0, -4)to(0, -3) -(-4, 0)to(-2, 0) -(2, 4)to(0, 0) -(2, -6)to(0, -2) -(-3, 0)to(3, -2) -(4, 2)to(0, 2) -(2, -5)to(0, -1) -(-5, 4)to(1, -2) -(1, 2)to(-3, 0) -(4, -5)to(2, -1) -(-3, 1)to(1, -3) -(3, 2)to(-1, 0) -(4, -7)to(2, -3) -(-5, 1)to(1, 1) -(1, 4)to(1, 0) -(1, -1)to(-1, 1) -(-3, 3)to(3, -1) -(2, 2)to(2, 0) -(3, -3)to(-3, 3) -(-4, 1)to(-2, -1) -(1, 3)to(-1, 3) -(-3, 3)to(-3, 4) -(-4, 2)to(-2, 2) -(0, 3)to(-2, 1) -(3, -5)to(-3, 5) -(-3, 2)to(3, 0) -(1, 0)to(-3, 2) -(-3, 5)to(-3, 6) -(-4, 3)to(0, 3) -(4, 4)to(3, 4) -(3, -7)to(-3, 7) -(1, -3)to(3, -3) -(3, 4)to(3, 2) -(-3, 7)to(-4, 7) -(-8, 4)to(-7, 3) -(3, 3)to(2, 3) -(3, -6)to(3, -5) -(-7, 3)to(-5, 1) -(2, 3)to(2, 2) -(3, -5)to(-3, 7) -(-6, 2)to(-4, 0) -(3, 2)to(1, 0) -(-3, 7)to(-2, 6) -(-5, 1)to(1, -1) -(2, 2)to(1, 2) -(1, -5)to(-1, 5) -(-6, 3)to(-5, 3) -(1, 2)to(-1, 2) -(3, -4)to(3, -5) -(-6, 4)to(-4, 2) -(-2, 1)to(-4, -1) -(3, -5)to(-3, 7) -(-5, 3)to(1, -3) -(-1, 3)to(-2, 3) -(-3, 7)to(-4, 8) -(-4, 2)to(-3, 1) -(-2, 3)to(-2, 1) -(-1, 5)to(-2, 5) -(-3, 1)to(-1, -1) -(-1, 0)to(-1, -2) -(0, 1)to(-4, 5) -(-4, 4)to(-3, 3) -(2, 1)to(0, 1) -(-1, 1)to(-1, 3) -(-3, 3)to(-1, 1) -(4, 1)to(2, 1) -(0, -1)to(-1, 0) -(-2, -1)to(0, -1) -(-2, 1)to(-2, -1) -(-1, 0)to(-2, 1) -(-2, 0)to(-1, 0) -(0, 0)to(-2, -2) -(0, -2)to(-2, 0) -(-2, 2)to(0, 0) -(2, -2)to(0, -4) -(2, -4)to(0, -2) -(0, 0)to(4, -4) -(2, 0)to(0, 0) -(-2, 0)to(-2, 2) -(1, -2)to(5, -4) -(4, 0)to(2, -2) -(-2, 2)to(-2, 3) -(-4, 0)to(-2, 0) -(0, 2)to(-2, 2) -(-1, 3)to(-1, 5) -(-1, 0)to(1, -2) -(-1, 2)to(-1, 0) -(-2, 1)to(-3, 1) -(4, -4)to(6, -4) -(0, 1)to(-2, 1) -(-3, 1)to(-3, 7) -(5, -4)to(7, -4) -(2, 1)to(0, 1) -(2, -1)to(2, 0) -(0, -1)to(4, -1) -(0, 1)to(0, -1) -(2, 0)to(0, 4) -(3, 0)to(5, -2) -(-2, -1)to(-2, -3) -(2, -3)to(0, 1) -(3, -1)to(5, -1) -(-2, 1)to(-2, -1) -(0, -3)to(2, -1) -(1, -1)to(3, -1) -(0, -1)to(0, -3) -(0, -2)to(0, -1) -(-2, 0)to(0, -2) -(0, 0)to(-4, -2) -(-2, 3)to(-2, 4) -(0, -2)to(6, -2) -(-2, 2)to(-4, 0) -(0, -1)to(-2, 1) -(4, -1)to(6, -3) -(2, -2)to(-2, 0) -(-2, 1)to(-2, 2) -(5, -1)to(7, -3) -(-1, -2)to(-3, -2) -(-2, 2)to(-3, 3) -(6, -2)to(8, -4) -(-1, 0)to(-1, -2) -(0, 1)to(-1, 2) -(-1, -1)to(0, -1) -(-2, -2)to(-2, -4) -(2, -1)to(-2, 3) -(0, -1)to(4, -3) -(-2, 0)to(-2, -2) -(-2, 3)to(-1, 3) -(0, 3)to(4, -1) -(-4, -1)to(-4, -3) -(-1, 2)to(-1, 4) -(3, -1)to(5, -1) -(-2, -1)to(-4, -1) -(-1, 3)to(-3, 5) -(3, -2)to(5, -4) -(-3, -2)to(-3, -4) -(1, -4)to(2, -4) -(1, -2)to(3, -2) -(-1, -2)to(-3, -2) -(2, -4)to(0, -2) -(3, -3)to(5, -3) -(-4, -2)to(-4, -4) -(0, -2)to(0, -1) -(-1, 1)to(0, 1) -(-4, 0)to(-4, -2) -(0, -1)to(0, 0) -(0, 1)to(2, -1) -(1, 0)to(-1, 0) -(0, 0)to(-1, 1) -(1, 1)to(2, 1) -(3, 1)to(1, 1) -(-1, 1)to(-2, 2) -(1, -3)to(2, -3) -(1, 1)to(0, 1) -(-2, 2)to(-4, 6) -(2, 1)to(3, 0) -(0, 1)to(0, 0) -(-3, 3)to(-4, 4) -(0, 0)to(-4, 0) -(2, -3)to(3, -3) -(-3, 2)to(-3, 1) -(2, -1)to(3, -1) -(-3, 1)to(-3, -3) -(3, 0)to(4, 0) -(-1, 0)to(-1, -1) -(4, 0)to(6, -2) -(-1, -1)to(-1, -2) -(3, -3)to(4, -4) -(-1, -2)to(-1, -4) -(3, -2)to(4, -2) -(-3, 0)to(-3, -1) -(4, -1)to(4, 0) -(0, -3)to(-1, -3) \ No newline at end of file diff --git a/replays/replay-20231029-214014.txt b/replays/replay-20231029-214014.txt deleted file mode 100644 index e197fc7..0000000 --- a/replays/replay-20231029-214014.txt +++ /dev/null @@ -1,120 +0,0 @@ -2 -(2, -5)to(2, -3) -(-5, 1)to(-3, 1) -(4, -7)to(0, -3) -(-7, 3)to(-5, 1) -(3, -5)to(3, -3) -(-5, 2)to(-3, 2) -(3, -7)to(1, -3) -(-7, 4)to(-3, 0) -(1, -4)to(-1, -2) -(-5, 3)to(-3, 3) -(1, -5)to(-1, -1) -(-4, 1)to(0, -1) -(2, -4)to(0, 0) -(-4, 2)to(2, -2) -(4, -4)to(-2, 2) -(-4, 4)to(4, -4) -(0, -4)to(-4, 4) -(-4, 0)to(0, -2) -(0, -3)to(-2, 3) -(-6, 2)to(-2, 0) -(3, -4)to(-1, 0) -(-5, 4)to(-1, 2) -(3, -3)to(-1, 3) -(-3, 3)to(3, -3) -(2, -3)to(-2, 1) -(-3, 1)to(1, -1) -(4, -5)to(2, -1) -(0, -1)to(4, -3) -(-1, -1)to(-3, 3) -(-6, 4)to(-4, 0) -(1, -3)to(-3, 1) -(1, -1)to(3, -1) -(-1, -2)to(3, 0) -(2, -2)to(4, 0) -(2, -1)to(2, 1) -(3, -1)to(5, -1) -(3, 0)to(1, 2) -(3, -3)to(5, -3) -(2, 1)to(0, 3) -(4, -3)to(6, -3) -(1, 2)to(-1, 4) -(4, 0)to(6, -4) -(-2, 2)to(0, 4) -(-2, 0)to(0, 2) -(0, 0)to(-2, 4) -(-4, 0)to(0, 0) -(-1, 3)to(-3, 5) -(-1, 2)to(1, 2) -(-2, 3)to(-4, 5) -(-4, 3)to(-2, 3) -(-3, 3)to(-1, 5) -(5, -3)to(7, -3) -(-3, 1)to(-3, 3) -(-3, 2)to(-1, 2) -(-1, 0)to(-3, 6) -(-2, 3)to(0, 1) -(0, 3)to(-4, 7) -(-1, 2)to(1, 0) -(-2, 4)to(-4, 8) -(0, 0)to(2, 0) -(0, 4)to(-2, 6) -(0, 1)to(2, -1) -(-4, 4)to(-4, 6) -(1, 0)to(3, -2) -(-3, 5)to(-3, 7) -(2, 0)to(4, -2) -(4, -8)to(4, -7) -(3, -2)to(7, -4) -(4, -7)to(2, -5) -(6, -4)to(8, -4) -(2, -6)to(2, -4) -(-8, 4)to(-7, 3) -(3, -6)to(1, -4) -(-7, 3)to(-5, 3) -(2, -5)to(0, -1) -(-6, 3)to(-2, 3) -(4, -6)to(3, -5) -(-5, 3)to(-4, 3) -(3, -5)to(3, -1) -(2, -1)to(4, -3) -(1, -4)to(3, 0) -(4, -2)to(6, -4) -(2, -4)to(2, -3) -(1, 2)to(5, -4) -(2, -3)to(2, -2) -(4, -4)to(6, -2) -(3, 0)to(-3, 4) -(-5, 1)to(-4, 1) -(-3, 3)to(-3, 5) -(-4, 1)to(-2, -1) -(2, -2)to(2, -1) -(-3, 0)to(1, -2) -(2, -1)to(2, 0) -(0, -2)to(2, -2) -(3, -1)to(-3, 3) -(1, -2)to(3, -2) -(0, -1)to(0, 0) -(2, -2)to(4, -4) -(2, 0)to(2, 1) -(-4, 3)to(-3, 2) -(0, 0)to(0, 1) -(-3, 2)to(-1, 0) -(0, 1)to(-2, 5) -(-2, -1)to(-1, -2) -(2, 1)to(1, 2) -(-2, 3)to(-1, 2) -(-2, 1)to(-3, 2) -(-1, 0)to(0, 0) -(1, 2)to(0, 3) -(-1, 2)to(1, 2) -(0, 3)to(0, 4) -(-1, -2)to(0, -2) -(-3, 3)to(-4, 4) -(0, 0)to(1, -1) -(-3, 2)to(-3, 3) -(0, -2)to(1, -2) -(-4, 4)to(-2, 4) -(0, 2)to(1, 1) -(-3, 3)to(-4, 4) \ No newline at end of file