diff --git a/docs/guide/devContainer.md b/docs/guide/devContainer.md deleted file mode 100644 index 2126630..0000000 --- a/docs/guide/devContainer.md +++ /dev/null @@ -1,39 +0,0 @@ -# Development Environment - -We will be using vscode, and Dev Containers to develop the project. This will allow us to have a consistent development environment across all developers. - -## Getting Started - -* Have [Docker](https://docs.docker.com/get-docker/) installed - * Verify that docker is installed by running `docker --version` -* Install the [Dev Container](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers) extension for vscode -* Pull the latest changes from the repository -* Go to the "Remote Explorer" tab in vscode -* Click on the "Reopen in Container" button for the project -* Done! You should now have a fully functional development environment - -## Extensions - -The following extensions come with the dev container: - -* Prettier -* Code spell checker (to keep the documentation and code clean) -* Docker (in case we will be using docker more in the future) -* Markdownlint (to keep the documentation consistent and clean) -* The default python extensions - * Python - * Pylance - * Debugpy - -You can add more extensions by going to the "Extensions" tab in vscode and clicking on the "Install in Container" button for the extensions you cant live without. - -## Dependencies -When you build the container, the dependencies will be installed. 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 -``` -in the dev container, and then committing the changes. - -## Structural Changes - -In case of structural changes to the project, like adding a new service, or changing `devcontainer.json`, you will need to rebuild the dev container. You can do this by clicking on the "Rebuild Container" button in the "Remote Explorer" tab in vscode. 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/main.py b/main.py index 4b2fc37..5868091 100644 --- a/main.py +++ b/main.py @@ -5,18 +5,20 @@ from src.agents.heuristic import ( 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(1): + # for i in range(10): # end_board = play_game(agent, game, 7) # end_board.printBoard() # sum_rows_removed += end_board.rowsRemoved - # print(f"Rows removed: {end_board.rowsRemoved}") - # print(f"Average rows removed: {sum_rows_removed}") + # print(f"Average rows removed: {sum_rows_removed / 10}") # possible_moves = game.getPossibleBoards() # for boards in possible_moves: @@ -27,4 +29,12 @@ manager = TetrisGameManager(board) agent = create_agent("heuristic") - manager.startDemo(agent) \ No newline at end of file + # manager.startGame() + + # train() + + + algAgent = GeneticAlgAgentJM() + algAgent.number_of_selection(2) + print(algAgent.getBestPop()) + diff --git a/src/agents/agent.py b/src/agents/agent.py index c6bc0cf..33b35ff 100644 --- a/src/agents/agent.py +++ b/src/agents/agent.py @@ -47,17 +47,20 @@ 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. """ + #count = 0 + while not board.isGameOver(): # Get the result of the agent's action - result = agent.result(board) - # Perform the action(s) on the board - if isinstance(result, list): - for action in result: - board.doAction(action) - # board.printBoard() - else: - board.doAction(result) - # board.printBoard() + 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) + + #count += 1 # Advance the game by one frame board.doAction(Action.SOFT_DROP) if board.blockHasLanded: 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 new file mode 100644 index 0000000..d28393c --- /dev/null +++ b/src/agents/geneticAlgAgentJon.py @@ -0,0 +1,136 @@ +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 +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) +# 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 + +class GeneticAlgAgentJM: + agents: list[list[list[float], float]] = [] + + def number_of_selection(self, number_of_selections: int): + 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 + 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 + + print(self.getBestPop()) + + + def initAgents(self) -> list[list[list[float], float]]: + 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 + lines_cleared = random.randrange(0, 1000)/1000 + bumpiness = random.randrange(-1000, 0)/1000 + holes = random.randrange(-1000, 0)/1000 + + 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(_) + + + 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 = 20 + 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 / number_of_rounds + + + def replace_30_percent(self, pop_list: list[list[list[float], float]]) -> list[list[float], float]: + # Number of pops needed for 30% of total number + num_pops_needed = int(len(pop_list) * 0.3) + + 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.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) + + # 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.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 + + + 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 = [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=True) + return pop_list[0] diff --git a/src/agents/heuristic.py b/src/agents/heuristic.py index 0870416..4891a12 100644 --- a/src/agents/heuristic.py +++ b/src/agents/heuristic.py @@ -7,10 +7,12 @@ def utility(gameState: Tetris, aggregate_heights_weight: float, max_height_weigh lines_cleared_weight: float, bumpiness_weight: float, holes_weight: float) -> float: """Returns the utility of the given game state.""" sum = 0 - sum += aggregate_heights_weight * aggregate_heights(gameState) - sum += max_height_weight * max_height(gameState) + aggregate, max_height, bumpiness = calculate_heights(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) # print("--------------------") @@ -23,6 +25,32 @@ def utility(gameState: Tetris, aggregate_heights_weight: float, max_height_weigh 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.""" @@ -65,7 +93,7 @@ def bumpiness(gameState: Tetris) -> int: for row in range(gameState.SPAWN_ROWS, 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/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 diff --git a/src/game/tetris.py b/src/game/tetris.py index 08cd5a2..8e2682e 100644 --- a/src/game/tetris.py +++ b/src/game/tetris.py @@ -1,5 +1,6 @@ import random -import copy +from copy import copy +import numpy as np from enum import Enum, auto from time import sleep @@ -58,7 +59,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. """ @@ -73,8 +74,13 @@ 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, random.randint(0, 6)) + else: + self.nextBlock = nextBlock + self.prevBoard = self.deep_copy_list_of_lists(self.board) self._placeBlock() self.prevBlock = self.block.copy() @@ -92,7 +98,11 @@ 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: list[list[int]]) -> list[list[int]]: + copied = [row[:] for row in original] + return copied def doAction(self, action: Action, demo: bool = False) -> None: """ @@ -138,7 +148,7 @@ def updateBoard(self): 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) if self.isGameOver(): return self._shiftToNewBlock() @@ -153,28 +163,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 @@ -187,13 +188,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 @@ -202,7 +199,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(): @@ -253,7 +250,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 = [] @@ -266,10 +263,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 + (4 - self.block.getRightmostImageCoordinate())): - moveBoard = copy.deepcopy(rotationBoard) + moveBoard = rotationBoard.copy() # Calibrate the to the left toLeft = moveBoard.block.x + moveBoard.block.getLeftmostImageCoordinate() @@ -295,13 +292,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 True + 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()) + return tetris def printBoard(self): print("_______________________________________") diff --git a/test/agents/test_heuristic.py b/test/agents/test_heuristic.py index 51b2a1c..c05998f 100644 --- a/test/agents/test_heuristic.py +++ b/test/agents/test_heuristic.py @@ -30,11 +30,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], @@ -63,7 +62,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(): @@ -95,7 +94,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(): @@ -127,7 +126,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(): @@ -157,7 +156,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(): @@ -188,7 +187,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" @@ -220,7 +219,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" @@ -371,7 +370,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(): @@ -402,7 +403,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(): @@ -433,7 +434,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(): @@ -464,7 +465,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(): @@ -495,7 +496,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(): diff --git a/test/game/test_board.py b/test/game/test_board.py index 5ce765d..e1b4c84 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()