From 00ef79aa177eb11a25d95c2839e84870d9b199dc Mon Sep 17 00:00:00 2001 From: Jon Bergland <144770553+JonBergland@users.noreply.github.com> Date: Tue, 19 Mar 2024 17:39:29 +0100 Subject: [PATCH 01/19] Revert "3 make tetris board" --- .devcontainer/devcontainer.json | 41 ++++++++ docs/guide/{devContainer.md => env.md} | 0 docs/guide/venv.md | 25 ----- docs/planning/12.03.2024.md | 14 --- src/game/board.py | 132 ------------------------- 5 files changed, 41 insertions(+), 171 deletions(-) create mode 100644 .devcontainer/devcontainer.json rename docs/guide/{devContainer.md => env.md} (100%) delete mode 100644 docs/guide/venv.md delete mode 100644 docs/planning/12.03.2024.md delete mode 100644 src/game/board.py diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..9af928d --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,41 @@ +// For format details, see https://aka.ms/devcontainer.json. For config options, see the +// README at: https://github.com/devcontainers/templates/tree/main/src/python +{ + "name": "Python 3", + // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile + "image": "mcr.microsoft.com/devcontainers/python:1-3.12-bullseye", + "settings": { + "terminal.integrated.shell.linux": "/bin/bash" + }, + // Add the IDs of extensions you want installed when the container is created. + "customizations": { + "vscode": { + "extensions": [ + "streetsidesoftware.code-spell-checker", + "ms-azuretools.vscode-docker", + "DavidAnson.vscode-markdownlint", + "esbenp.prettier-vscode", + "ms-python.python", + "ms-python.vscode-pylance", + "ms-python.debugpy" + ] + } + }, + // install the pip packages on container creation + "postCreateCommand": "pip3 install --user -r requirements.txt", + + // Features to add to the dev container. More info: https://containers.dev/features. + // "features": {}, + + // Use 'forwardPorts' to make a list of ports inside the container available locally. + // "forwardPorts": [], + + // Use 'postCreateCommand' to run commands after the container is created. + // "postCreateCommand": "pip3 install --user -r requirements.txt", + + // Configure tool-specific properties. + // "customizations": {}, + + // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. + // "remoteUser": "root" +} diff --git a/docs/guide/devContainer.md b/docs/guide/env.md similarity index 100% rename from docs/guide/devContainer.md rename to docs/guide/env.md diff --git a/docs/guide/venv.md b/docs/guide/venv.md deleted file mode 100644 index 3689853..0000000 --- a/docs/guide/venv.md +++ /dev/null @@ -1,25 +0,0 @@ -# Development Environment - -We will be utilizing a virtual environment to develop the project. This will allow us to have a consistent development environment across all developers.In this case we will be using `venv` to create the virtual environment. - -## Getting Started - -* Have [Python](https://www.python.org/downloads/) installed - * Verify that python is installed by running `python --version` -* Pip install the virtual environment package - * Verify that pip is installed by running `pip --version` - * Install the virtual environment package by running `pip install virtualenv` -* In the root of the project, create a virtual environment by running `python -m venv .venv` -* Activate the virtual environment - * On Windows, run `.venv\Scripts\activate` - * On Mac/Linux, run `source .venv/bin/activate` -* Done! You should now have a fully functional development environment -* To deactivate the virtual environment, run `deactivate` - -## Dependencies -Once you have entered venv you need to make sure the dependencies are installed by running `pip install -r requirements.txt`. -If you use a new pip dependency, make sure to add it to the `requirements.txt` file. This can be done by running: -```bash -pip freeze > requirements.txt -``` -after you pip installed it locally, and then committing the changes. diff --git a/docs/planning/12.03.2024.md b/docs/planning/12.03.2024.md deleted file mode 100644 index 6884f3a..0000000 --- a/docs/planning/12.03.2024.md +++ /dev/null @@ -1,14 +0,0 @@ -# Cogito work night nr. 3 - -## Agenda - -- Venv -- [Mer klarere plan for Tetris](#tetris) -- Progge -- Hva skjer neste uke? - - -## Tetris -- **Headless:** ingen grafikk ting skjer sekvensielt. En viss boardstate og en ny block -> velger presist hvor den skal plasseres (blant mulige plasser) uten tidsbegrensning. - For development purpuses har vi en to print outs av brettet. Første viser brettet med den uplasserte blokken (øverst i midten), andre viser brettet etter at blokken er plassert. Bruker de objektene som vi har blitt enige om. Tanken er at vi kan bruke dette til å teste ut forskjellige algoritmer for å plassere blokken (ai/algorytme velger posisjonen). -- **Grafisk:** pygame. Adapsjon av samme objekter som headless bare at vi nå styrer hvor blokken skal plasseres dynamisk. Blokken faller nedover med en viss hastighet og vi kan flytte den rundt med en viss hastighet (feks. et tastetrykk = forflytter blokken en rute). For å la agenten spille må vi lage et oversettelses lag mellom headless og grafisk, hvor vi kan sende input til grafisk for å manuvrere blokken til samme posisjon som headless ville plassert den. \ No newline at end of file diff --git a/src/game/board.py b/src/game/board.py deleted file mode 100644 index bde7c32..0000000 --- a/src/game/board.py +++ /dev/null @@ -1,132 +0,0 @@ -import pygame - -''' -Denne skriver ut brettet i terminal bare. - -Vi lager brettet med en matrise. Det er O overalt i matrisen. legger du en brikke så nendrer vi i matrisen. -Dersom en hel rad i matrisen er full, så fjerner vi denne raden og flytter alt sammen nedover + oppdatererr poengsummen" -En brikke i matrisen represneteres med x-er. -''' - -class Board: - rows = 20 - columns = 10 - gameOver = False - rowsRemoved = 0 - - def __init__(self): - self.matrix = [["O" for _ in range(self.columns)] for _ in range(self.rows)] - - def spawnNewBlock(self): - block = Block() - self.placeBlock(Block()) - if block.isValidMove(): - self.placeBlock(block) - - #vi oppretter en nyt blokk - #vi får tilbakemelding om hvilke blokk denne vil være og hvilke koordinater den skal ha. - #Dersom det er plass så plasserer vi den. - - def print_matrix(self): - for row in self.matrix: - print(' '.join(row)) - - def placeBlock(self, block): - if block.isValidMove(): - #block.position = [(x,y),(x,y)(x,y),(x,y)] - - for gridPositions in block.getPosition(): #må ha noe bedre logikk for å fjerne gamle posisjoner - for x, y in gridPositions - self.matrix[x][y] = "O" - - for gridPositions in block.getPosition(): - for x, y in gridPositions: - self.matrix[x][y] = "X" - - def rotateBlockRight(self, block): - if block.rotateRight().isValidMove(): - self.plaser_brikke(block.rotateRight()) - - def moveBlockDown(self, block): - if block.moveDown().isValidMove(): - self.plaser_brikke(block.moveDown()) - - def moveBlockLeft(self, block): - if block.moveLeft().isValidMove(): - self.plaser_brikke(block.moveLeft()) - - def moveBlockRight(self, block): - if block.moveRight().isValidMove(): - self.plaser_brikke(block.moveRight()) - - def rotateBlockLeft(self, block): - if block.rotateLeft().isValidMove(): - self.plaser_brikke(block.roterLeft()) - - def gameOver(self): - return self.gameOver - - def isvalidMove(self, block): - for gridPositions in block.position: - for x, y in gridPositions: - if self.matrix[x][y] != "O": - return False - return True - - def clearRow(self, rownumber): #tar vare på alt under randen som skal fjernes og legger alt over den som skal fjernes over + lager ny tom rad - newMatrix = self.matrix[0, rownumber] + self.matrix[rownumber+1, self.rows] + ["O" for _ in range(self.columns)] - self.Matrix = newMatrix - rowsRemoved += 1 - - def checkGameState(self): #itterer over alle rader og sjekker om de er fulle. fjerner alle som er fulle og teller hvor mange på rad som ble fjernet - amount = 0 - for row in self.matrix: - if "O" not in row: - self.clearRow(row) - amount += 1 - return amount - - - -#for å teste greiene - -# def plaser_brikke(): -# def plaser_brikke(self): -# # Choose a random position on the board to place the brick. -# row = random.randint(0, self.rows - 1) -# col = random.randint(0, self.columns - 1) - -# # Place the brick on the board. -# self.board[row][col] = 1 - -# # Print the updated board. -# self.print_board()'' - - -# # Create an instance of the Board class. - -my_board = Board() -my_board.print_matrix() - - - - - - - - - - - - - - - - - - - - - - - From 4e510d73eec63f2987e155f0c12ee0ae5adc4536 Mon Sep 17 00:00:00 2001 From: Maia Austigard Date: Tue, 16 Apr 2024 19:59:33 +0200 Subject: [PATCH 02/19] feat: start implementing geneticAlgAgent Co-authored-by: Jon Bergland --- src/agents/agent.py | 8 ++++-- src/agents/geneticAlgAgentJon.py | 46 ++++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 2 deletions(-) create mode 100644 src/agents/geneticAlgAgentJon.py diff --git a/src/agents/agent.py b/src/agents/agent.py index 951a5f4..cc5838b 100644 --- a/src/agents/agent.py +++ b/src/agents/agent.py @@ -34,7 +34,7 @@ def result(board: Tetris) -> Union[Action, list[Action]]: pass -def play_game(agent: Agent, board: Tetris, actions_per_drop: int = 1) -> Tetris: +def play_game(agent: Agent, board: Tetris, max_count: int, actions_per_drop: int = 1) -> Tetris: """ Plays a game of Tetris with the given agent. @@ -46,7 +46,9 @@ def play_game(agent: Agent, board: Tetris, actions_per_drop: int = 1) -> Tetris: Returns: The final state of the board after the game is over. """ - while not board.isGameOver(): + count = 0 + + while not board.isGameOver() or count < max_count: # Get the result of the agent's action for _ in range(actions_per_drop): result = agent.result(board) @@ -56,6 +58,8 @@ def play_game(agent: Agent, board: Tetris, actions_per_drop: int = 1) -> Tetris: board.doAction(action) else: board.doAction(result) + + count += 1 # Advance the game by one frame board.doAction(Action.SOFT_DROP) #board.printBoard() diff --git a/src/agents/geneticAlgAgentJon.py b/src/agents/geneticAlgAgentJon.py new file mode 100644 index 0000000..46e1ca4 --- /dev/null +++ b/src/agents/geneticAlgAgentJon.py @@ -0,0 +1,46 @@ +import random +from src.game.tetris import * +from src.agents.agent_factory import create_agent +from src.agents.agent import Agent, play_game +# From paper: https://codemyroad.wordpress.com/2013/04/14/tetris-ai-the-near-perfect-player/ +# the weigts the author got: +# a x (Aggregate Height) + b x (Complete Lines) + c x (Holes) + d x (Bumpiness) +# a = -0.510066 b = 0.760666 c = -0.35663 d = -0.184483 +# TODO Read the part of the article about the genetic algorithm +# TODO Create a fitness function + +# TODO Create a genetic algorithm based on the + +# List over vectors with boards attached: +# [(List over parameters, board), ...] + +# TODO create init-method that creates agents with random vectors +# TODO create run_games-method that goes through the agents, and play 100 games each, return average lines cleared +# TODO create method for fetching a random 10%, and finds the two with highest lines cleared, and makes a child (with 5% chance of mutation) +# TODO create method that makes 30% new agents from existing agents (last method), replace worst 30% with the new agents + +list = [] + +for _ in range(0, 100): + agg_height = random.random(-1, 0) + max_height = random.random(-1, 0) + lines_cleared = random.random(0, 1) + bumpiness = random.random(-1, 0) + holes = random.random(-1, 0) + + game = Tetris() + agent: Agent = create_agent("heuristic") + total_cleared = 0 + for _ in range(0, 100): + board = play_game(agent, game, 5) + total_cleared += board.rowsRemoved + list.append = ([agg_height, max_height, lines_cleared, bumpiness, holes], total_cleared/100) + + + +def fitness_crossover(pop1: tuple(list[int], int), pop2: tuple(list[int], int)) -> tuple(list[int], int): + return_pop: tuple(list[int], int) + # Combines the two vectors proportionaly by how many lines they cleared + child_pop = pop1[1] * pop1[0] + pop2[1] * pop2[0] + + return tuple(child_pop, 0) \ No newline at end of file From 52bb3739df7b2ae42200dfbb4429513bdf2d0a29 Mon Sep 17 00:00:00 2001 From: Maia Austigard Date: Mon, 22 Apr 2024 20:05:17 +0200 Subject: [PATCH 03/19] feat: continue working on genetic algorithm agent Co-authored-by: Jon Bergland --- main.py | 27 ++-- src/agents/geneticAlgAgent.py | 3 +- src/agents/geneticAlgAgentJon.py | 130 +++++++++++++++--- src/agents/heuristic.py | 4 +- src/agents/heuristic_with_parameters_agent.py | 44 ++++++ 5 files changed, 176 insertions(+), 32 deletions(-) create mode 100644 src/agents/heuristic_with_parameters_agent.py diff --git a/main.py b/main.py index 2c5e19a..b5a5ec5 100644 --- a/main.py +++ b/main.py @@ -6,17 +6,24 @@ utility ) from src.agents.heuristic_trainer import train +from src.agents.geneticAlgAgentJon import GeneticAlgAgentJM if __name__ == "__main__": - game = Tetris() - agent: Agent = create_agent("heuristic") - sum_rows_removed = 0 - for i in range(10): - end_board = play_game(agent, game, 7) - end_board.printBoard() - sum_rows_removed += end_board.rowsRemoved + algAgent = GeneticAlgAgentJM() + algAgent.number_of_selection(1) + print(algAgent.getBestPop) - print(f"Average rows removed: {sum_rows_removed / 10}") + + + # game = Tetris() + # agent: Agent = create_agent("heuristic") + # sum_rows_removed = 0 + # for i in range(10): + # end_board = play_game(agent, game, 7) + # end_board.printBoard() + # sum_rows_removed += end_board.rowsRemoved + + # print(f"Average rows removed: {sum_rows_removed / 10}") # possible_moves = game.getPossibleBoards() # for boards in possible_moves: @@ -28,4 +35,6 @@ # manager.startGame() - #train() + # train() + + diff --git a/src/agents/geneticAlgAgent.py b/src/agents/geneticAlgAgent.py index 83f61a8..2511b93 100644 --- a/src/agents/geneticAlgAgent.py +++ b/src/agents/geneticAlgAgent.py @@ -4,4 +4,5 @@ # a = -0.510066 b = 0.760666 c = -0.35663 d = -0.184483 # TODO Read the part of the article about the genetic algorithm # TODO Create a fitness function -# TODO Create a genetic algorithm based on the \ No newline at end of file +# TODO Create a genetic algorithm based on the + diff --git a/src/agents/geneticAlgAgentJon.py b/src/agents/geneticAlgAgentJon.py index 46e1ca4..d2b2326 100644 --- a/src/agents/geneticAlgAgentJon.py +++ b/src/agents/geneticAlgAgentJon.py @@ -1,7 +1,8 @@ import random from src.game.tetris import * from src.agents.agent_factory import create_agent -from src.agents.agent import Agent, play_game +from src.agents.agent import Agent +from src.agents.heuristic_with_parameters_agent import * # From paper: https://codemyroad.wordpress.com/2013/04/14/tetris-ai-the-near-perfect-player/ # the weigts the author got: # a x (Aggregate Height) + b x (Complete Lines) + c x (Holes) + d x (Bumpiness) @@ -19,28 +20,117 @@ # TODO create method for fetching a random 10%, and finds the two with highest lines cleared, and makes a child (with 5% chance of mutation) # TODO create method that makes 30% new agents from existing agents (last method), replace worst 30% with the new agents -list = [] +class GeneticAlgAgentJM: + agents: list[list[list[float], float]] = [] -for _ in range(0, 100): - agg_height = random.random(-1, 0) - max_height = random.random(-1, 0) - lines_cleared = random.random(0, 1) - bumpiness = random.random(-1, 0) - holes = random.random(-1, 0) + def number_of_selection(self, number_of_selections: int): + self.agents = self.initAgents() + for i in range(0, number_of_selections): + # Select new pops + self.agents = self.replace_30_percent(self.agents) + + # Run new test + for i in range(len(self.agents)): + param_list = self.agents[i][0] + average_cleared = self.play_game(param_list[0], param_list[1], param_list[2], param_list[3], param_list[4]) + self.agents[i][1] = average_cleared + + + + def initAgents(self) -> list[list[list[float], float]]: + number_of_agents = 10 + for _ in range(0, number_of_agents): + agg_height = random.randrange(-1000, 0)/1000 + max_height = random.randrange(-1000, 0)/1000 + lines_cleared = random.randrange(0, 1000)/1000 + bumpiness = random.randrange(-1000, 0)/1000 + holes = random.randrange(-1000, 0)/1000 + + # agents = [] + average_cleared = self.play_game(agg_height, max_height, lines_cleared, bumpiness, holes) / number_of_agents + self.agents.append = ([agg_height, max_height, lines_cleared, bumpiness, holes], average_cleared) + print(_) + + # return agents - game = Tetris() - agent: Agent = create_agent("heuristic") - total_cleared = 0 - for _ in range(0, 100): - board = play_game(agent, game, 5) - total_cleared += board.rowsRemoved - list.append = ([agg_height, max_height, lines_cleared, bumpiness, holes], total_cleared/100) + + def play_game(self, agg_height, max_height, lines_cleared, bumpiness, holes): + + board = Tetris() + agent: Agent = HeuristicWithParametersAgent([agg_height, max_height, lines_cleared, bumpiness, holes]) + total_cleared = 0 + number_of_rounds = 10 + for _ in range(0, number_of_rounds): + + max_moves = number_of_rounds + move = 0 + actions_per_drop = 7 + + while not board.isGameOver() and move < max_moves: + # Get the result of the agent's action + for _ in range(actions_per_drop): + result = agent.result(board) + # Perform the action(s) on the board + if isinstance(result, list): + for action in result: + board.doAction(action) + else: + board.doAction(result) + + move += 1 + # Advance the game by one frame + board.doAction(Action.SOFT_DROP) + #board.printBoard() + + total_cleared += board.rowsRemoved + + return total_cleared + + def fitness_crossover(self, pop1: list[list[float], float], pop2: list[list[float], float]) -> list[list[float], float]: + # Combines the two vectors proportionaly by how many lines they cleared + child_pop = pop1[1] * pop1[0] + pop2[1] * pop2[0] + + return list(child_pop, 0) + + # TODO create method for fetching a random 10%, and finds the two with highest lines cleared, and makes a child (with 5% chance of mutation) + def paring_pop(self, pop_list: list[list[list[float], float]]) -> list[list[float], float]: + # Gets the number of pops to select + num_pops_to_select: int(len(pop_list) * 0.1) + + # Get a sample of pops based on the previous number + random_pop_sample: random.sample(pop_list, num_pops_to_select) + + # Gets the two pops with the highest lines cleared + highest_values: sorted(random_pop_sample, key=lambda x: x[1], reverse=True)[:2] + + # Gets the child pop of the two pops + new_pop = self.fitness_crossover(highest_values[0], highest_values[1]) + + # Mutate 5% of children pops + if random.random(0,1) < 0.2: + random_parameter = int(random.randint(0,4)) + new_pop[0][random_parameter] = (random.randrange(-200, 200)/1000) * new_pop[0][random_parameter] + + return new_pop -def fitness_crossover(pop1: tuple(list[int], int), pop2: tuple(list[int], int)) -> tuple(list[int], int): - return_pop: tuple(list[int], int) - # Combines the two vectors proportionaly by how many lines they cleared - child_pop = pop1[1] * pop1[0] + pop2[1] * pop2[0] + def replace_30_percent(self, pop_list: list[list[list[float], float]]) -> list[list[float], float]: + new_list: list[list[list[float], float]] + + # Number of pops needed for 30% of total number + num_pops_needed: int(len(pop_list) * 0.3) + + for _ in range(0, num_pops_needed): + new_list.append(self.paring_pop(pop_list)) + + pop_list: sorted(pop_list, key=lambda x: x[1], reverse=False)[:num_pops_needed] + + pop_list.append(new_list) + + return pop_list + - return tuple(child_pop, 0) \ No newline at end of file + def getBestPop(self) -> list[list[float], float]: + pop_list: sorted(pop_list, key, key=lambda x: x[1], reverse=False) + return pop_list[0] diff --git a/src/agents/heuristic.py b/src/agents/heuristic.py index 26eea48..992a3e8 100644 --- a/src/agents/heuristic.py +++ b/src/agents/heuristic.py @@ -3,8 +3,8 @@ from src.game.tetris import Tetris -def utility(gameState: Tetris, aggregate_heights_weight: int, max_height_weight: int, - lines_cleared_weight: int, bumpiness_weight: int, holes_weight: int) -> int: +def utility(gameState: Tetris, aggregate_heights_weight: float, max_height_weight: float, + lines_cleared_weight: float, bumpiness_weight: float, holes_weight: float) -> int: """Returns the utility of the given game state.""" sum = 0 diff --git a/src/agents/heuristic_with_parameters_agent.py b/src/agents/heuristic_with_parameters_agent.py new file mode 100644 index 0000000..0a2e8cd --- /dev/null +++ b/src/agents/heuristic_with_parameters_agent.py @@ -0,0 +1,44 @@ +from src.agents.agent import Agent +from src.game.tetris import Action, Tetris, transition_model, get_all_actions +from src.agents.heuristic import ( + utility +) + +class HeuristicWithParametersAgent(Agent): + + aggregate_heights_weight: float + max_height_weight: float + lines_cleared_weight: float + bumpiness_weight: float + holes_weight: float + + def __init__(self, params: list[float]): + self.aggregate_heights_weight = params[0] + self.max_height_weight = params[1] + self.lines_cleared_weight = params[2] + self.bumpiness_weight = params[3] + self.holes_weight = params[4] + + def result(self, board: Tetris) -> list[Action]: + # Get all possible boards + possible_boards = board.getPossibleBoards() + + best_board: Tetris + best_utility = float("-inf") + # Check which board has the best outcome based on the heuristic + for boards in possible_boards: + current_utility = utility(boards, self.aggregate_heights_weight, self.max_height_weight, + self.lines_cleared_weight, self.bumpiness_weight, self.holes_weight) + + if current_utility > best_utility: + best_board = boards + best_utility = current_utility + + + # Find the actions needed to transform the current board to the new board + actions = [] + try: + actions = transition_model(board, best_board) + return actions + except: + return actions From 6d3846aa1e4b70aaeee1750dd74449b70438e5e2 Mon Sep 17 00:00:00 2001 From: Maia Austigard Date: Tue, 23 Apr 2024 17:20:21 +0200 Subject: [PATCH 04/19] fix: optimize code --- main.py | 9 ++--- src/agents/agent.py | 8 ++-- src/agents/geneticAlgAgentJon.py | 67 +++++++++++++++++--------------- 3 files changed, 44 insertions(+), 40 deletions(-) diff --git a/main.py b/main.py index b5a5ec5..75e2897 100644 --- a/main.py +++ b/main.py @@ -9,11 +9,6 @@ from src.agents.geneticAlgAgentJon import GeneticAlgAgentJM if __name__ == "__main__": - algAgent = GeneticAlgAgentJM() - algAgent.number_of_selection(1) - print(algAgent.getBestPop) - - # game = Tetris() # agent: Agent = create_agent("heuristic") @@ -37,4 +32,8 @@ # train() + + algAgent = GeneticAlgAgentJM() + algAgent.number_of_selection(1) + print(algAgent.getBestPop()) diff --git a/src/agents/agent.py b/src/agents/agent.py index cc5838b..d46cd55 100644 --- a/src/agents/agent.py +++ b/src/agents/agent.py @@ -34,7 +34,7 @@ def result(board: Tetris) -> Union[Action, list[Action]]: pass -def play_game(agent: Agent, board: Tetris, max_count: int, actions_per_drop: int = 1) -> Tetris: +def play_game(agent: Agent, board: Tetris, actions_per_drop: int = 1) -> Tetris: """ Plays a game of Tetris with the given agent. @@ -46,9 +46,9 @@ def play_game(agent: Agent, board: Tetris, max_count: int, actions_per_drop: int Returns: The final state of the board after the game is over. """ - count = 0 + #count = 0 - while not board.isGameOver() or count < max_count: + while not board.isGameOver(): # Get the result of the agent's action for _ in range(actions_per_drop): result = agent.result(board) @@ -59,7 +59,7 @@ def play_game(agent: Agent, board: Tetris, max_count: int, actions_per_drop: int else: board.doAction(result) - count += 1 + #count += 1 # Advance the game by one frame board.doAction(Action.SOFT_DROP) #board.printBoard() diff --git a/src/agents/geneticAlgAgentJon.py b/src/agents/geneticAlgAgentJon.py index d2b2326..9171c3a 100644 --- a/src/agents/geneticAlgAgentJon.py +++ b/src/agents/geneticAlgAgentJon.py @@ -24,9 +24,10 @@ class GeneticAlgAgentJM: agents: list[list[list[float], float]] = [] def number_of_selection(self, number_of_selections: int): - self.agents = self.initAgents() + self.initAgents() for i in range(0, number_of_selections): # Select new pops + print(len(self.agents)) self.agents = self.replace_30_percent(self.agents) # Run new test @@ -36,9 +37,8 @@ def number_of_selection(self, number_of_selections: int): self.agents[i][1] = average_cleared - def initAgents(self) -> list[list[list[float], float]]: - number_of_agents = 10 + number_of_agents = 20 for _ in range(0, number_of_agents): agg_height = random.randrange(-1000, 0)/1000 max_height = random.randrange(-1000, 0)/1000 @@ -48,7 +48,7 @@ def initAgents(self) -> list[list[list[float], float]]: # agents = [] average_cleared = self.play_game(agg_height, max_height, lines_cleared, bumpiness, holes) / number_of_agents - self.agents.append = ([agg_height, max_height, lines_cleared, bumpiness, holes], average_cleared) + self.agents.append([[agg_height, max_height, lines_cleared, bumpiness, holes], average_cleared]) print(_) # return agents @@ -87,50 +87,55 @@ def play_game(self, agg_height, max_height, lines_cleared, bumpiness, holes): return total_cleared - def fitness_crossover(self, pop1: list[list[float], float], pop2: list[list[float], float]) -> list[list[float], float]: - # Combines the two vectors proportionaly by how many lines they cleared - child_pop = pop1[1] * pop1[0] + pop2[1] * pop2[0] - - return list(child_pop, 0) + def replace_30_percent(self, pop_list: list[list[list[float], float]]) -> list[list[float], float]: + new_list = []#: list[list[list[float], float]] + - # TODO create method for fetching a random 10%, and finds the two with highest lines cleared, and makes a child (with 5% chance of mutation) + # Number of pops needed for 30% of total number + num_pops_needed = int(len(pop_list) * 0.3) + + for _ in range(0, num_pops_needed): + new_list.append(self.paring_pop(pop_list)) # liste.append(liste[liste, float]) = liste[liste[liste, float]] + + pop_list: sorted(pop_list, key=lambda x: x[1], reverse=False)[:num_pops_needed] + + pop_list.extend(new_list) + + return pop_list + + + # TODO create method for fetching a random 10%, and finds the two with highest lines cleared, and makes a child (with 5% chance of mutation) def paring_pop(self, pop_list: list[list[list[float], float]]) -> list[list[float], float]: # Gets the number of pops to select - num_pops_to_select: int(len(pop_list) * 0.1) + num_pops_to_select = int(len(pop_list) * 0.1) # Get a sample of pops based on the previous number - random_pop_sample: random.sample(pop_list, num_pops_to_select) + random_pop_sample = random.sample(pop_list, num_pops_to_select) # Gets the two pops with the highest lines cleared - highest_values: sorted(random_pop_sample, key=lambda x: x[1], reverse=True)[:2] + highest_values = sorted(random_pop_sample, key=lambda x: x[1], reverse=True)[:2] # Gets the child pop of the two pops - new_pop = self.fitness_crossover(highest_values[0], highest_values[1]) + new_pop = self.fitness_crossover(highest_values[0], highest_values[1]) # liste[liste, float] # Mutate 5% of children pops - if random.random(0,1) < 0.2: + if random.randrange(0,1000)/1000 < 0.2: random_parameter = int(random.randint(0,4)) new_pop[0][random_parameter] = (random.randrange(-200, 200)/1000) * new_pop[0][random_parameter] - return new_pop - - - def replace_30_percent(self, pop_list: list[list[list[float], float]]) -> list[list[float], float]: - new_list: list[list[list[float], float]] - - # Number of pops needed for 30% of total number - num_pops_needed: int(len(pop_list) * 0.3) - - for _ in range(0, num_pops_needed): - new_list.append(self.paring_pop(pop_list)) - - pop_list: sorted(pop_list, key=lambda x: x[1], reverse=False)[:num_pops_needed] + return new_pop # liste[liste, float] - pop_list.append(new_list) - return pop_list + def fitness_crossover(self, pop1: list[list[float], float], pop2: list[list[float], float]) -> list[list[float], float]: + # Combines the two vectors proportionaly by how many lines they cleared + parent_pop1 = [h * pop1[1] for h in pop1[0]] + parent_pop2 = [h * pop2[1] for h in pop2[0]] + child_pop = [h1 + h2 for h1, h2 in zip(parent_pop1, parent_pop2)] + return [child_pop, 0.0] # liste[liste, float] + def getBestPop(self) -> list[list[float], float]: - pop_list: sorted(pop_list, key, key=lambda x: x[1], reverse=False) + pop_list = self.agents + pop_list = sorted(pop_list, key=lambda x: x[1], reverse=False) return pop_list[0] From 06fed5edbafdbc8225c6fa03e4ae2696e89fdaff Mon Sep 17 00:00:00 2001 From: Jon Bergland <144770553+JonBergland@users.noreply.github.com> Date: Tue, 19 Mar 2024 17:39:29 +0100 Subject: [PATCH 05/19] Revert "3 make tetris board" --- .devcontainer/devcontainer.json | 41 ++++++++++++++++++++++++++ docs/guide/{devContainer.md => env.md} | 0 docs/guide/venv.md | 25 ---------------- docs/planning/12.03.2024.md | 14 --------- 4 files changed, 41 insertions(+), 39 deletions(-) create mode 100644 .devcontainer/devcontainer.json rename docs/guide/{devContainer.md => env.md} (100%) delete mode 100644 docs/guide/venv.md delete mode 100644 docs/planning/12.03.2024.md diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..9af928d --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,41 @@ +// For format details, see https://aka.ms/devcontainer.json. For config options, see the +// README at: https://github.com/devcontainers/templates/tree/main/src/python +{ + "name": "Python 3", + // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile + "image": "mcr.microsoft.com/devcontainers/python:1-3.12-bullseye", + "settings": { + "terminal.integrated.shell.linux": "/bin/bash" + }, + // Add the IDs of extensions you want installed when the container is created. + "customizations": { + "vscode": { + "extensions": [ + "streetsidesoftware.code-spell-checker", + "ms-azuretools.vscode-docker", + "DavidAnson.vscode-markdownlint", + "esbenp.prettier-vscode", + "ms-python.python", + "ms-python.vscode-pylance", + "ms-python.debugpy" + ] + } + }, + // install the pip packages on container creation + "postCreateCommand": "pip3 install --user -r requirements.txt", + + // Features to add to the dev container. More info: https://containers.dev/features. + // "features": {}, + + // Use 'forwardPorts' to make a list of ports inside the container available locally. + // "forwardPorts": [], + + // Use 'postCreateCommand' to run commands after the container is created. + // "postCreateCommand": "pip3 install --user -r requirements.txt", + + // Configure tool-specific properties. + // "customizations": {}, + + // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. + // "remoteUser": "root" +} diff --git a/docs/guide/devContainer.md b/docs/guide/env.md similarity index 100% rename from docs/guide/devContainer.md rename to docs/guide/env.md diff --git a/docs/guide/venv.md b/docs/guide/venv.md deleted file mode 100644 index 3689853..0000000 --- a/docs/guide/venv.md +++ /dev/null @@ -1,25 +0,0 @@ -# Development Environment - -We will be utilizing a virtual environment to develop the project. This will allow us to have a consistent development environment across all developers.In this case we will be using `venv` to create the virtual environment. - -## Getting Started - -* Have [Python](https://www.python.org/downloads/) installed - * Verify that python is installed by running `python --version` -* Pip install the virtual environment package - * Verify that pip is installed by running `pip --version` - * Install the virtual environment package by running `pip install virtualenv` -* In the root of the project, create a virtual environment by running `python -m venv .venv` -* Activate the virtual environment - * On Windows, run `.venv\Scripts\activate` - * On Mac/Linux, run `source .venv/bin/activate` -* Done! You should now have a fully functional development environment -* To deactivate the virtual environment, run `deactivate` - -## Dependencies -Once you have entered venv you need to make sure the dependencies are installed by running `pip install -r requirements.txt`. -If you use a new pip dependency, make sure to add it to the `requirements.txt` file. This can be done by running: -```bash -pip freeze > requirements.txt -``` -after you pip installed it locally, and then committing the changes. diff --git a/docs/planning/12.03.2024.md b/docs/planning/12.03.2024.md deleted file mode 100644 index 6884f3a..0000000 --- a/docs/planning/12.03.2024.md +++ /dev/null @@ -1,14 +0,0 @@ -# Cogito work night nr. 3 - -## Agenda - -- Venv -- [Mer klarere plan for Tetris](#tetris) -- Progge -- Hva skjer neste uke? - - -## Tetris -- **Headless:** ingen grafikk ting skjer sekvensielt. En viss boardstate og en ny block -> velger presist hvor den skal plasseres (blant mulige plasser) uten tidsbegrensning. - For development purpuses har vi en to print outs av brettet. Første viser brettet med den uplasserte blokken (øverst i midten), andre viser brettet etter at blokken er plassert. Bruker de objektene som vi har blitt enige om. Tanken er at vi kan bruke dette til å teste ut forskjellige algoritmer for å plassere blokken (ai/algorytme velger posisjonen). -- **Grafisk:** pygame. Adapsjon av samme objekter som headless bare at vi nå styrer hvor blokken skal plasseres dynamisk. Blokken faller nedover med en viss hastighet og vi kan flytte den rundt med en viss hastighet (feks. et tastetrykk = forflytter blokken en rute). For å la agenten spille må vi lage et oversettelses lag mellom headless og grafisk, hvor vi kan sende input til grafisk for å manuvrere blokken til samme posisjon som headless ville plassert den. \ No newline at end of file From 45202d98e5ff82c7d818b02eec0fb360a646da3b Mon Sep 17 00:00:00 2001 From: Maia Austigard Date: Tue, 16 Apr 2024 19:59:33 +0200 Subject: [PATCH 06/19] feat: start implementing geneticAlgAgent Co-authored-by: Jon Bergland --- src/agents/agent.py | 8 ++++-- src/agents/geneticAlgAgentJon.py | 46 ++++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 2 deletions(-) create mode 100644 src/agents/geneticAlgAgentJon.py diff --git a/src/agents/agent.py b/src/agents/agent.py index 951a5f4..cc5838b 100644 --- a/src/agents/agent.py +++ b/src/agents/agent.py @@ -34,7 +34,7 @@ def result(board: Tetris) -> Union[Action, list[Action]]: pass -def play_game(agent: Agent, board: Tetris, actions_per_drop: int = 1) -> Tetris: +def play_game(agent: Agent, board: Tetris, max_count: int, actions_per_drop: int = 1) -> Tetris: """ Plays a game of Tetris with the given agent. @@ -46,7 +46,9 @@ def play_game(agent: Agent, board: Tetris, actions_per_drop: int = 1) -> Tetris: Returns: The final state of the board after the game is over. """ - while not board.isGameOver(): + count = 0 + + while not board.isGameOver() or count < max_count: # Get the result of the agent's action for _ in range(actions_per_drop): result = agent.result(board) @@ -56,6 +58,8 @@ def play_game(agent: Agent, board: Tetris, actions_per_drop: int = 1) -> Tetris: board.doAction(action) else: board.doAction(result) + + count += 1 # Advance the game by one frame board.doAction(Action.SOFT_DROP) #board.printBoard() diff --git a/src/agents/geneticAlgAgentJon.py b/src/agents/geneticAlgAgentJon.py new file mode 100644 index 0000000..46e1ca4 --- /dev/null +++ b/src/agents/geneticAlgAgentJon.py @@ -0,0 +1,46 @@ +import random +from src.game.tetris import * +from src.agents.agent_factory import create_agent +from src.agents.agent import Agent, play_game +# From paper: https://codemyroad.wordpress.com/2013/04/14/tetris-ai-the-near-perfect-player/ +# the weigts the author got: +# a x (Aggregate Height) + b x (Complete Lines) + c x (Holes) + d x (Bumpiness) +# a = -0.510066 b = 0.760666 c = -0.35663 d = -0.184483 +# TODO Read the part of the article about the genetic algorithm +# TODO Create a fitness function + +# TODO Create a genetic algorithm based on the + +# List over vectors with boards attached: +# [(List over parameters, board), ...] + +# TODO create init-method that creates agents with random vectors +# TODO create run_games-method that goes through the agents, and play 100 games each, return average lines cleared +# TODO create method for fetching a random 10%, and finds the two with highest lines cleared, and makes a child (with 5% chance of mutation) +# TODO create method that makes 30% new agents from existing agents (last method), replace worst 30% with the new agents + +list = [] + +for _ in range(0, 100): + agg_height = random.random(-1, 0) + max_height = random.random(-1, 0) + lines_cleared = random.random(0, 1) + bumpiness = random.random(-1, 0) + holes = random.random(-1, 0) + + game = Tetris() + agent: Agent = create_agent("heuristic") + total_cleared = 0 + for _ in range(0, 100): + board = play_game(agent, game, 5) + total_cleared += board.rowsRemoved + list.append = ([agg_height, max_height, lines_cleared, bumpiness, holes], total_cleared/100) + + + +def fitness_crossover(pop1: tuple(list[int], int), pop2: tuple(list[int], int)) -> tuple(list[int], int): + return_pop: tuple(list[int], int) + # Combines the two vectors proportionaly by how many lines they cleared + child_pop = pop1[1] * pop1[0] + pop2[1] * pop2[0] + + return tuple(child_pop, 0) \ No newline at end of file From 1916547e8fe96bd2e1db1749cad612897a75d882 Mon Sep 17 00:00:00 2001 From: Maia Austigard Date: Mon, 22 Apr 2024 20:05:17 +0200 Subject: [PATCH 07/19] feat: continue working on genetic algorithm agent Co-authored-by: Jon Bergland --- main.py | 27 ++-- src/agents/geneticAlgAgent.py | 3 +- src/agents/geneticAlgAgentJon.py | 130 +++++++++++++++--- src/agents/heuristic.py | 4 +- src/agents/heuristic_with_parameters_agent.py | 44 ++++++ 5 files changed, 176 insertions(+), 32 deletions(-) create mode 100644 src/agents/heuristic_with_parameters_agent.py diff --git a/main.py b/main.py index 2c5e19a..b5a5ec5 100644 --- a/main.py +++ b/main.py @@ -6,17 +6,24 @@ utility ) from src.agents.heuristic_trainer import train +from src.agents.geneticAlgAgentJon import GeneticAlgAgentJM if __name__ == "__main__": - game = Tetris() - agent: Agent = create_agent("heuristic") - sum_rows_removed = 0 - for i in range(10): - end_board = play_game(agent, game, 7) - end_board.printBoard() - sum_rows_removed += end_board.rowsRemoved + algAgent = GeneticAlgAgentJM() + algAgent.number_of_selection(1) + print(algAgent.getBestPop) - print(f"Average rows removed: {sum_rows_removed / 10}") + + + # game = Tetris() + # agent: Agent = create_agent("heuristic") + # sum_rows_removed = 0 + # for i in range(10): + # end_board = play_game(agent, game, 7) + # end_board.printBoard() + # sum_rows_removed += end_board.rowsRemoved + + # print(f"Average rows removed: {sum_rows_removed / 10}") # possible_moves = game.getPossibleBoards() # for boards in possible_moves: @@ -28,4 +35,6 @@ # manager.startGame() - #train() + # train() + + diff --git a/src/agents/geneticAlgAgent.py b/src/agents/geneticAlgAgent.py index 83f61a8..2511b93 100644 --- a/src/agents/geneticAlgAgent.py +++ b/src/agents/geneticAlgAgent.py @@ -4,4 +4,5 @@ # a = -0.510066 b = 0.760666 c = -0.35663 d = -0.184483 # TODO Read the part of the article about the genetic algorithm # TODO Create a fitness function -# TODO Create a genetic algorithm based on the \ No newline at end of file +# TODO Create a genetic algorithm based on the + diff --git a/src/agents/geneticAlgAgentJon.py b/src/agents/geneticAlgAgentJon.py index 46e1ca4..d2b2326 100644 --- a/src/agents/geneticAlgAgentJon.py +++ b/src/agents/geneticAlgAgentJon.py @@ -1,7 +1,8 @@ import random from src.game.tetris import * from src.agents.agent_factory import create_agent -from src.agents.agent import Agent, play_game +from src.agents.agent import Agent +from src.agents.heuristic_with_parameters_agent import * # From paper: https://codemyroad.wordpress.com/2013/04/14/tetris-ai-the-near-perfect-player/ # the weigts the author got: # a x (Aggregate Height) + b x (Complete Lines) + c x (Holes) + d x (Bumpiness) @@ -19,28 +20,117 @@ # TODO create method for fetching a random 10%, and finds the two with highest lines cleared, and makes a child (with 5% chance of mutation) # TODO create method that makes 30% new agents from existing agents (last method), replace worst 30% with the new agents -list = [] +class GeneticAlgAgentJM: + agents: list[list[list[float], float]] = [] -for _ in range(0, 100): - agg_height = random.random(-1, 0) - max_height = random.random(-1, 0) - lines_cleared = random.random(0, 1) - bumpiness = random.random(-1, 0) - holes = random.random(-1, 0) + def number_of_selection(self, number_of_selections: int): + self.agents = self.initAgents() + for i in range(0, number_of_selections): + # Select new pops + self.agents = self.replace_30_percent(self.agents) + + # Run new test + for i in range(len(self.agents)): + param_list = self.agents[i][0] + average_cleared = self.play_game(param_list[0], param_list[1], param_list[2], param_list[3], param_list[4]) + self.agents[i][1] = average_cleared + + + + def initAgents(self) -> list[list[list[float], float]]: + number_of_agents = 10 + for _ in range(0, number_of_agents): + agg_height = random.randrange(-1000, 0)/1000 + max_height = random.randrange(-1000, 0)/1000 + lines_cleared = random.randrange(0, 1000)/1000 + bumpiness = random.randrange(-1000, 0)/1000 + holes = random.randrange(-1000, 0)/1000 + + # agents = [] + average_cleared = self.play_game(agg_height, max_height, lines_cleared, bumpiness, holes) / number_of_agents + self.agents.append = ([agg_height, max_height, lines_cleared, bumpiness, holes], average_cleared) + print(_) + + # return agents - game = Tetris() - agent: Agent = create_agent("heuristic") - total_cleared = 0 - for _ in range(0, 100): - board = play_game(agent, game, 5) - total_cleared += board.rowsRemoved - list.append = ([agg_height, max_height, lines_cleared, bumpiness, holes], total_cleared/100) + + def play_game(self, agg_height, max_height, lines_cleared, bumpiness, holes): + + board = Tetris() + agent: Agent = HeuristicWithParametersAgent([agg_height, max_height, lines_cleared, bumpiness, holes]) + total_cleared = 0 + number_of_rounds = 10 + for _ in range(0, number_of_rounds): + + max_moves = number_of_rounds + move = 0 + actions_per_drop = 7 + + while not board.isGameOver() and move < max_moves: + # Get the result of the agent's action + for _ in range(actions_per_drop): + result = agent.result(board) + # Perform the action(s) on the board + if isinstance(result, list): + for action in result: + board.doAction(action) + else: + board.doAction(result) + + move += 1 + # Advance the game by one frame + board.doAction(Action.SOFT_DROP) + #board.printBoard() + + total_cleared += board.rowsRemoved + + return total_cleared + + def fitness_crossover(self, pop1: list[list[float], float], pop2: list[list[float], float]) -> list[list[float], float]: + # Combines the two vectors proportionaly by how many lines they cleared + child_pop = pop1[1] * pop1[0] + pop2[1] * pop2[0] + + return list(child_pop, 0) + + # TODO create method for fetching a random 10%, and finds the two with highest lines cleared, and makes a child (with 5% chance of mutation) + def paring_pop(self, pop_list: list[list[list[float], float]]) -> list[list[float], float]: + # Gets the number of pops to select + num_pops_to_select: int(len(pop_list) * 0.1) + + # Get a sample of pops based on the previous number + random_pop_sample: random.sample(pop_list, num_pops_to_select) + + # Gets the two pops with the highest lines cleared + highest_values: sorted(random_pop_sample, key=lambda x: x[1], reverse=True)[:2] + + # Gets the child pop of the two pops + new_pop = self.fitness_crossover(highest_values[0], highest_values[1]) + + # Mutate 5% of children pops + if random.random(0,1) < 0.2: + random_parameter = int(random.randint(0,4)) + new_pop[0][random_parameter] = (random.randrange(-200, 200)/1000) * new_pop[0][random_parameter] + + return new_pop -def fitness_crossover(pop1: tuple(list[int], int), pop2: tuple(list[int], int)) -> tuple(list[int], int): - return_pop: tuple(list[int], int) - # Combines the two vectors proportionaly by how many lines they cleared - child_pop = pop1[1] * pop1[0] + pop2[1] * pop2[0] + def replace_30_percent(self, pop_list: list[list[list[float], float]]) -> list[list[float], float]: + new_list: list[list[list[float], float]] + + # Number of pops needed for 30% of total number + num_pops_needed: int(len(pop_list) * 0.3) + + for _ in range(0, num_pops_needed): + new_list.append(self.paring_pop(pop_list)) + + pop_list: sorted(pop_list, key=lambda x: x[1], reverse=False)[:num_pops_needed] + + pop_list.append(new_list) + + return pop_list + - return tuple(child_pop, 0) \ No newline at end of file + def getBestPop(self) -> list[list[float], float]: + pop_list: sorted(pop_list, key, key=lambda x: x[1], reverse=False) + return pop_list[0] diff --git a/src/agents/heuristic.py b/src/agents/heuristic.py index 26eea48..992a3e8 100644 --- a/src/agents/heuristic.py +++ b/src/agents/heuristic.py @@ -3,8 +3,8 @@ from src.game.tetris import Tetris -def utility(gameState: Tetris, aggregate_heights_weight: int, max_height_weight: int, - lines_cleared_weight: int, bumpiness_weight: int, holes_weight: int) -> int: +def utility(gameState: Tetris, aggregate_heights_weight: float, max_height_weight: float, + lines_cleared_weight: float, bumpiness_weight: float, holes_weight: float) -> int: """Returns the utility of the given game state.""" sum = 0 diff --git a/src/agents/heuristic_with_parameters_agent.py b/src/agents/heuristic_with_parameters_agent.py new file mode 100644 index 0000000..0a2e8cd --- /dev/null +++ b/src/agents/heuristic_with_parameters_agent.py @@ -0,0 +1,44 @@ +from src.agents.agent import Agent +from src.game.tetris import Action, Tetris, transition_model, get_all_actions +from src.agents.heuristic import ( + utility +) + +class HeuristicWithParametersAgent(Agent): + + aggregate_heights_weight: float + max_height_weight: float + lines_cleared_weight: float + bumpiness_weight: float + holes_weight: float + + def __init__(self, params: list[float]): + self.aggregate_heights_weight = params[0] + self.max_height_weight = params[1] + self.lines_cleared_weight = params[2] + self.bumpiness_weight = params[3] + self.holes_weight = params[4] + + def result(self, board: Tetris) -> list[Action]: + # Get all possible boards + possible_boards = board.getPossibleBoards() + + best_board: Tetris + best_utility = float("-inf") + # Check which board has the best outcome based on the heuristic + for boards in possible_boards: + current_utility = utility(boards, self.aggregate_heights_weight, self.max_height_weight, + self.lines_cleared_weight, self.bumpiness_weight, self.holes_weight) + + if current_utility > best_utility: + best_board = boards + best_utility = current_utility + + + # Find the actions needed to transform the current board to the new board + actions = [] + try: + actions = transition_model(board, best_board) + return actions + except: + return actions From 1719762b7b1bc7f03f030215d1560441eee496e8 Mon Sep 17 00:00:00 2001 From: Maia Austigard Date: Tue, 23 Apr 2024 17:20:21 +0200 Subject: [PATCH 08/19] fix: optimize code --- main.py | 9 ++--- src/agents/agent.py | 8 ++-- src/agents/geneticAlgAgentJon.py | 67 +++++++++++++++++--------------- 3 files changed, 44 insertions(+), 40 deletions(-) diff --git a/main.py b/main.py index b5a5ec5..75e2897 100644 --- a/main.py +++ b/main.py @@ -9,11 +9,6 @@ from src.agents.geneticAlgAgentJon import GeneticAlgAgentJM if __name__ == "__main__": - algAgent = GeneticAlgAgentJM() - algAgent.number_of_selection(1) - print(algAgent.getBestPop) - - # game = Tetris() # agent: Agent = create_agent("heuristic") @@ -37,4 +32,8 @@ # train() + + algAgent = GeneticAlgAgentJM() + algAgent.number_of_selection(1) + print(algAgent.getBestPop()) diff --git a/src/agents/agent.py b/src/agents/agent.py index cc5838b..d46cd55 100644 --- a/src/agents/agent.py +++ b/src/agents/agent.py @@ -34,7 +34,7 @@ def result(board: Tetris) -> Union[Action, list[Action]]: pass -def play_game(agent: Agent, board: Tetris, max_count: int, actions_per_drop: int = 1) -> Tetris: +def play_game(agent: Agent, board: Tetris, actions_per_drop: int = 1) -> Tetris: """ Plays a game of Tetris with the given agent. @@ -46,9 +46,9 @@ def play_game(agent: Agent, board: Tetris, max_count: int, actions_per_drop: int Returns: The final state of the board after the game is over. """ - count = 0 + #count = 0 - while not board.isGameOver() or count < max_count: + while not board.isGameOver(): # Get the result of the agent's action for _ in range(actions_per_drop): result = agent.result(board) @@ -59,7 +59,7 @@ def play_game(agent: Agent, board: Tetris, max_count: int, actions_per_drop: int else: board.doAction(result) - count += 1 + #count += 1 # Advance the game by one frame board.doAction(Action.SOFT_DROP) #board.printBoard() diff --git a/src/agents/geneticAlgAgentJon.py b/src/agents/geneticAlgAgentJon.py index d2b2326..9171c3a 100644 --- a/src/agents/geneticAlgAgentJon.py +++ b/src/agents/geneticAlgAgentJon.py @@ -24,9 +24,10 @@ class GeneticAlgAgentJM: agents: list[list[list[float], float]] = [] def number_of_selection(self, number_of_selections: int): - self.agents = self.initAgents() + self.initAgents() for i in range(0, number_of_selections): # Select new pops + print(len(self.agents)) self.agents = self.replace_30_percent(self.agents) # Run new test @@ -36,9 +37,8 @@ def number_of_selection(self, number_of_selections: int): self.agents[i][1] = average_cleared - def initAgents(self) -> list[list[list[float], float]]: - number_of_agents = 10 + number_of_agents = 20 for _ in range(0, number_of_agents): agg_height = random.randrange(-1000, 0)/1000 max_height = random.randrange(-1000, 0)/1000 @@ -48,7 +48,7 @@ def initAgents(self) -> list[list[list[float], float]]: # agents = [] average_cleared = self.play_game(agg_height, max_height, lines_cleared, bumpiness, holes) / number_of_agents - self.agents.append = ([agg_height, max_height, lines_cleared, bumpiness, holes], average_cleared) + self.agents.append([[agg_height, max_height, lines_cleared, bumpiness, holes], average_cleared]) print(_) # return agents @@ -87,50 +87,55 @@ def play_game(self, agg_height, max_height, lines_cleared, bumpiness, holes): return total_cleared - def fitness_crossover(self, pop1: list[list[float], float], pop2: list[list[float], float]) -> list[list[float], float]: - # Combines the two vectors proportionaly by how many lines they cleared - child_pop = pop1[1] * pop1[0] + pop2[1] * pop2[0] - - return list(child_pop, 0) + def replace_30_percent(self, pop_list: list[list[list[float], float]]) -> list[list[float], float]: + new_list = []#: list[list[list[float], float]] + - # TODO create method for fetching a random 10%, and finds the two with highest lines cleared, and makes a child (with 5% chance of mutation) + # Number of pops needed for 30% of total number + num_pops_needed = int(len(pop_list) * 0.3) + + for _ in range(0, num_pops_needed): + new_list.append(self.paring_pop(pop_list)) # liste.append(liste[liste, float]) = liste[liste[liste, float]] + + pop_list: sorted(pop_list, key=lambda x: x[1], reverse=False)[:num_pops_needed] + + pop_list.extend(new_list) + + return pop_list + + + # TODO create method for fetching a random 10%, and finds the two with highest lines cleared, and makes a child (with 5% chance of mutation) def paring_pop(self, pop_list: list[list[list[float], float]]) -> list[list[float], float]: # Gets the number of pops to select - num_pops_to_select: int(len(pop_list) * 0.1) + num_pops_to_select = int(len(pop_list) * 0.1) # Get a sample of pops based on the previous number - random_pop_sample: random.sample(pop_list, num_pops_to_select) + random_pop_sample = random.sample(pop_list, num_pops_to_select) # Gets the two pops with the highest lines cleared - highest_values: sorted(random_pop_sample, key=lambda x: x[1], reverse=True)[:2] + highest_values = sorted(random_pop_sample, key=lambda x: x[1], reverse=True)[:2] # Gets the child pop of the two pops - new_pop = self.fitness_crossover(highest_values[0], highest_values[1]) + new_pop = self.fitness_crossover(highest_values[0], highest_values[1]) # liste[liste, float] # Mutate 5% of children pops - if random.random(0,1) < 0.2: + if random.randrange(0,1000)/1000 < 0.2: random_parameter = int(random.randint(0,4)) new_pop[0][random_parameter] = (random.randrange(-200, 200)/1000) * new_pop[0][random_parameter] - return new_pop - - - def replace_30_percent(self, pop_list: list[list[list[float], float]]) -> list[list[float], float]: - new_list: list[list[list[float], float]] - - # Number of pops needed for 30% of total number - num_pops_needed: int(len(pop_list) * 0.3) - - for _ in range(0, num_pops_needed): - new_list.append(self.paring_pop(pop_list)) - - pop_list: sorted(pop_list, key=lambda x: x[1], reverse=False)[:num_pops_needed] + return new_pop # liste[liste, float] - pop_list.append(new_list) - return pop_list + def fitness_crossover(self, pop1: list[list[float], float], pop2: list[list[float], float]) -> list[list[float], float]: + # Combines the two vectors proportionaly by how many lines they cleared + parent_pop1 = [h * pop1[1] for h in pop1[0]] + parent_pop2 = [h * pop2[1] for h in pop2[0]] + child_pop = [h1 + h2 for h1, h2 in zip(parent_pop1, parent_pop2)] + return [child_pop, 0.0] # liste[liste, float] + def getBestPop(self) -> list[list[float], float]: - pop_list: sorted(pop_list, key, key=lambda x: x[1], reverse=False) + pop_list = self.agents + pop_list = sorted(pop_list, key=lambda x: x[1], reverse=False) return pop_list[0] From c5d917f0f43fe762165a7d8f0939c47fd07c7ad0 Mon Sep 17 00:00:00 2001 From: Maia Austigard Date: Tue, 23 Apr 2024 17:57:17 +0200 Subject: [PATCH 09/19] refactor: optimize code Co-authored-by: Jon Bergland Co-authored-by: henrinha --- main.py | 2 +- src/agents/geneticAlgAgentJon.py | 31 +++++++++++-------------------- 2 files changed, 12 insertions(+), 21 deletions(-) diff --git a/main.py b/main.py index 75e2897..a771d8c 100644 --- a/main.py +++ b/main.py @@ -34,6 +34,6 @@ algAgent = GeneticAlgAgentJM() - algAgent.number_of_selection(1) + algAgent.number_of_selection(2) print(algAgent.getBestPop()) diff --git a/src/agents/geneticAlgAgentJon.py b/src/agents/geneticAlgAgentJon.py index 9171c3a..30980ab 100644 --- a/src/agents/geneticAlgAgentJon.py +++ b/src/agents/geneticAlgAgentJon.py @@ -35,7 +35,8 @@ def number_of_selection(self, number_of_selections: int): param_list = self.agents[i][0] average_cleared = self.play_game(param_list[0], param_list[1], param_list[2], param_list[3], param_list[4]) self.agents[i][1] = average_cleared - + print(self.getBestPop()) + def initAgents(self) -> list[list[list[float], float]]: number_of_agents = 20 @@ -46,12 +47,9 @@ def initAgents(self) -> list[list[list[float], float]]: bumpiness = random.randrange(-1000, 0)/1000 holes = random.randrange(-1000, 0)/1000 - # agents = [] - average_cleared = self.play_game(agg_height, max_height, lines_cleared, bumpiness, holes) / number_of_agents + average_cleared = self.play_game(agg_height, max_height, lines_cleared, bumpiness, holes) self.agents.append([[agg_height, max_height, lines_cleared, bumpiness, holes], average_cleared]) print(_) - - # return agents def play_game(self, agg_height, max_height, lines_cleared, bumpiness, holes): @@ -84,20 +82,16 @@ def play_game(self, agg_height, max_height, lines_cleared, bumpiness, holes): total_cleared += board.rowsRemoved - return total_cleared + return total_cleared / number_of_rounds def replace_30_percent(self, pop_list: list[list[list[float], float]]) -> list[list[float], float]: - new_list = []#: list[list[list[float], float]] - - # Number of pops needed for 30% of total number num_pops_needed = int(len(pop_list) * 0.3) - for _ in range(0, num_pops_needed): - new_list.append(self.paring_pop(pop_list)) # liste.append(liste[liste, float]) = liste[liste[liste, float]] + new_list = [self.paring_pop(pop_list) for _ in range(num_pops_needed)] - pop_list: sorted(pop_list, key=lambda x: x[1], reverse=False)[:num_pops_needed] + pop_list = sorted(pop_list, key=lambda x: x[1], reverse=False)[num_pops_needed:] pop_list.extend(new_list) @@ -116,26 +110,23 @@ def paring_pop(self, pop_list: list[list[list[float], float]]) -> list[list[floa highest_values = sorted(random_pop_sample, key=lambda x: x[1], reverse=True)[:2] # Gets the child pop of the two pops - new_pop = self.fitness_crossover(highest_values[0], highest_values[1]) # liste[liste, float] + new_pop = self.fitness_crossover(highest_values[0], highest_values[1]) # Mutate 5% of children pops if random.randrange(0,1000)/1000 < 0.2: random_parameter = int(random.randint(0,4)) new_pop[0][random_parameter] = (random.randrange(-200, 200)/1000) * new_pop[0][random_parameter] - return new_pop # liste[liste, float] + return new_pop def fitness_crossover(self, pop1: list[list[float], float], pop2: list[list[float], float]) -> list[list[float], float]: # Combines the two vectors proportionaly by how many lines they cleared - parent_pop1 = [h * pop1[1] for h in pop1[0]] - parent_pop2 = [h * pop2[1] for h in pop2[0]] - child_pop = [h1 + h2 for h1, h2 in zip(parent_pop1, parent_pop2)] - - return [child_pop, 0.0] # liste[liste, float] + child_pop = [h1 * pop1[1] + h2 * pop2[1] for h1, h2 in zip(pop1[0], pop2[0])] + return [child_pop, 0.0] def getBestPop(self) -> list[list[float], float]: pop_list = self.agents - pop_list = sorted(pop_list, key=lambda x: x[1], reverse=False) + pop_list = sorted(pop_list, key=lambda x: x[1], reverse=True) return pop_list[0] From 384e97e0ef4bea38bc77e25f577259bb47952fb5 Mon Sep 17 00:00:00 2001 From: Maia Austigard Date: Tue, 23 Apr 2024 18:11:35 +0200 Subject: [PATCH 10/19] refactor: optimize code --- src/agents/geneticAlgAgentJon.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/agents/geneticAlgAgentJon.py b/src/agents/geneticAlgAgentJon.py index 30980ab..4880d45 100644 --- a/src/agents/geneticAlgAgentJon.py +++ b/src/agents/geneticAlgAgentJon.py @@ -35,7 +35,8 @@ def number_of_selection(self, number_of_selections: int): param_list = self.agents[i][0] average_cleared = self.play_game(param_list[0], param_list[1], param_list[2], param_list[3], param_list[4]) self.agents[i][1] = average_cleared - print(self.getBestPop()) + + print(self.getBestPop()) def initAgents(self) -> list[list[list[float], float]]: From 70e7e561b063331e91104354d805314118c206e5 Mon Sep 17 00:00:00 2001 From: Jon Bergland Date: Tue, 23 Apr 2024 18:56:43 +0200 Subject: [PATCH 11/19] refactor: optimize the equals function in tetris.py --- src/game/tetris.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/game/tetris.py b/src/game/tetris.py index ec45feb..f05c093 100644 --- a/src/game/tetris.py +++ b/src/game/tetris.py @@ -1,5 +1,6 @@ import random import copy +import numpy as np from enum import Enum, auto @@ -281,12 +282,12 @@ def __eq__(self, other: "Tetris") -> bool: return False # Check if the blocks are the same - for r in range(self.ROWS): - for c in range(self.COLUMNS): - if self.board[r][c] != other.board[r][c]: - return False + # for r in range(self.ROWS): + # for c in range(self.COLUMNS): + # if self.board[r][c] != other.board[r][c]: + # return False - return True + return np.array_equal(self.board, other.board) def printBoard(self): print("_______________________________________") From d145c8c2ca13f48fbc63c93731b9ddd00618936e Mon Sep 17 00:00:00 2001 From: Maia Austigard Date: Tue, 23 Apr 2024 18:58:14 +0200 Subject: [PATCH 12/19] feat: add normalizing of new vector --- src/agents/geneticAlgAgentJon.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/agents/geneticAlgAgentJon.py b/src/agents/geneticAlgAgentJon.py index 4880d45..d28393c 100644 --- a/src/agents/geneticAlgAgentJon.py +++ b/src/agents/geneticAlgAgentJon.py @@ -1,4 +1,5 @@ import random +import numpy as np from src.game.tetris import * from src.agents.agent_factory import create_agent from src.agents.agent import Agent @@ -58,7 +59,7 @@ def play_game(self, agg_height, max_height, lines_cleared, bumpiness, holes): board = Tetris() agent: Agent = HeuristicWithParametersAgent([agg_height, max_height, lines_cleared, bumpiness, holes]) total_cleared = 0 - number_of_rounds = 10 + number_of_rounds = 20 for _ in range(0, number_of_rounds): max_moves = number_of_rounds @@ -117,7 +118,9 @@ def paring_pop(self, pop_list: list[list[list[float], float]]) -> list[list[floa if random.randrange(0,1000)/1000 < 0.2: random_parameter = int(random.randint(0,4)) new_pop[0][random_parameter] = (random.randrange(-200, 200)/1000) * new_pop[0][random_parameter] - + + new_pop[0] = (new_pop[0] / np.linalg.norm(new_pop[0])).tolist() + return new_pop From 76913533e43b7bd543c5dc898cf6950afc8370aa Mon Sep 17 00:00:00 2001 From: Maia Austigard Date: Tue, 23 Apr 2024 19:42:37 +0200 Subject: [PATCH 13/19] feat: add copy-method for Tetris-object --- src/game/tetris.py | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/src/game/tetris.py b/src/game/tetris.py index f05c093..94a179d 100644 --- a/src/game/tetris.py +++ b/src/game/tetris.py @@ -55,7 +55,7 @@ class Tetris: START_X = 3 START_Y = 0 - def __init__(self, board: list[list[int]] = None, block: Block = None): + def __init__(self, board: list[list[int]] = None, block: Block = None, nextBlock: Block = None): """ Initializes a new game board instance, setting up an empty board, placing the first block, and selecting the next block. """ @@ -70,8 +70,12 @@ def __init__(self, board: list[list[int]] = None, block: Block = None): self.block = Block(self.START_X, self.START_Y, 0) else: self.block = block - self.prevBoard = copy.deepcopy(self.board) + if nextBlock == None: + self.nextBlock = Block(self.START_X, self.START_Y, 0) + else: + self.nextBlock = nextBlock + self.prevBoard = copy.deepcopy(self.board) self._placeBlock() self.prevBlock = self.block.copy() @@ -253,10 +257,10 @@ def getPossibleBoards(self) -> list["Tetris"]: else: rotations = 1 - rotationBoard = copy.deepcopy(self) + rotationBoard = self.copy() for _ in range(rotations): for column in range(0, self.COLUMNS): - moveBoard = copy.deepcopy(rotationBoard) + moveBoard = rotationBoard.copy() # Calibrate the to the left toLeft = moveBoard.block.x @@ -281,13 +285,11 @@ def __eq__(self, other: "Tetris") -> bool: if not isinstance(other, Tetris): return False - # Check if the blocks are the same - # for r in range(self.ROWS): - # for c in range(self.COLUMNS): - # if self.board[r][c] != other.board[r][c]: - # return False - return np.array_equal(self.board, other.board) + + def copy(self) -> "Tetris": + tetris = Tetris(self.board, self.block, self.nextBlock) + return tetris def printBoard(self): print("_______________________________________") From adedbfda37f770444e3229784a982f06b458ab04 Mon Sep 17 00:00:00 2001 From: Maia Austigard Date: Tue, 23 Apr 2024 20:04:12 +0200 Subject: [PATCH 14/19] feat: add deep_copy-method for copying board --- src/game/tetris.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/src/game/tetris.py b/src/game/tetris.py index 94a179d..fd12e10 100644 --- a/src/game/tetris.py +++ b/src/game/tetris.py @@ -92,7 +92,20 @@ def _initBoard(self) -> list[list[int]]: return board def getBoard(self) -> list[list[int]]: - return copy.deepcopy(self.board) + return self.deep_copy_list_of_lists(self.board) + + def deep_copy_list_of_lists(self, original): + if not isinstance(original, list): + return original # Base case: return non-list elements directly + + copied = [] + for sublist in original: + if isinstance(sublist, list): + copied.append(self.deep_copy_list_of_lists(sublist)) # Recursively deep copy nested lists + else: + raise TypeError("Input must be a list of lists of integers") + + return copied def doAction(self, action: Action) -> None: """ @@ -288,7 +301,7 @@ def __eq__(self, other: "Tetris") -> bool: return np.array_equal(self.board, other.board) def copy(self) -> "Tetris": - tetris = Tetris(self.board, self.block, self.nextBlock) + tetris = Tetris(self.deep_copy_list_of_lists(self.board), self.block.copy(), self.nextBlock.copy()) return tetris def printBoard(self): From 39fcfd880508e56916f4ac72d7659660a86d482f Mon Sep 17 00:00:00 2001 From: Jon Bergland Date: Tue, 23 Apr 2024 23:46:58 +0200 Subject: [PATCH 15/19] refactor: change from copy.deepcopy to self defined copy-method --- src/game/tetris.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/game/tetris.py b/src/game/tetris.py index fd12e10..4098197 100644 --- a/src/game/tetris.py +++ b/src/game/tetris.py @@ -1,5 +1,4 @@ import random -import copy import numpy as np from enum import Enum, auto @@ -66,20 +65,21 @@ def __init__(self, board: list[list[int]] = None, block: Block = None, nextBlock self.board = self._initBoard() else: self.board = board + if block == None: self.block = Block(self.START_X, self.START_Y, 0) else: self.block = block + if nextBlock == None: - self.nextBlock = Block(self.START_X, self.START_Y, 0) + self.nextBlock = Block(self.START_X, self.START_Y, random.randint(0, 6)) else: self.nextBlock = nextBlock - self.prevBoard = copy.deepcopy(self.board) + self.prevBoard = self.deep_copy_list_of_lists(self.board) self._placeBlock() self.prevBlock = self.block.copy() - self.nextBlock = Block(self.START_X, self.START_Y, random.randint(0, 6)) def _initBoard(self) -> list[list[int]]: """Initializes an empty the board""" @@ -103,7 +103,7 @@ def deep_copy_list_of_lists(self, original): if isinstance(sublist, list): copied.append(self.deep_copy_list_of_lists(sublist)) # Recursively deep copy nested lists else: - raise TypeError("Input must be a list of lists of integers") + copied.append(sublist) return copied @@ -145,7 +145,7 @@ def doAction(self, action: Action) -> None: self._checkForFullRows() self._checkGameOver() # Store the previous board state before the new block placement - self.prevBoard = copy.deepcopy(self.board) + self.prevBoard = self.deep_copy_list_of_lists(self.board) self._shiftToNewBlock() def isValidBlockPosition(self, block: Block) -> bool: @@ -207,7 +207,7 @@ def isGameOver(self): def _placeBlock(self): """Places the current block on the board""" - self.board = copy.deepcopy(self.prevBoard) + self.board = self.deep_copy_list_of_lists(self.prevBoard) for i in range(4): for j in range(4): if i * 4 + j in self.block.image(): @@ -257,7 +257,7 @@ def _clearRow(self, rownumber: int): newMatrix.insert(0, [0 for _ in range(self.COLUMNS)]) self.board = newMatrix self.rowsRemoved += 1 - self.prevBoard = copy.deepcopy(self.board) + self.prevBoard = self.deep_copy_list_of_lists(self.board) def getPossibleBoards(self) -> list["Tetris"]: possibleMoves = [] @@ -301,7 +301,7 @@ def __eq__(self, other: "Tetris") -> bool: return np.array_equal(self.board, other.board) def copy(self) -> "Tetris": - tetris = Tetris(self.deep_copy_list_of_lists(self.board), self.block.copy(), self.nextBlock.copy()) + tetris = Tetris(self.deep_copy_list_of_lists(self.prevBoard), self.block.copy(), self.nextBlock.copy()) return tetris def printBoard(self): From b75e8f7b666a2da6e7cc08b5cf90b70c63c123c2 Mon Sep 17 00:00:00 2001 From: Jon Bergland Date: Wed, 24 Apr 2024 00:14:41 +0200 Subject: [PATCH 16/19] refactor: make use of list comprehensions in deepcopy-method in tetris.py --- src/game/tetris.py | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/src/game/tetris.py b/src/game/tetris.py index 4098197..d77c351 100644 --- a/src/game/tetris.py +++ b/src/game/tetris.py @@ -1,4 +1,5 @@ import random +from copy import copy import numpy as np from enum import Enum, auto @@ -94,17 +95,8 @@ def _initBoard(self) -> list[list[int]]: def getBoard(self) -> list[list[int]]: return self.deep_copy_list_of_lists(self.board) - def deep_copy_list_of_lists(self, original): - if not isinstance(original, list): - return original # Base case: return non-list elements directly - - copied = [] - for sublist in original: - if isinstance(sublist, list): - copied.append(self.deep_copy_list_of_lists(sublist)) # Recursively deep copy nested lists - else: - copied.append(sublist) - + def deep_copy_list_of_lists(self, original: list[list[int]]) -> list[list[int]]: + copied = [row[:] for row in original] return copied def doAction(self, action: Action) -> None: From cb5d5393a1659946bc830c08a5e1a82371da1915 Mon Sep 17 00:00:00 2001 From: Jon Bergland Date: Wed, 24 Apr 2024 23:10:49 +0200 Subject: [PATCH 17/19] refactor: optimize _outOfBounds, _intersects, isValidBlockPosition and equals methods --- src/game/tetris.py | 33 ++++++++++----------------------- 1 file changed, 10 insertions(+), 23 deletions(-) diff --git a/src/game/tetris.py b/src/game/tetris.py index d77c351..5336a63 100644 --- a/src/game/tetris.py +++ b/src/game/tetris.py @@ -150,28 +150,19 @@ def isValidBlockPosition(self, block: Block) -> bool: Returns: bool: True if the block's position is valid, False otherwise. """ - - if self._outOfBounds(block): - return False - - if self._intersects(block): - return False - - if self.isGameOver(): - return False - - return True + return not (self._outOfBounds(block) or self._intersects(block) or self.isGameOver()) def _outOfBounds(self, block: Block) -> bool: """Checks if the block is out of bounds""" for row in range(4): for column in range(4): if row * 4 + column in block.image(): + block_x, block_y = block.x + column, block.y + row if ( - row + block.y > self.ROWS - 1 - or row + block.y < 0 - or column + block.x > self.COLUMNS - 1 - or column + block.x < 0 + block_y > self.ROWS - 1 + or block_y < 0 + or block_x > self.COLUMNS - 1 + or block_x < 0 ): return True @@ -184,13 +175,9 @@ def _intersects(self, block: Block) -> bool: if row * 4 + column in block.image(): # Check if the block intersects with the board # That is, if the block is on top of another block that is not itself - blockOverlaps = self.prevBoard[row + block.y][column + block.x] > 0 - isItSelf = ( - block.x + column == self.block.x - and block.y + row == self.block.y - ) - - if blockOverlaps and not isItSelf: + block_x, block_y = block.x + column, block.y + row + prev_value = self.prevBoard[block_y][block_x] + if prev_value > 0 and (block_x, block_y) != (self.block.x, self.block.y): return True return False @@ -290,7 +277,7 @@ def __eq__(self, other: "Tetris") -> bool: if not isinstance(other, Tetris): return False - return np.array_equal(self.board, other.board) + return self.board == other.board def copy(self) -> "Tetris": tetris = Tetris(self.deep_copy_list_of_lists(self.prevBoard), self.block.copy(), self.nextBlock.copy()) From ee028a23f52d9c32e4fffcbb44c169bd0885a15b Mon Sep 17 00:00:00 2001 From: Jon Bergland Date: Thu, 25 Apr 2024 00:12:00 +0200 Subject: [PATCH 18/19] feat: add tests for equal method in tetris --- test/game/test_board.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/test/game/test_board.py b/test/game/test_board.py index 4e20900..4bdcca9 100644 --- a/test/game/test_board.py +++ b/test/game/test_board.py @@ -35,6 +35,11 @@ def test_board_equal_for_the_same_object(): board1 = Tetris() assert board1 == board1 +def test_board_equal_for_equal_different_objects(): + board1 = Tetris() + board2 = board1.copy() + assert board1 == board1 + def test_clear_row(): board: Tetris = Tetris() From 6eaeca737fea2faf49dc0b19136e9b404a08f423 Mon Sep 17 00:00:00 2001 From: Jon Bergland Date: Thu, 25 Apr 2024 01:04:50 +0200 Subject: [PATCH 19/19] refactor: combine aggregate height, max_height and bumpiness --- src/agents/heuristic.py | 36 ++++++++++++++++++++++++++++++----- test/agents/test_heuristic.py | 27 +++++++++++++------------- 2 files changed, 45 insertions(+), 18 deletions(-) diff --git a/src/agents/heuristic.py b/src/agents/heuristic.py index 992a3e8..bd7b143 100644 --- a/src/agents/heuristic.py +++ b/src/agents/heuristic.py @@ -7,15 +7,42 @@ def utility(gameState: Tetris, aggregate_heights_weight: float, max_height_weigh lines_cleared_weight: float, bumpiness_weight: float, holes_weight: float) -> int: """Returns the utility of the given game state.""" sum = 0 + aggregate, max_height, bumpiness = calculate_heights(gameState) - sum += aggregate_heights_weight * aggregate_heights(gameState) - sum += max_height_weight * max_height(gameState) + sum += aggregate_heights_weight * aggregate + sum += max_height_weight * max_height sum += lines_cleared_weight * lines_cleaned(gameState) - sum += bumpiness_weight * bumpiness(gameState) + sum += bumpiness_weight * bumpiness sum += holes_weight * find_holes(gameState) return sum +def calculate_heights(gameState: Tetris) -> tuple[int, int, int]: + """Calculates the sum and maximum height of the columns in the game state.""" + #sum_heights = 0 + max_height = 0 + checked_list = [0] * gameState.COLUMNS + + + total_bumpiness = 0 + columnHeightMap = {0: 0, 1: 0, 2: 0, 3: 0, 4: 0, 5: 0, 6: 0, 7: 0, 8: 0, 9: 0} + + + + for row in range(gameState.ROWS - 1, -1, -1): + for column in range(gameState.COLUMNS): + if gameState.prevBoard[row][column] != 0: + height = gameState.ROWS - row + checked_list[column] = height + max_height = max(max_height, height) + columnHeightMap[column] = gameState.ROWS - row + + + for key in range(gameState.COLUMNS - 1): + total_bumpiness += abs(columnHeightMap[key] - columnHeightMap[key + 1]) + + + return sum(checked_list), max_height , total_bumpiness def aggregate_heights(gameState: Tetris) -> int: """Returns the sum of the heights of the columns in the game state.""" @@ -51,13 +78,12 @@ def lines_cleaned(gameState: Tetris) -> int: def bumpiness(gameState: Tetris) -> int: """Returns the sum of the absolute height between all the columns""" total_bumpiness = 0 - max_height = 20 columnHeightMap = {0: 0, 1: 0, 2: 0, 3: 0, 4: 0, 5: 0, 6: 0, 7: 0, 8: 0, 9: 0} for column in range(gameState.COLUMNS): for row in range(gameState.ROWS): if gameState.prevBoard[row][column] > 0: if columnHeightMap[column] == 0: - columnHeightMap[column] = max_height - row + columnHeightMap[column] = gameState.ROWS - row for key in range(gameState.COLUMNS - 1): total_bumpiness += abs(columnHeightMap[key] - columnHeightMap[key + 1]) diff --git a/test/agents/test_heuristic.py b/test/agents/test_heuristic.py index 25f69fe..8a9eb53 100644 --- a/test/agents/test_heuristic.py +++ b/test/agents/test_heuristic.py @@ -27,11 +27,10 @@ def test_heuristic_height_aggregate_empty_board(): [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], ] board = Tetris(initBoard) - assert aggregate_heights(board) == 0 + assert calculate_heights(board)[0] == 0, "Expected aggregate height of 0 for an empty board" def test_heuristic_aggregate_with_equal_heights(): - initBoard = [ [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], @@ -57,7 +56,7 @@ def test_heuristic_aggregate_with_equal_heights(): ] board = Tetris(initBoard) expected = 3 * 9 - assert aggregate_heights(board) == expected + assert calculate_heights(board)[0] == expected def test_heuristic_high_line_heights(): @@ -86,7 +85,7 @@ def test_heuristic_high_line_heights(): ] board = Tetris(initBoard) expected = 3 * 9 - assert aggregate_heights(board) == expected + assert calculate_heights(board)[0] == expected def test_heuristic_different_heights(): @@ -115,7 +114,7 @@ def test_heuristic_different_heights(): ] board = Tetris(initBoard) expected = 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10 - assert aggregate_heights(board) == expected + assert calculate_heights(board)[0] == expected def test_max_height_empty_board(): @@ -142,7 +141,7 @@ def test_max_height_empty_board(): [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], ] board = Tetris(initBoard) - assert max_height(board) == 0, "Expected max height of 0 for an empty board" + assert calculate_heights(board)[1] == 0, "Expected max height of 0 for an empty board" def test_max_height_equal_heights(): @@ -170,7 +169,7 @@ def test_max_height_equal_heights(): ] board = Tetris(initBoard) assert ( - max_height(board) == 20 + calculate_heights(board)[1] == 20 ), "Expected max height of 20 for a board with equal heights" @@ -199,7 +198,7 @@ def test_max_height_takes_highest(): ] board = Tetris(initBoard) assert ( - max_height(board) == 20 + calculate_heights(board)[1] == 20 ), "Expected max height of 20 for a single column with height 20" @@ -335,7 +334,9 @@ def test_bumpiness_empty(): [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], ] - assert bumpiness(board) == 0 + + assert calculate_heights(board)[2] == 0 + #assert bumpiness(board) == 0 def test_bumpiness_five(): @@ -363,7 +364,7 @@ def test_bumpiness_five(): [1, 1, 1, 1, 1, 1, 1, 1, 1, 1], ] board = Tetris(initBoard) - assert bumpiness(board) == 2 + assert calculate_heights(board)[2] == 2 def test_bumpiness_nine(): @@ -391,7 +392,7 @@ def test_bumpiness_nine(): [1, 0, 1, 0, 1, 0, 1, 0, 1, 0], ] board = Tetris(initBoard) - assert bumpiness(board) == 9 + assert calculate_heights(board)[2] == 9 def test_bumpiness_with_holes(): @@ -419,7 +420,7 @@ def test_bumpiness_with_holes(): [1, 1, 1, 0, 1, 0, 1, 0, 1, 0], ] board = Tetris(initBoard) - assert bumpiness(board) == 0 + assert calculate_heights(board)[2] == 0 def test_bumpiness_40(): @@ -447,7 +448,7 @@ def test_bumpiness_40(): [1, 0, 1, 0, 1, 0, 1, 0, 1, 0], ] board = Tetris(initBoard) - assert bumpiness(board) == 40 + assert calculate_heights(board)[2] == 40 def test_aggregate_height_zero():