diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json deleted file mode 100644 index 9af928d..0000000 --- a/.devcontainer/devcontainer.json +++ /dev/null @@ -1,41 +0,0 @@ -// 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 - Shortcut.lnk b/docs - Shortcut.lnk new file mode 100644 index 0000000..882ff5d Binary files /dev/null and b/docs - Shortcut.lnk differ diff --git a/docs/guide/env.md b/docs/guide/devContainer.md similarity index 100% rename from docs/guide/env.md rename to docs/guide/devContainer.md diff --git a/docs/guide/venv.md b/docs/guide/venv.md new file mode 100644 index 0000000..3689853 --- /dev/null +++ b/docs/guide/venv.md @@ -0,0 +1,25 @@ +# 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 new file mode 100644 index 0000000..6884f3a --- /dev/null +++ b/docs/planning/12.03.2024.md @@ -0,0 +1,14 @@ +# 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/docs/planning/agentDesign.md b/docs/planning/agentDesign.md new file mode 100644 index 0000000..71c92f4 --- /dev/null +++ b/docs/planning/agentDesign.md @@ -0,0 +1,47 @@ +# Agent design + +## Environment + +When designing an AI agent for playing Tetris, it's essential to understand the task environment's properties. Here's a breakdown of these properties according to common AI environment classifications: + +### Observability (Fully Observable vs. Partially Observable) + +**Fully Observable:** In Tetris, the AI can see the entire game board at all times, including the positions and shapes of all placed pieces. It also knows the current piece and usually the next piece(s) to come. Therefore, Tetris is considered a fully observable environment. + +### Determinism (Deterministic vs. Stochastic) + +**Deterministic** +The environment in Tetris is deterministic because the next state of the environment is completely determined by the current state and the action executed by the agent. There is no randomness in how the pieces respond to control inputs; however, the sequence of the Tetris pieces themselves can introduce some level of unpredictability if the sequence generation is not known in advance, yet the piece manipulation is deterministic. + +### Episodic vs. Sequential + +**Sequential:** +Tetris is sequential because the decisions an agent makes depend on the previous actions, and these decisions accumulate over time to affect the final outcome. Each move affects future opportunities and challenges. + +### Static vs. Dynamic + +**Semi-Dynamic:** +While the game board itself does not change while the agent is making a decision (the timer stops if the piece is not moved), the game introduces new pieces continually, which requires dynamic planning and response. Therefore, it's static during decision-making but dynamic overall because the game progresses with the addition of new pieces. + +### Discrete vs. Continuous + +**Discrete:** Both the time in which decisions are made and the actions available (e.g., moving pieces left, right, rotating them, or dropping them) are discrete. The game operates in steps defined by piece movements and placements. + +### Single-agent vs. Multi-agent + +**Single-agent:** Although some versions of Tetris feature competitive play against other players, the standard environment for Tetris is a single-agent setting where the AI competes against the game environment itself, not against other agents. +Understanding these properties can significantly influence how you design your AI agent, from the decision-making algorithms used (like heuristic functions in deterministic environments) to handling the unpredictability of piece sequence and managing the game's progression. + +## Agent types + +Here are some suggested agent types for playing Tetris: + +- **Random Agent**: Makes random moves without any strategy. +- **Heuristic Agent**: Uses a set of predefined rules or heuristics to evaluate the game state and make decisions. Usually makes the greedy choice based on the current state. +- **Search-based Agent**: Uses search algorithms like iterative deepening, or Monte Carlo Tree Search to explore possible future states and make decisions based on the search results. +- **Reinforcement Learning Agent**: Learns to play Tetris through trial and error, adjusting its strategy based on rewards and penalties received during gameplay. +- Genetic Algorithm Agent: Uses genetic algorithms to evolve a population of agents over time, selecting the best-performing agents for reproduction and mutation. + +## Thoughts + +When doing search based agents it is worth to experiment with giving it foresight into several moves ahead to see if it can make better decisions. And maybe try different reward distributions to see if it can reliebly set up for tetrises. diff --git a/main.py b/main.py index fb2323f..9877935 100644 --- a/main.py +++ b/main.py @@ -1,8 +1,54 @@ -from src.game.TetrisGameManager import TetrisGameManager -from src.game.board import Board - +from src.game.tetris import Tetris +from src.game.TetrisGameManager import * +from src.agents.agent import Agent, play_game +from src.agents.agent_factory import create_agent +from src.agents.heuristic import ( + utility +) +from src.agents.heuristic_trainer import train +from src.agents.geneticAlgAgentJon import GeneticAlgAgentJM +def test(): + # algAgent = GeneticAlgAgentJM() + # algAgent.number_of_selection(2) + # print(algAgent.getBestPop()) + + board = Tetris() + agent = create_agent("heuristic") + manager = TetrisGameManager(board) + manager.startDemo(agent) + if __name__ == "__main__": - board = Board() - game = TetrisGameManager(board) - game.startGame() + + # 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: + # print(utility(boards, 0, -1, 0, 0, 0)) + # boards.printBoard() + + # board = Tetris() + # manager = TetrisGameManager(board) + # agent = create_agent("heuristic") + + # # manager.startGame() + + # # train() + + + # algAgent = GeneticAlgAgentJM() + # algAgent.number_of_selection(2) + # print(algAgent.getBestPop()) + + test() + + + # cProfile.run('main()', 'restats') diff --git a/src/agents/agent.py b/src/agents/agent.py index 65ad5cf..33b35ff 100644 --- a/src/agents/agent.py +++ b/src/agents/agent.py @@ -6,7 +6,8 @@ class for all agents in the simulation. from abc import ABC, abstractmethod from typing import Any, Union -from src.game.board import Action, Board +from src.game.tetris import Action, Tetris +from time import sleep class Agent(ABC): @@ -21,7 +22,7 @@ def __subclasscheck__(cls, subclass: Any) -> bool: return hasattr(subclass, "result") and callable(subclass.result) @abstractmethod - def result(board: Board) -> Union[Action, list[Action]]: + def result(board: Tetris) -> Union[Action, list[Action]]: """ Determines the next move for the agent based on the current state of the board. @@ -34,7 +35,7 @@ def result(board: Board) -> Union[Action, list[Action]]: pass -def play_game(agent: Agent, board: Board, actions_per_drop: int = 1) -> Board: +def play_game(agent: Agent, board: Tetris, actions_per_drop: int = 1) -> Tetris: """ Plays a game of Tetris with the given agent. @@ -46,6 +47,8 @@ def play_game(agent: Agent, board: Board, actions_per_drop: int = 1) -> Board: 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 for _ in range(actions_per_drop): @@ -56,8 +59,38 @@ def play_game(agent: Agent, board: Board, actions_per_drop: int = 1) -> Board: board.doAction(action) else: board.doAction(result) + + #count += 1 # Advance the game by one frame board.doAction(Action.SOFT_DROP) - board.printBoard() + if board.blockHasLanded: + board.updateBoard() + #board.printBoard() return board + +def playGameDemoStepByStep(agent: Agent, board: Tetris) -> Tetris: + """ + Plays a game of Tetris with the given agent where actions are slowed down for demonstration purposes. + + Args: + agent (Agent): The agent to play the game. + board (Board): The initial state of the board. + """ + + # Get the result of the agent's action + result = agent.result(board) + + if Action.HARD_DROP in result: + result.remove(Action.HARD_DROP) + result.append([Action.SOFT_DROP] * 20) + # Perform the action(s) on the board + if isinstance(result, list): + for action in result: + board.doAction(action, demo=True) + else: + board.doAction(action, demo=True) + # Advance the game by one frame + board.doAction(Action.SOFT_DROP) + if board.blockHasLanded: + board.updateBoard() diff --git a/src/agents/agent_factory.py b/src/agents/agent_factory.py index 5832bcb..8a056e4 100644 --- a/src/agents/agent_factory.py +++ b/src/agents/agent_factory.py @@ -1,13 +1,17 @@ """ This module contains the factory function for creating agents. """ from src.agents.agent import Agent -from src.agents.randomAgent import RandomAgent +from src.agents.random_agent import RandomAgent +from src.agents.heuristic_agent import HeuristicAgent + def create_agent(agent_type: str) -> Agent: - """ Create an agent of the specified type. """ + """Create an agent of the specified type.""" - match agent_type.lower(): - case 'random': - return RandomAgent() - case _: - raise ValueError(f'Unknown agent type: {agent_type}') \ No newline at end of file + if agent_type.lower() == "random": + return RandomAgent() + elif agent_type.lower() == "heuristic": + hyperparameters = [1,1,1,1,1] + return HeuristicAgent() + else: + raise ValueError(f"Unknown agent type: {agent_type}") diff --git a/src/agents/geneticAlgAgent.py b/src/agents/geneticAlgAgent.py new file mode 100644 index 0000000..2511b93 --- /dev/null +++ b/src/agents/geneticAlgAgent.py @@ -0,0 +1,8 @@ +# 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 + 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 ec961ab..4891a12 100644 --- a/src/agents/heuristic.py +++ b/src/agents/heuristic.py @@ -1,68 +1,113 @@ """ The heuristic module contains the heuristics used by the agents. """ -from src.game.board import Board +from src.game.tetris import Tetris -def utility(gameState: Board) -> int: +def utility(gameState: Tetris, aggregate_heights_weight: float, max_height_weight: float, + lines_cleared_weight: float, bumpiness_weight: float, holes_weight: float) -> float: """Returns the utility of the given game state.""" - pass + sum = 0 + 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 + sum += holes_weight * find_holes(gameState) + + # print("--------------------") + # print("Aggregate Heights: ", aggregate_heights(gameState)) + # print("Max Height: ", max_height(gameState)) + # print("Lines Cleared: ", lines_cleaned(gameState)) + # print("Bumpiness: ", bumpiness(gameState)) + # print("Holes: ", find_holes(gameState)) + # print("--------------------") + + 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 + -def aggregate_heights(gameState: Board) -> int: + 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.""" checkedList = [0 for i in range(gameState.COLUMNS)] - for i in range(gameState.ROWS): - for j in range(gameState.COLUMNS): - if gameState.board[i][j] > 0: - if checkedList[j] == 0: - checkedList[j] = gameState.ROWS - i + for row in range(gameState.ROWS): + for column in range(gameState.COLUMNS): + if gameState.prevBoard[row][column] != 0: + if checkedList[column] == 0: + checkedList[column] = gameState.ROWS - row return sum(checkedList) -def max_height(gameState: Board) -> int: +def max_height(gameState: Tetris) -> int: """Returns the maximum height of the columns in the game state.""" checkedList = [0 for i in range(gameState.COLUMNS)] - for i in range(gameState.ROWS): - for j in range(gameState.COLUMNS): - if gameState.board[i][j] > 0: - if checkedList[j] == 0: - checkedList[j] = gameState.ROWS - i + for row in range(gameState.ROWS): + for column in range(gameState.COLUMNS): + if gameState.prevBoard[row][column] > 0: + if checkedList[column] == 0: + checkedList[column] = gameState.ROWS - row return max(checkedList) -def lines_cleaned(gameState: Board) -> int: +# Does this work? row cleared in get_possible_boards?? +def lines_cleaned(gameState: Tetris) -> int: """Retrurns the number of lines cleared.""" sum = 0 for row in gameState.board: - if all(cell == 1 for cell in row): + if all(cell >= 1 for cell in row): sum += 1 return sum -def bumpiness(gameState: Board) -> int: +def bumpiness(gameState: Tetris) -> int: """Returns the sum of the absolute height between all the columns""" total_bumpiness = 0 - max_height = 20 + max_height = gameState.ROWS 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.board[row][column] > 0: + 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]) return total_bumpiness - -def aggregate_height(gameState: Board) -> int: +# Henrik exluse ??? +def aggregate_height(gameState: Tetris) -> int: "Returns the sum of all column-heights" - max_height = 20 + max_height = gameState.ROWS total_height = 0 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.board[row][column] > 0: + if gameState.prevBoard[row][column] > 0: if columnHeightMap[column] == 0: columnHeightMap[column] = max_height - row @@ -71,7 +116,7 @@ def aggregate_height(gameState: Board) -> int: return total_height -def find_holes(gameState: Board) -> int: +def find_holes(gameState: Tetris) -> int: """Returns number of empty cells on the board. Args: @@ -81,12 +126,12 @@ def find_holes(gameState: Board) -> int: The heuristic value """ holes = 0 - for i in range(gameState.COLUMNS): - top_block = 20 - for j in range(gameState.ROWS): - if (gameState.board[j][i] == 1) and (j < top_block): - top_block = j - if (gameState.board[j][i] == 0) and (j > top_block): + for column in range(gameState.COLUMNS): + top_block = gameState.ROWS + for row in range(gameState.ROWS): + if (gameState.prevBoard[row][column] >= 1) and (row < top_block): + top_block = row + if (gameState.prevBoard[row][column] == 0) and (row > top_block): holes += 1 return holes diff --git a/src/agents/heuristic_agent.py b/src/agents/heuristic_agent.py new file mode 100644 index 0000000..6ae948c --- /dev/null +++ b/src/agents/heuristic_agent.py @@ -0,0 +1,29 @@ +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 HeuristicAgent(Agent): + + def result(self, board: Tetris) -> list[Action]: + # Get all possible boards + possible_boards = board.getPossibleBoards() + + best_board = None + best_utility = float('-inf') + # Check which board has the best outcome based on the heuristic + for candidate_board in possible_boards: + # current_utility = utility(candidate_board, -0.8, -1.2, 4, -0.3,-0.6) + current_utility = utility(candidate_board, -0.510066, 0, 0.760666, -0.184483, -0.3566) + + if current_utility > best_utility: + best_board = candidate_board + best_utility = current_utility + + # Find the actions needed to transform the current board to the new board + actions = [] + actions = transition_model(board, best_board) + return actions + diff --git a/src/agents/heuristic_agent_Henrik.py b/src/agents/heuristic_agent_Henrik.py new file mode 100644 index 0000000..6f75174 --- /dev/null +++ b/src/agents/heuristic_agent_Henrik.py @@ -0,0 +1,29 @@ +from src.agents.agent import Agent +from src.game.tetris import Action, Tetris, transition_model, get_all_actions +from src.agents.heuristic import ( + utility, + find_holes, + aggregate_height, + max_height, + bumpiness, +) + + +class HeuristicAgentHenrik(Agent): + + def __init__(self, hyperparameters: list[float]): #hyperparameter skal være en liste med 5 tall + self.hyperparameters = hyperparameters + + def result(self, board: Tetris) -> list[Action]: + + all_possible_boards = board.getPossibleBoards() + best_board: Tetris + best_score = float("-inf") + for possible_board in all_possible_boards: + board_utility = utility(possible_board, self.hyperparameters[0], self.hyperparameters[1],self.hyperparameters[2],self.hyperparameters[3],self.hyperparameters[4]) + if board_utility > best_score: + best_board = possible_board + best_score = board_utility + + return transition_model(board, best_board) + \ No newline at end of file diff --git a/src/agents/heuristic_trainer.py b/src/agents/heuristic_trainer.py new file mode 100644 index 0000000..195f152 --- /dev/null +++ b/src/agents/heuristic_trainer.py @@ -0,0 +1,209 @@ +import random +from src.agents.agent import Agent +from src.game.tetris import Action, Tetris, transition_model, get_all_actions +from src.agents.heuristic import ( + utility, + find_holes, + aggregate_height, + max_height, + bumpiness, +) +from src.agents.heuristic_agent_Henrik import HeuristicAgentHenrik + +#kan visualisere lines cleared hvordan den bederer seg over tid. + +def train(): + current_itteration = 0 + agents = _create_heuristic_agents(30) + max_itterations = len(agents) + best_agent = None + best_agent_lines_cleared = 10 + min_lines_cleared = 8 + + hyperparameters_seed = [-0.9550579397805573, -1.713732853744936, 0.48480501821908994, -0.8785318347320727, -1.828473435227082] + epsilon = 0.001 + learning_rate = 0.01 + + print(len(agents)) + + for agent in agents: + game = Tetris() + newAgent = _create_heuristic_agents_hyper(hyperparameters_seed) + end_state = play_game(newAgent, game) + current_itteration += 1 + print(f"[INFO] new agent train, itteration {current_itteration} of {max_itterations}, current best {best_agent_lines_cleared}, this took {end_state.rowsRemoved} ") + if end_state.rowsRemoved > best_agent_lines_cleared: + print(f"[UPDATE] Ny beste agent funnet med {end_state.rowsRemoved} rader fjernet.") + improvement_factor = (end_state.rowsRemoved - best_agent_lines_cleared) / (best_agent_lines_cleared + epsilon) + hyperparameters_seed = [ + seed + learning_rate * improvement_factor * (agent_param - seed) + for seed, agent_param in zip(hyperparameters_seed, agent.hyperparameters) + ] + best_agent = agent + best_agent_lines_cleared = end_state.rowsRemoved + + elif end_state.rowsRemoved > min_lines_cleared: + relative_improvement = (end_state.rowsRemoved - min_lines_cleared) / (best_agent_lines_cleared - min_lines_cleared + epsilon) + hyperparameters_seed = [ + seed + learning_rate * relative_improvement * epsilon * (agent_param - seed) + for seed, agent_param in zip(hyperparameters_seed, agent.hyperparameters) + ] + + #return best_agent, hyperparameters_seed + + print(f'Dette var de beste hyperparameterne: {best_agent.hyperparameters}') + print(f"Dette er antall linjer vi fjernet med dem! :-) {best_agent_lines_cleared}") + + game = Tetris() + end_state = play_game(best_agent, game, shall_render=False) + +def check_average(): + hyperparameters = [-0.9550579397805573, -1.713732853744936, 0.48480501821908994, -0.8785318347320727, -1.828473435227082] + agents = [HeuristicAgentHenrik(hyperparameters) for _ in range(10)] + current_itteration = 0 + max_itterations = 10 + best_agent_lines_cleared = 0 + best_agent = None + + for agent in agents: + game = Tetris() + end_state = play_game(agent, game) + current_itteration += 1 + print(f"[INFO] new agent train, itteration {current_itteration} of {max_itterations}, current best {best_agent_lines_cleared}, this took {end_state.rowsRemoved} ") + if end_state.rowsRemoved > best_agent_lines_cleared: + print(f"[UPDATE] Ny beste agent funnet med {end_state.rowsRemoved} rader fjernet.") + best_agent = agent + best_agent_lines_cleared = end_state.rowsRemoved + + print(f'Dette var de beste hyperparameterne: {best_agent.hyperparameters}') + print(f"Dette er antall linjer vi fjernet med dem! :-) {best_agent_lines_cleared}") + + + +def train_random(): + current_itteration = 0 + agents = _create_heuristic_agents(50) + max_itterations = len(agents) + best_agent = None + best_agent_lines_cleared = 0 + + print(f'Det er {len(agents)} agenter som nå skaø prøve seg med tilfeldige hyperparametere!') + + for agent in agents: + game = Tetris() + end_state = play_game(agent, game) + current_itteration += 1 + print(f"[INFO] new agent train, itteration {current_itteration} of {max_itterations}, current best {best_agent_lines_cleared}, this took {end_state.rowsRemoved} ") + if end_state.rowsRemoved > best_agent_lines_cleared: + print(f"[UPDATE] Ny beste agent funnet med {end_state.rowsRemoved} rader fjernet.") + best_agent = agent + best_agent_lines_cleared = end_state.rowsRemoved + + print(f'Dette var de beste hyperparameterne: {best_agent.hyperparameters}') + print(f"Dette er antall linjer vi fjernet med dem! :-) {best_agent_lines_cleared}") + + game = Tetris() + end_state = play_game(best_agent, game, shall_render=False) + + +def _create_heuristic_agents(num_agents: int): + agents = [HeuristicAgentHenrik(create_random_hyperparameters()) for _ in range(num_agents)] + return agents + +def _create_heuristic_agents_hyper(hyperparameters): + return HeuristicAgentHenrik(hyperparameters) + + +def create_random_hyperparameters(): + return [random.uniform(-1, 0), #aggregate_heights_weight + random.uniform(-3, 0), #max_height_weight + random.uniform(0, 10), #lines_cleared_weight + random.uniform(-1, 0), #bumpiness_weight + random.uniform(-2, 0) #holes_weight + ] + +def play_game(agent: Agent, board: Tetris, actions_per_drop: int = 10, max_moves: int = 50000, shall_render = False) -> Tetris: + """ + Plays a game of Tetris with the given agent. + + Args: + agent (Agent): The agent to play the game. + board (Board): The initial state of the board. + actions_per_drop (int, optional): The number of actions to perform per soft drop. Defaults to 1. + + Returns: + The final state of the board after the game is over. + """ + + + move = 0 + try: + while not board.isGameOver() or move < max_moves: + move += 1 + # 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) + # Advance the game by one frame + board.doAction(Action.SOFT_DROP) + if shall_render: + board.printBoard() + + except Exception: + return board + + return board + + + +def train_beta(): + current_iteration = 0 + agents = _create_heuristic_agents(500) + max_iterations = len(agents) + best_agent = None + best_agent_lines_cleared = 10 + min_lines_cleared = 8 + + hyperparameters_seed = [-0.6272731926460421, -0.029942858429951258, 1.1576374779977394, -0.9984880816033778, -0.4298512882832837] + best_hyperparameters = list(hyperparameters_seed) + learning_rate = 0.1 + learning_rate_decay = 0.9 + exploration_rate = 0.1 + + print(len(agents)) + + for agent in agents: + game = Tetris() + end_state = play_game(agent, game) + current_iteration += 1 + lines_cleared = end_state.rowsRemoved + + print(f"[INFO] Ny agent trener, iterasjon {current_iteration} av {max_iterations}, nåværende beste {best_agent_lines_cleared}, dette tok {lines_cleared} rader") + + # Utforsk nye hyperparametre med en sjanse på exploration_rate + if random.random() < exploration_rate: + hyperparameters_seed = create_random_hyperparameters() + + # Hvis agenten presterer bedre, oppdater de beste hyperparametrene + if lines_cleared > best_agent_lines_cleared: + print(f"[UPDATE] Ny beste agent funnet med {lines_cleared} rader fjernet.") + best_hyperparameters = list(agent.hyperparameters) + best_agent_lines_cleared = lines_cleared + best_agent = agent + learning_rate *= learning_rate_decay # Reduser læringsraten etter en vellykket oppdatering + elif lines_cleared < best_agent_lines_cleared: + # Tilbakestill til de beste kjente hyperparametrene hvis ytelsen er dårlig + hyperparameters_seed = list(best_hyperparameters) + learning_rate /= learning_rate_decay # Øk læringsraten for å utforske mer + + + + print(f'Dette var de beste hyperparameterne: {best_agent.hyperparameters}') + print(f"Dette er antall linjer vi fjernet med dem! :-) {best_agent_lines_cleared}") + + 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/agents/randomAgent.py b/src/agents/randomAgent.py deleted file mode 100644 index 44f2ba7..0000000 --- a/src/agents/randomAgent.py +++ /dev/null @@ -1,14 +0,0 @@ -from src.agents.agent import Agent -from src.game.board import Action, Board, get_all_actions - -from random import choice - - -class RandomAgent(Agent): - """Random agent that selects a random move from the list of possible moves""" - - def result(self, board: Board) -> Action: - # TODO: Get all possible actions - - # TODO: Return a random action - pass diff --git a/src/agents/random_agent.py b/src/agents/random_agent.py new file mode 100644 index 0000000..845986c --- /dev/null +++ b/src/agents/random_agent.py @@ -0,0 +1,14 @@ +from src.agents.agent import Agent +from src.game.tetris import Action, Tetris, get_all_actions + +from random import choice + + +class RandomAgent(Agent): + """Random agent that selects a random move from the list of possible moves""" + + def result(self, board: Tetris) -> Action: + possible_actions = get_all_actions() + return choice(possible_actions) + + diff --git a/src/game/TetrisGameManager.py b/src/game/TetrisGameManager.py index 7bdb59b..8989563 100644 --- a/src/game/TetrisGameManager.py +++ b/src/game/TetrisGameManager.py @@ -1,51 +1,38 @@ -from pynput.keyboard import Key, Listener +from copy import deepcopy +import pygame +from pygame.locals import * import time as t import sys -from src.game.board import Action, Board +from src.agents.agent import Agent, playGameDemoStepByStep +from src.game.tetris import Action, Tetris +from src.game.block import COLORS baseScore = 100 -""" TODO: Timer for piece drop - keyboard input for piece movement - keyboard input for piece rotation - keyboard input for piece drop - keyboard input for game start - soft drop and hard drop implementation - """ +# pygame visuals setup +BLOCK_SIZE = 40 +WIDTH = 10 +HEIGHT = 23 +START_HEIGHT = 3 +SCREEN_WIDTH = WIDTH * BLOCK_SIZE +SCREEN_HEIGHT = (HEIGHT - START_HEIGHT) * BLOCK_SIZE +# Colors +BLACK = (0, 0, 0) +WHITE = (255, 255, 255) class TetrisGameManager: - currentPiece = None nextPiece = None updateTimer = 1 streak = 1 - def __init__(self, board: Board): + def __init__(self, board: Tetris): self.board = board self.score = 0 self.currentTime = int(round(t.time() * 1000)) - self.switcher = { - Key.down: Action.SOFT_DROP, - Key.left: Action.MOVE_LEFT, - Key.right: Action.MOVE_RIGHT, - Key.space: Action.HARD_DROP, - Key.up: Action.ROTATE_CLOCKWISE, - } - - def onPress(self, key): - # Default action if key not found - default_action = lambda: "Key not recognized" - - # Get the function to execute based on the key, or default action - action = self.switcher.get(key, default_action) - self.movePiece(action) - - def onRelease(self, key): - pass - def movePiece(self, direction: Action): self.board.doAction(direction) self.board.printBoard() @@ -54,41 +41,76 @@ def isGameOver(self): return self.board.isGameOver() def startGame(self): - self.currentPiece = self.newPiece() - self.nextPiece = self.newPiece() - self.board.printBoard() + pygame.init() + self.screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT)) + pygame.display.set_caption('Tetris') # Set window title - listener = Listener(on_press=self.onPress, on_release=self.onRelease) - listener.start() + clock = pygame.time.Clock() while not self.board.gameOver: - + self.draw_board(self.board) + self.inputHandling() + if self.board.blockHasLanded: + self.board.updateBoard() # Update the board after a block has landed and spawn a new block self.checkTimer() - - t.sleep(0.1) # Add a small delay to reduce CPU usage - - # Stop the listener when the game is over - print("Stopping listener") - listener.stop() - - def newPiece(self): - pass - # return self.pieces.getNewPiece() - - def updateScore(self, linesCleared): - self.score += self.streak * (baseScore**linesCleared) + pygame.display.update() + clock.tick(60) # Cap the frame rate to 60 FPS + + self.stopGame() + + def startDemo(self, agent: Agent): + pygame.init() + self.screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT)) + pygame.display.set_caption('Tetris') # Set window title + + clock = pygame.time.Clock() + + while not self.board.gameOver: + self.draw_board(self.board) + playGameDemoStepByStep(agent, self.board) + pygame.display.update() + clock.tick(60) # Cap the frame rate to 60 FPS + + self.stopGame() + + + + + def inputHandling(self): + for event in pygame.event.get(): + if event.type == QUIT: + self.stopGame() + else: + keys = pygame.key.get_pressed() + if keys[K_DOWN]: + self.movePiece(Action.SOFT_DROP) + elif keys[K_LEFT]: + self.movePiece(Action.MOVE_LEFT) + elif keys[K_RIGHT]: + self.movePiece(Action.MOVE_RIGHT) + elif keys[K_SPACE]: + self.movePiece(Action.HARD_DROP) + elif keys[K_UP]: + self.movePiece(Action.ROTATE_CLOCKWISE) def checkTimer(self): checkTime = self.currentTime + 1000 / self.updateTimer newTime = int(round(t.time() * 1000)) - # if (checkTime < newTime): - # self.currentTime = newTime - # self.movePiece("DOWN") - # print("Timer checked") - # self.board.printBoard() + if checkTime < newTime: + self.currentTime = newTime + self.movePiece(Action.SOFT_DROP) + + def draw_board(self, gameState: Tetris): + self.screen.fill(BLACK) + temp = deepcopy(gameState) + temp_board = temp.board[START_HEIGHT:] + for y in range(HEIGHT-START_HEIGHT): + for x in range(WIDTH): + if temp_board[y][x] != 0: + pygame.draw.rect(self.screen, COLORS[temp_board[y][x]-1], (x * BLOCK_SIZE, y * BLOCK_SIZE, BLOCK_SIZE, BLOCK_SIZE)) + pygame.draw.rect(self.screen, WHITE, (x * BLOCK_SIZE, y * BLOCK_SIZE, BLOCK_SIZE, BLOCK_SIZE), 1) - return True def stopGame(self): - self.board.gameOver = True + pygame.quit() sys.exit() diff --git a/src/game/block.py b/src/game/block.py index bb0752c..1e8c464 100644 --- a/src/game/block.py +++ b/src/game/block.py @@ -3,15 +3,19 @@ FIGURES = [ # Definitions for each Tetris block rotation - [[1, 5, 9, 13], [4, 5, 6, 7], [2, 6, 10, 14], [8, 9, 10, 11]], # I - [[4, 5, 9, 10], [2, 6, 5, 9], [0, 1, 5, 6], [1, 5, 4, 8]], # Z - [[9, 10, 6, 7], [1, 5, 6, 10], [5, 6, 2, 3], [2, 6, 7, 11]], # S - [[1, 2, 5, 9], [0, 4, 5, 6], [1, 5, 9, 8], [4, 5, 6, 10]], # L - [[1, 2, 6, 10], [5, 6, 7, 9], [2, 6, 10, 11], [3, 5, 6, 7]], # J - [[1, 4, 5, 6], [1, 4, 5, 9], [4, 5, 6, 9], [1, 5, 6, 9]], # T + + [[4, 5, 6, 7], [2, 6, 10, 14], [8, 9, 10, 11], [1, 5, 9, 13]], # I + [[0, 1, 5, 6], [2, 5, 6, 9], [4, 5, 9, 10], [5, 8, 9, 12]], # Z + [[4, 5, 1, 2], [1, 5, 6, 10], [8, 9, 5, 6], [0, 4, 5, 9]], # S + [[2, 4, 5, 6], [1, 5, 9, 10], [4, 5, 6, 8], [0, 1, 5, 9]], # L + [[0, 4, 5, 6], [1, 2, 5, 9], [4, 5, 6, 10], [1, 5, 8, 9]], # J + [[1, 4, 5, 6], [1, 5, 6, 9], [4, 5, 6, 9], [1, 4, 5, 9]], # T [[1, 2, 5, 6]], # O ] + + + # Colors for the blocks COLORS = [ # RGB color definitions for each block type @@ -125,3 +129,27 @@ def getListCoordinates(self) -> list: listCoordinates.append((x, y)) return listCoordinates + + def getLeftmostImageCoordinate(self) -> int: + """ + Returns: + int: The leftmost x-coordinate of the block's image. + """ + leftmost = 4 + for i in self.image(): + x = i % 4 + if x < leftmost: + leftmost = x + return leftmost + + def getRightmostImageCoordinate(self) -> int: + """ + Returns: + int: The rightmost x-coordinate of the block's image. + """ + rightmost = 0 + for i in self.image(): + x = i % 4 + if x > rightmost: + rightmost = x + return rightmost \ No newline at end of file diff --git a/src/game/board.py b/src/game/tetris.py similarity index 67% rename from src/game/board.py rename to src/game/tetris.py index 747251e..a6dfb10 100644 --- a/src/game/board.py +++ b/src/game/tetris.py @@ -1,10 +1,14 @@ import random -import copy +from copy import copy +import numpy as np from enum import Enum, auto +from time import sleep from src.game.block import Block +DEMO_SLEEP = 0.05 + class Action(Enum): """Enumeration for the possible actions that can be performed on the board""" @@ -34,7 +38,7 @@ def get_all_actions() -> list[Action]: ] -class Board: +class Tetris: """ Represents the Tetris game board, handling block placements, movements, and rotations, as well as checking for game over conditions. @@ -49,32 +53,39 @@ class Board: nextBlock (Block): The next block that will be introduced to the board after the current block is placed. """ - ROWS = 20 + ROWS = 23 + SPAWN_ROWS = 3 COLUMNS = 10 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. """ self.gameOver = False self.rowsRemoved = 0 - if board == None: + if board is None: self.board = self._initBoard() else: self.board = board - if block == None: + if block is 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() self.nextBlock = Block(self.START_X, self.START_Y, random.randint(0, 6)) + self.blockHasLanded = False def _initBoard(self) -> list[list[int]]: """Initializes an empty the board""" @@ -87,53 +98,60 @@ 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) -> None: + def doAction(self, action: Action, demo: bool = False) -> None: """ Performs the specified action on the current block and updates the game board accordingly. Args: action (Action): The action to perform, as defined in the Action enumeration. + demo (bool): If True, the action will be performed with a delay for demonstration purposes. """ # Move the new block according to the action new_block = self.block.copy() - match action: - case Action.MOVE_LEFT: - new_block.moveLeft() - case Action.MOVE_RIGHT: - new_block.moveRight() - case Action.ROTATE_CLOCKWISE: - new_block.rotateRight() - case Action.ROTATE_COUNTERCLOCKWISE: - new_block.rotateLeft() - case Action.HARD_DROP: - while True: - new_block.moveDown() - if not self.isValidBlockPosition(new_block): - new_block.moveUp() - break - case Action.SOFT_DROP: + if action == Action.MOVE_LEFT: + new_block.moveLeft() + elif action == Action.MOVE_RIGHT: + new_block.moveRight() + elif action == Action.ROTATE_CLOCKWISE: + new_block.rotateRight() + elif action == Action.ROTATE_COUNTERCLOCKWISE: + new_block.rotateLeft() + elif action == Action.HARD_DROP: + while self.isValidBlockPosition(new_block): new_block.moveDown() - - # Given the new block position, check if it is valid and update the board - if self.isValidBlockPosition(new_block): - self.block = new_block - self._placeBlock() + elif action == Action.SOFT_DROP: + new_block.moveDown() # For blocks reaching the bottom of the board, place the block and introduce a new one if ( - not self.isValidBlockPosition(new_block) - and action == Action.SOFT_DROP - or action == Action.HARD_DROP + action in [Action.HARD_DROP, Action.SOFT_DROP] + and not self.isValidBlockPosition(new_block) ): + new_block.moveUp() + self.blockHasLanded = True + if self.isValidBlockPosition(new_block): + self.block = new_block self._placeBlock() - self._checkGameOver() - # Store the previous board state before the new block placement - self.prevBoard = copy.deepcopy(self.board) - self._checkForFullRows() - self._shiftToNewBlock() + if demo: + sleep(DEMO_SLEEP) + + + def updateBoard(self): + self.blockHasLanded = False + self._checkForFullRows() + self._checkGameOver() + # Store the previous board state before the new block placement + self.prevBoard = self.deep_copy_list_of_lists(self.board) + if self.isGameOver(): + return + self._shiftToNewBlock() def isValidBlockPosition(self, block: Block) -> bool: """ @@ -145,30 +163,19 @@ def isValidBlockPosition(self, block: Block) -> bool: Returns: bool: True if the block's position is valid, False otherwise. """ - - if self._outOfBounds(block): - print("[DEBUG] Out of bounds") - return False - - if self._intersects(block): - print("[DEBUG] Intersects") - 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 @@ -176,19 +183,14 @@ def _outOfBounds(self, block: Block) -> bool: def _intersects(self, block: Block) -> bool: """Checks if the block intersects with another block on the board""" - ## TODO: Fix this for row in range(4): for column in range(4): 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 @@ -197,13 +199,14 @@ 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(): self.board[i + self.block.y][ j + self.block.x - ] = 1 # self.block.color + ] = self.block.type + 1 # implicit color 1 to 7 + def _shiftToNewBlock(self): """Places the current block on the board and sets the next block as the current block""" @@ -215,14 +218,15 @@ def _shiftToNewBlock(self): if i * 4 + j in self.block.image(): self.board[i + self.block.y][ j + self.block.x - ] = 1 # self.block.color + ] = self.block.type + 1 # implicit color 1 to 7 def _checkGameOver(self): """Checks if the game is over""" - for cell in self.board[0]: - if cell > 0: - self.gameOver = True - break + for spawn_row in range(self.SPAWN_ROWS): + for cell in self.board[spawn_row]: + if cell > 0: + self.gameOver = True + return def _checkForFullRows(self) -> int: """Checks the board for full rows and removes them, returning the number of rows removed""" @@ -234,7 +238,7 @@ def _checkForFullRows(self) -> int: if 0 not in row: fullRows.append(rowIndex) # Remove all full rows - for rowIndex in reversed(fullRows): + for rowIndex in fullRows: self._clearRow(rowIndex) amount += 1 return amount @@ -243,11 +247,12 @@ def _clearRow(self, rownumber: int): """Clears the specified row and moves all rows above down one step""" # Remove the row and add a new empty row at the top newMatrix = self.board[:rownumber] + self.board[rownumber + 1 :] - newMatrix.append([0 for _ in range(self.COLUMNS)]) + newMatrix.insert(0, [0 for _ in range(self.COLUMNS)]) self.board = newMatrix self.rowsRemoved += 1 + self.prevBoard = self.deep_copy_list_of_lists(self.board) - def getPossibleBoards(self) -> list["Board"]: + def getPossibleBoards(self) -> list["Tetris"]: possibleMoves = [] # Number of rotations which gives unique block positions @@ -258,14 +263,14 @@ def getPossibleBoards(self) -> list["Board"]: else: rotations = 1 - rotationBoard = copy.deepcopy(self) + rotationBoard = self.copy() for _ in range(rotations): - for column in range(self.COLUMNS): - moveBoard = copy.deepcopy(rotationBoard) + for column in range(0, self.COLUMNS + (4 - self.block.getRightmostImageCoordinate())): + moveBoard = rotationBoard.copy() # Calibrate the to the left - toLeft = moveBoard.block.x - for _ in range(toLeft): + toLeft = moveBoard.block.x + moveBoard.block.getLeftmostImageCoordinate() + for _ in range(toLeft + 1): moveBoard.doAction(Action.MOVE_LEFT) # Move the block to the correct column for _ in range(column): @@ -275,6 +280,7 @@ def getPossibleBoards(self) -> list["Board"]: if not moveBoard.isValidBlockPosition(moveBoard.block): continue + moveBoard.prevBoard = moveBoard.deep_copy_list_of_lists(moveBoard.board) if moveBoard not in possibleMoves: possibleMoves.append(moveBoard) @@ -282,17 +288,15 @@ def getPossibleBoards(self) -> list["Board"]: return possibleMoves - def __eq__(self, other: "Board") -> bool: - if not isinstance(other, Board): + 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("_______________________________________") @@ -302,13 +306,13 @@ def printBoard(self): print("‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾") def _checkCharacter(self, character) -> str: - if character == 1: + if character >= 1: return "■" else: return "▧" -def transition_model(current_state: Board, target_state: Board) -> list[Action]: +def transition_model(current_state: Tetris, target_state: Tetris) -> list[Action]: """ Calculates the sequence of actions required to transition from the current board state to the target board state. @@ -323,11 +327,12 @@ def transition_model(current_state: Board, target_state: Board) -> list[Action]: actions = [] if current_state == target_state: - print("No transition needed") + actions.append(Action.SOFT_DROP) + # print("No transition needed") return actions # Find where the last block is in the target state - target_block = target_state.prevBlock + target_block = target_state.block # Find the correct rotation needed_rotation = target_block.rotation - current_state.block.rotation @@ -338,7 +343,7 @@ def transition_model(current_state: Board, target_state: Board) -> list[Action]: actions += [Action.MOVE_RIGHT] * (target_block.x - current_state.block.x) elif current_state.block.x > target_block.x: actions += [Action.MOVE_LEFT] * (current_state.block.x - target_block.x) - # Move the block down to the correct y position + # Move the block down to the correct y position as it would be used in reality actions.append(Action.HARD_DROP) return actions diff --git a/test/agents/test_agent_factory.py b/test/agents/test_agent_factory.py index 46d6713..1ba346a 100644 --- a/test/agents/test_agent_factory.py +++ b/test/agents/test_agent_factory.py @@ -1,13 +1,15 @@ from src.agents.agent_factory import create_agent -from src.agents.randomAgent import RandomAgent +from src.agents.random_agent import RandomAgent + def test_create_agent_random(): - agent = create_agent('random') + agent = create_agent("random") assert isinstance(agent, RandomAgent) + def test_create_agent_unknown(): try: - create_agent('unknown') + create_agent("unknown") assert False, "Expected ValueError" except ValueError as e: - assert str(e) == "Unknown agent type: unknown" \ No newline at end of file + assert str(e) == "Unknown agent type: unknown" diff --git a/test/agents/test_heuristic.py b/test/agents/test_heuristic.py index 47d5b8c..c05998f 100644 --- a/test/agents/test_heuristic.py +++ b/test/agents/test_heuristic.py @@ -1,10 +1,13 @@ -from src.game.board import Board +from src.game.tetris import Tetris from src.agents.heuristic import * def test_heuristic_height_aggregate_empty_board(): - board = Board() - board.board = [ + + initBoard = [ + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], @@ -26,14 +29,16 @@ def test_heuristic_height_aggregate_empty_board(): [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], ] - - assert aggregate_heights(board) == 0 + board = Tetris(initBoard) + assert calculate_heights(board)[0] == 0, "Expected aggregate height of 0 for an empty board" def test_heuristic_aggregate_with_equal_heights(): - board = Board() - board.board = [ + initBoard = [ + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], @@ -55,13 +60,17 @@ def test_heuristic_aggregate_with_equal_heights(): [1, 1, 1, 1, 1, 1, 1, 1, 1, 0], [1, 1, 1, 1, 1, 1, 1, 1, 1, 0], ] + board = Tetris(initBoard) expected = 3 * 9 - assert aggregate_heights(board) == expected + assert calculate_heights(board)[0] == expected def test_heuristic_high_line_heights(): - board = Board() - board.board = [ + + initBoard = [ + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], @@ -83,13 +92,17 @@ def test_heuristic_high_line_heights(): [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], ] + board = Tetris(initBoard) expected = 3 * 9 - assert aggregate_heights(board) == expected + assert calculate_heights(board)[0] == expected def test_heuristic_different_heights(): - board = Board() - board.board = [ + + initBoard = [ + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], @@ -111,14 +124,16 @@ def test_heuristic_different_heights(): [0, 1, 0, 0, 0, 0, 0, 0, 0, 0], [1, 0, 0, 0, 0, 0, 0, 0, 0, 0], ] - + 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(): - board = Board() - board.board = [ + initBoard = [ + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], @@ -140,12 +155,15 @@ def test_max_height_empty_board(): [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], ] - assert max_height(board) == 0, "Expected max height of 0 for an empty board" + board = Tetris(initBoard) + assert calculate_heights(board)[1] == 0, "Expected max height of 0 for an empty board" def test_max_height_equal_heights(): - board = Board() - board.board = [ + initBoard = [ + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [1, 1, 1, 1, 1, 1, 1, 1, 1, 1], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], @@ -167,14 +185,17 @@ def test_max_height_equal_heights(): [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], ] + board = Tetris(initBoard) assert ( - max_height(board) == 20 + calculate_heights(board)[1] == 20 ), "Expected max height of 20 for a board with equal heights" def test_max_height_takes_highest(): - board = Board() - board.board = [ + initBoard = [ + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [1, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], @@ -196,13 +217,17 @@ def test_max_height_takes_highest(): [0, 1, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], ] + 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" - + + def test_lines_cleared(): - board = Board() - board.board = [ + initBoard = [ + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], @@ -224,11 +249,12 @@ def test_lines_cleared(): [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [1, 1, 1, 1, 1, 1, 1, 1, 1, 1], ] - assert ( - lines_cleaned(board) == 1 ) - + board = Tetris(initBoard) + assert lines_cleaned(board) == 1 + + def test_no_lines_cleared(): - board = Board() + board = Tetris() board.board = [ [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], @@ -250,13 +276,18 @@ def test_no_lines_cleared(): [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], ] - assert ( - lines_cleaned(board) == 0 ) + assert lines_cleaned(board) == 0 + def test_twenty_lines_cleared(): - board = Board() - board.board = [ + initBoard = [ + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [1, 1, 1, 1, 1, 1, 1, 1, 1, 1], [1, 1, 1, 1, 1, 1, 1, 1, 1, 1], [1, 1, 1, 1, 1, 1, 1, 1, 1, 1], @@ -278,12 +309,15 @@ def test_twenty_lines_cleared(): [1, 1, 1, 1, 1, 1, 1, 1, 1, 1], [1, 1, 1, 1, 1, 1, 1, 1, 1, 1], ] - assert ( - lines_cleaned(board) == 20 ) - + board = Tetris(initBoard) + assert lines_cleaned(board) == 20 + + def test_ten_lines_cleared(): - board = Board() - board.board = [ + initBoard = [ + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], @@ -305,11 +339,12 @@ def test_ten_lines_cleared(): [1, 1, 1, 1, 1, 1, 1, 1, 1, 1], [1, 1, 1, 1, 1, 1, 1, 1, 1, 1], ] - assert ( - lines_cleaned(board) == 10 ) + board = Tetris(initBoard) + assert lines_cleaned(board) == 10 + def test_bumpiness_empty(): - board = Board() + board = Tetris() board.board = [ [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], @@ -331,14 +366,21 @@ def test_bumpiness_empty(): [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [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(): - board = Board() - board.board = [ + + initBoard = [ + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], @@ -360,12 +402,16 @@ def test_bumpiness_five(): [0, 0, 0, 0, 0, 0, 0, 0, 1, 0], [1, 1, 1, 1, 1, 1, 1, 1, 1, 1], ] - assert ( - bumpiness(board) == 2 ) - + board = Tetris(initBoard) + assert calculate_heights(board)[2] == 2 + + def test_bumpiness_nine(): - board = Board() - board.board = [ + + initBoard = [ + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], @@ -387,12 +433,16 @@ def test_bumpiness_nine(): [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [1, 0, 1, 0, 1, 0, 1, 0, 1, 0], ] - assert ( - bumpiness(board) == 9 ) - + board = Tetris(initBoard) + assert calculate_heights(board)[2] == 9 + + def test_bumpiness_with_holes(): - board = Board() - board.board = [ + + initBoard = [ + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [1, 1, 1, 1, 1, 1, 1, 1, 1, 1], [1, 0, 0, 0, 0, 0, 0, 0, 0, 0], [1, 0, 0, 0, 0, 0, 0, 0, 0, 0], @@ -414,12 +464,16 @@ def test_bumpiness_with_holes(): [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [1, 1, 1, 0, 1, 0, 1, 0, 1, 0], ] - assert ( - bumpiness(board) == 0 ) - + board = Tetris(initBoard) + assert calculate_heights(board)[2] == 0 + + def test_bumpiness_40(): - board = Board() - board.board = [ + + initBoard = [ + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [1, 0, 1, 1, 1, 1, 1, 1, 1, 1], [1, 0, 0, 0, 0, 0, 0, 0, 0, 0], [1, 0, 0, 0, 0, 0, 0, 0, 0, 0], @@ -441,12 +495,16 @@ def test_bumpiness_40(): [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [1, 0, 1, 0, 1, 0, 1, 0, 1, 0], ] - assert ( - bumpiness(board) == 40 ) - + board = Tetris(initBoard) + assert calculate_heights(board)[2] == 40 + + def test_aggregate_height_zero(): - board = Board() - board.board = [ + + initBoard = [ + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], @@ -468,12 +526,16 @@ def test_aggregate_height_zero(): [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], ] - assert ( - aggregate_height(board) == 0 ) - + board = Tetris(initBoard) + assert aggregate_height(board) == 0 + + def test_aggregate_height_full(): - board = Board() - board.board = [ + + initBoard = [ + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [1, 1, 1, 1, 1, 1, 1, 1, 1, 1], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], @@ -495,12 +557,16 @@ def test_aggregate_height_full(): [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], ] - assert ( - aggregate_height(board) == 200 ) - + board = Tetris(initBoard) + assert aggregate_height(board) == 200 + + def test_aggregate_height_half(): - board = Board() - board.board = [ + + initBoard = [ + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [1, 1, 1, 1, 1, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], @@ -522,12 +588,16 @@ def test_aggregate_height_half(): [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], ] - assert ( - aggregate_height(board) == 100 ) - + board = Tetris(initBoard) + assert aggregate_height(board) == 100 + + def test_no_holes(): - board = Board() - board.board = [ + + initBoard = [ + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], @@ -549,12 +619,17 @@ def test_no_holes(): [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], ] - assert ( - find_holes(board) == 0 - ), "Expected 0 holes" -def test_no_holes(): - board = Board() - board.board = [ + board = Tetris(initBoard) + assert find_holes(board) == 0 + + + +def test_24_holes(): + + initBoard = [ + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 1, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], @@ -576,6 +651,5 @@ def test_no_holes(): [0, 0, 0, 1, 1, 1, 0, 0, 0, 0], [0, 0, 0, 0, 0, 1, 0, 0, 0, 1], ] - assert ( - find_holes(board) == 24 - ), "Expected 24 holes" \ No newline at end of file + board = Tetris(initBoard) + assert find_holes(board) == 24, "Expected 24 holes" diff --git a/test/agents/test_heuristic_agent.py b/test/agents/test_heuristic_agent.py new file mode 100644 index 0000000..b89a954 --- /dev/null +++ b/test/agents/test_heuristic_agent.py @@ -0,0 +1,74 @@ +from src.agents.agent_factory import create_agent +from src.agents.heuristic_agent import HeuristicAgent +from src.game.tetris import Action, Tetris +from src.agents.heuristic_agent import HeuristicAgent +from src.game.block import Block + +def test_result_heuristic_agent(): + + initBoard = [ + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [1, 0, 0, 0, 0, 0, 0, 0, 0, 1], + [1, 1, 0, 0, 0, 0, 0, 0, 1, 1], + [1, 1, 1, 1, 1, 0, 0, 1, 1, 1], + [1, 1, 1, 1, 1, 0, 1, 1, 1, 1], + ] + + expected_board = [ + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [1, 0, 0, 0, 0, 0, 0, 0, 0, 1], + [1, 1, 0, 0, 0, 0, 2, 0, 1, 1], + [1, 1, 1, 1, 1, 2, 2, 1, 1, 1], + [1, 1, 1, 1, 1, 2, 1, 1, 1, 1], + ] + + block = Block(3, 0, 1) + board = Tetris(initBoard, block) + + agent: HeuristicAgent = create_agent("heuristic") + result = agent.result(board) + for i in range(len(result)): + board.doAction(result[i]) + print(result) + for row in board.board: + print(row) + print('\n') + assert (board.board == expected_board) + + #end_board = play_game(agent, game, 7) \ No newline at end of file diff --git a/test/game/test_actions.py b/test/game/test_actions.py index 4ae25c6..0305f55 100644 --- a/test/game/test_actions.py +++ b/test/game/test_actions.py @@ -1,17 +1,17 @@ -from src.game.board import Action, Board +from src.game.tetris import Action, Tetris from src.game.block import Block -def non_random_board(init_board=None, first_block=None, next_block=None) -> Board: - board: Board +def non_random_board(init_board=None, first_block=None, next_block=None) -> Tetris: + board: Tetris initial_block = Block(3, 0, 0) if first_block is not None: initial_block = first_block if init_board is not None: - board = Board(board=init_board, block=initial_block) + board = Tetris(board=init_board, block=initial_block) else: - board = Board(block=initial_block) + board = Tetris(block=initial_block) board.nextBlock = Block(0, 0, 6) if next_block is not None: board.nextBlock = next_block @@ -19,7 +19,7 @@ def non_random_board(init_board=None, first_block=None, next_block=None) -> Boar def test_move_down(): - board: Board = non_random_board() + board: Tetris = non_random_board() expected_block = board.block.copy() expected_block.moveDown() @@ -28,7 +28,7 @@ def test_move_down(): def test_move_left(): - board: Board = non_random_board() + board: Tetris = non_random_board() expected_block = board.block.copy() expected_block.moveLeft() @@ -37,10 +37,8 @@ def test_move_left(): def test_hard_drop(): - board: Board = non_random_board() + board: Tetris = non_random_board() expected_board = [ - [0, 1, 1, 0, 0, 0, 0, 0, 0, 0], - [0, 1, 1, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], @@ -55,19 +53,25 @@ def test_hard_drop(): [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], - [0, 0, 0, 0, 1, 0, 0, 0, 0, 0], - [0, 0, 0, 0, 1, 0, 0, 0, 0, 0], - [0, 0, 0, 0, 1, 0, 0, 0, 0, 0], - [0, 0, 0, 0, 1, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 1, 1, 1, 1, 0, 0, 0], ] board.doAction(Action.HARD_DROP) + for board_row, expected_row in zip(board.board, expected_board): assert board_row == expected_row def test_move_right(): - board: Board = non_random_board() + board: Tetris = non_random_board() expected_block = board.block.copy() expected_block.moveRight() @@ -76,7 +80,7 @@ def test_move_right(): def test_rotate_clockwise(): - board: Board = non_random_board() + board: Tetris = non_random_board() expected_block = board.block.copy() expected_block.rotateRight() @@ -85,7 +89,7 @@ def test_rotate_clockwise(): def test_rotate_counter_clockwise(): - board: Board = non_random_board() + board: Tetris = non_random_board() expected_block = board.block.copy() expected_block.rotateLeft() @@ -94,12 +98,15 @@ def test_rotate_counter_clockwise(): def test_try_to_move_block_out_of_bound_left(): - board: Board = non_random_board() + board: Tetris = non_random_board() expected_board = [ - [1, 0, 0, 0, 0, 0, 0, 0, 0, 0], - [1, 0, 0, 0, 0, 0, 0, 0, 0, 0], - [1, 0, 0, 0, 0, 0, 0, 0, 0, 0], - [1, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [1, 1, 1, 1, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], @@ -122,17 +129,19 @@ def test_try_to_move_block_out_of_bound_left(): board.doAction(Action.MOVE_LEFT) for board_row, expected_row in zip(board.board, expected_board): - print(len(board_row), len(expected_row)) assert board_row == expected_row def test_try_to_move_block_out_of_bound_right(): - board: Board = non_random_board() + board: Tetris = non_random_board() expected_board = [ - [0, 0, 0, 0, 0, 0, 0, 0, 0, 1], - [0, 0, 0, 0, 0, 0, 0, 0, 0, 1], - [0, 0, 0, 0, 0, 0, 0, 0, 0, 1], - [0, 0, 0, 0, 0, 0, 0, 0, 0, 1], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 1, 1, 1, 1], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], @@ -159,7 +168,7 @@ def test_try_to_move_block_out_of_bound_right(): def test_try_to_rotate_block_out_of_bound(): - board: Board + board: Tetris # TODO: CREATE THIS TEST test_try_to_rotate_block_out_of_bound pass @@ -185,6 +194,9 @@ def test_drop_block_on_top_of_another_block(): [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [1, 1, 1, 1, 1, 1, 1, 1, 1, 0], ] @@ -193,8 +205,6 @@ def test_drop_block_on_top_of_another_block(): board.printBoard() expected_board = [ - [0, 1, 1, 0, 0, 0, 0, 0, 0, 0], - [0, 1, 1, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], @@ -208,10 +218,15 @@ def test_drop_block_on_top_of_another_block(): [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], - [0, 0, 0, 0, 1, 0, 0, 0, 0, 0], - [0, 0, 0, 0, 1, 0, 0, 0, 0, 0], - [0, 0, 0, 0, 1, 0, 0, 0, 0, 0], - [0, 0, 0, 0, 1, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 1, 1, 1, 1, 0, 0, 0], [1, 1, 1, 1, 1, 1, 1, 1, 1, 0], ] board.doAction(Action.HARD_DROP) @@ -222,7 +237,10 @@ def test_drop_block_on_top_of_another_block(): def test_slide_left_block_on_top_of_another_block(): - innitBoard = [ + initBoard = [ + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], @@ -244,10 +262,8 @@ def test_slide_left_block_on_top_of_another_block(): [0, 0, 0, 1, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], ] - board: Board = non_random_board(innitBoard) + board: Tetris = non_random_board(initBoard) expected_board = [ - [0, 1, 1, 0, 0, 0, 0, 0, 0, 0], - [0, 1, 1, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], @@ -262,14 +278,17 @@ def test_slide_left_block_on_top_of_another_block(): [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], - [0, 0, 0, 1, 1, 0, 0, 0, 0, 0], - [0, 0, 0, 1, 1, 0, 0, 0, 0, 0], - [0, 0, 0, 1, 1, 0, 0, 0, 0, 0], - [0, 0, 0, 0, 1, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 1, 1, 1, 1, 0, 0, 0, 0], + [0, 0, 0, 1, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 1, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 1, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], ] - needed_downs = 16 - for _ in range(needed_downs): - board.doAction(Action.SOFT_DROP) + board.doAction(Action.HARD_DROP) board.printBoard() board.doAction(Action.MOVE_LEFT) board.doAction(Action.SOFT_DROP) @@ -298,15 +317,16 @@ def test_slide_right_block_on_top_of_another_block(): [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 1, 0, 0, 0, 0], [0, 0, 0, 0, 0, 1, 0, 0, 0, 0], [0, 0, 0, 0, 0, 1, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], ] - board: Board = non_random_board(initBoard, initBlock) + board: Tetris = non_random_board(initBoard, initBlock) expected_board = [ - [0, 1, 1, 0, 0, 0, 0, 0, 0, 0], - [0, 1, 1, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], @@ -319,8 +339,13 @@ def test_slide_right_block_on_top_of_another_block(): [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], - [0, 0, 0, 0, 0, 1, 0, 0, 0, 0], - [0, 0, 0, 0, 1, 1, 1, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 6, 0, 0, 0, 0], + [0, 0, 0, 0, 6, 6, 6, 0, 0, 0], [0, 0, 0, 0, 0, 1, 0, 0, 0, 0], [0, 0, 0, 0, 0, 1, 0, 0, 0, 0], [0, 0, 0, 0, 0, 1, 0, 0, 0, 0], @@ -353,6 +378,9 @@ def test_slide_right_block_on_under_another_block(): [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 1, 0, 0, 0, 0], [0, 0, 0, 0, 0, 1, 0, 0, 0, 0], [0, 0, 0, 0, 0, 1, 0, 0, 0, 0], @@ -378,10 +406,13 @@ def test_slide_right_block_on_under_another_block(): [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 1, 0, 0, 0, 0], [0, 0, 0, 0, 0, 1, 0, 0, 0, 0], - [0, 0, 0, 0, 1, 1, 0, 0, 0, 0], - [0, 0, 0, 1, 1, 1, 0, 0, 0, 0], + [0, 0, 0, 0, 6, 1, 0, 0, 0, 0], + [0, 0, 0, 6, 6, 6, 0, 0, 0, 0], ] board.doAction(Action.MOVE_LEFT) diff --git a/test/game/test_board.py b/test/game/test_board.py index 1de6c7a..885fb8e 100644 --- a/test/game/test_board.py +++ b/test/game/test_board.py @@ -1,41 +1,257 @@ from src.game.block import Block -from src.game.board import Action, Board, transition_model +from src.game.tetris import Action, Tetris, transition_model import copy def test_get_possible_boards_for_line(): i_block = Block(0, 3, 0) - board: Board = Board(block=i_block) + board: Tetris = Tetris(block=i_block) possible_boards = board.getPossibleBoards() + for board in possible_boards: + board.printBoard() assert isinstance(possible_boards, list) for move in possible_boards: - assert isinstance(move, Board) + assert isinstance(move, Tetris) - standing_up_right = 9 + standing_up_right = 10 laying_down_right = 7 assert len(possible_boards) == standing_up_right + laying_down_right def test_get_possible_moves_for_square(): first_block = Block(0, 3, 6) - board: Board = Board(block=first_block) + board: Tetris = Tetris(block=first_block) possible_moves = board.getPossibleBoards() assert isinstance(possible_moves, list) for move in possible_moves: - assert isinstance(move, Board) - - assert len(possible_moves) == 8 + assert isinstance(move, Tetris) + assert len(possible_moves) == 9 + +def test_get_possible_moves_complex(): + + innitBoard = [ + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 1, 1, 0, 0], + [0, 0, 0, 0, 0, 1, 1, 0, 0, 0], + [0, 0, 0, 0, 0, 1, 0, 0, 0, 0], + [0, 0, 0, 0, 1, 1, 0, 0, 0, 0], + [0, 0, 0, 0, 1, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 1, 0, 0, 0, 0, 0], + [1, 1, 1, 1, 1, 1, 0, 0, 1, 1], + ] + + innitBlock1 = Block(3, 0, 0) # I + innitBlock2 = Block(3, 0, 1) # Z + innitBlock3 = Block(3, 0, 2) # S + innitBlock4 = Block(3, 0, 3) # L + innitBlock5 = Block(3, 0, 4) # J + innitBlock6 = Block(3, 0, 5) # T + innitBlock7 = Block(3, 0, 6) # O + + + possibleBoard1 = [ + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 1, 1, 0, 0], + [0, 0, 0, 0, 0, 1, 1, 0, 0, 0], + [0, 0, 0, 0, 0, 1, 0, 0, 0, 0], + [0, 0, 0, 0, 1, 1, 1, 0, 0, 0], + [0, 0, 0, 0, 1, 0, 1, 0, 0, 0], + [0, 0, 0, 0, 1, 0, 1, 0, 0, 0], + [1, 1, 1, 1, 1, 1, 1, 0, 1, 1], + ] + possibleBoard2 = [ + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 1, 1, 0, 0], + [0, 0, 0, 0, 0, 1, 1, 0, 0, 0], + [0, 0, 0, 0, 0, 1, 0, 0, 0, 0], + [0, 0, 0, 0, 1, 1, 2, 0, 0, 0], + [0, 0, 0, 0, 1, 2, 2, 0, 0, 0], + [0, 0, 0, 0, 1, 2, 0, 0, 0, 0], + [1, 1, 1, 1, 1, 1, 0, 0, 1, 1], + ] + + possibleBoard3 = [ + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 1, 1, 0, 0], + [0, 0, 0, 0, 0, 1, 1, 0, 0, 0], + [0, 0, 0, 0, 0, 1, 0, 0, 0, 0], + [0, 0, 0, 0, 1, 1, 0, 0, 0, 0], + [0, 0, 0, 0, 1, 3, 0, 0, 0, 0], + [0, 0, 0, 0, 1, 3, 3, 0, 0, 0], + [1, 1, 1, 1, 1, 1, 3, 0, 1, 1], + ] + + possibleBoard4 = [ + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 1, 1, 0, 0], + [0, 0, 0, 0, 0, 1, 1, 0, 0, 0], + [0, 0, 0, 0, 0, 1, 0, 0, 0, 0], + [0, 0, 0, 0, 1, 1, 0, 0, 0, 0], + [0, 0, 0, 0, 1, 4, 4, 0, 0, 0], + [0, 0, 0, 0, 1, 0, 4, 0, 0, 0], + [1, 1, 1, 1, 1, 1, 4, 0, 1, 1], + ] + + possibleBoard5 = [ + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 1, 1, 0, 0], + [0, 0, 0, 0, 0, 1, 1, 0, 0, 0], + [0, 0, 0, 0, 0, 1, 0, 0, 0, 0], + [0, 0, 0, 0, 1, 1, 0, 0, 0, 0], + [0, 0, 0, 0, 1, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 1, 5, 5, 5, 0, 0], + [1, 1, 1, 1, 1, 1, 0, 5, 1, 1], + ] + + possibleBoard6 = [ + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 1, 1, 0, 0], + [0, 0, 0, 0, 0, 1, 1, 0, 0, 0], + [0, 0, 0, 0, 0, 1, 0, 0, 0, 0], + [0, 0, 0, 0, 1, 1, 0, 0, 0, 0], + [0, 0, 0, 0, 1, 0, 6, 0, 0, 0], + [0, 0, 0, 0, 1, 6, 6, 0, 0, 0], + [1, 1, 1, 1, 1, 1, 6, 0, 1, 1], + ] + + possibleBoard7 = [ + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 1, 1, 0, 0], + [0, 0, 0, 0, 0, 1, 1, 0, 0, 0], + [0, 0, 0, 0, 0, 1, 0, 0, 0, 0], + [0, 0, 0, 0, 1, 1, 0, 0, 0, 0], + [0, 0, 0, 0, 1, 7, 7, 0, 0, 0], + [0, 0, 0, 0, 1, 7, 7, 0, 0, 0], + [1, 1, 1, 1, 1, 1, 0, 0, 1, 1], + ] + + blocks = [innitBlock1, innitBlock2, innitBlock3, innitBlock4, innitBlock5, innitBlock6, innitBlock7] + possibleBoards = [possibleBoard1, possibleBoard2, possibleBoard3, possibleBoard4, possibleBoard5, possibleBoard6, possibleBoard7] + + for i in range(7): + # Construct the test scenario with the initial board and block + actualBoard = Tetris(innitBoard, blocks[i]) + possibleMoves = actualBoard.getPossibleBoards() + for x in possibleMoves: x.printBoard() + + # Construct the expected board + expectedBoard = Tetris() + expectedBoard.board = possibleBoards[i] + + # Check if possible moves contain the expected board + assert expectedBoard in possibleMoves def test_board_equal_for_the_same_object(): - board1 = Board() + 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: Board = Board() + board: Tetris = Tetris() board.board = [ [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], @@ -91,7 +307,7 @@ def test_clear_row(): def test_clear_rows(): - board: Board = Board() + board: Tetris = Tetris() board.board = [ [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [1, 1, 1, 1, 1, 1, 1, 1, 1, 1], @@ -147,7 +363,7 @@ def test_clear_rows(): def test_do_not_clear_not_full_row(): - board: Board = Board() + board: Tetris = Tetris() board.board = [ [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [1, 1, 1, 1, 1, 1, 1, 1, 0, 0], @@ -179,27 +395,29 @@ def test_do_not_clear_not_full_row(): def test_transition_model_for_no_transition(): - current_board: Board = Board() - target_board: Board = current_board + current_board: Tetris = Tetris() + target_board: Tetris = current_board actions = transition_model(current_board, target_board) assert isinstance(actions, list) - assert len(actions) == 0 + assert len(actions) == 1 def test_transition_model_x_direction(): - current_board: Board = Board() - target_board: Board = copy.deepcopy(current_board) + current_board: Tetris = Tetris() + target_board: Tetris = copy.deepcopy(current_board) action = Action.MOVE_RIGHT target_board.doAction(action) actions = transition_model(current_board, target_board) assert isinstance(actions, list) - assert len(actions) == 1 + assert action in actions + assert len(actions) == 2 # 1 for moving right, 1 for hard drop which is always appended def test_transition_model_complex_target(): - current_board: Board = Board() - target_board: Board = copy.deepcopy(current_board) + initial_block = Block(0, 3, 0) + current_board: Tetris = Tetris(None, initial_block) + target_board: Tetris = copy.deepcopy(current_board) actual_actions = [ Action.ROTATE_CLOCKWISE, Action.MOVE_RIGHT, @@ -217,8 +435,8 @@ def test_transition_model_complex_target(): def test_transition_model_left_movement(): - current_board: Board = Board() - target_board: Board = copy.deepcopy(current_board) + current_board: Tetris = Tetris() + target_board: Tetris = copy.deepcopy(current_board) actual_actions = [ Action.ROTATE_CLOCKWISE, Action.ROTATE_CLOCKWISE, @@ -236,8 +454,8 @@ def test_transition_model_left_movement(): def test_transition_model_execution(): - current_board: Board = Board() - target_board: Board = copy.deepcopy(current_board) + current_board: Tetris = Tetris() + target_board: Tetris = copy.deepcopy(current_board) actual_actions = [ Action.ROTATE_CLOCKWISE, Action.ROTATE_CLOCKWISE, @@ -254,8 +472,8 @@ def test_transition_model_execution(): def test_transition_model_execution_complex(): - current_board: Board = Board() - target_board: Board = copy.deepcopy(current_board) + current_board: Tetris = Tetris() + target_board: Tetris = copy.deepcopy(current_board) actual_actions = [ Action.ROTATE_CLOCKWISE, Action.MOVE_LEFT, @@ -274,8 +492,8 @@ def test_transition_model_execution_complex(): def test_transition_model_execution_of_invalid_move_sequence(): - current_board: Board = Board() - target_board: Board = copy.deepcopy(current_board) + current_board: Tetris = Tetris() + target_board: Tetris = copy.deepcopy(current_board) actual_actions = [Action.MOVE_LEFT] * 20 actual_actions += [Action.MOVE_RIGHT] * 20 actual_actions += [Action.HARD_DROP] @@ -286,3 +504,53 @@ def test_transition_model_execution_of_invalid_move_sequence(): for action in actions: current_board.doAction(action) assert current_board == target_board + +def test_transition_model_result_complex(): + + innitBoard = [ + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 1, 1, 0, 0], + [0, 0, 0, 0, 0, 1, 1, 0, 0, 0], + [0, 0, 0, 0, 0, 1, 0, 0, 0, 0], + [0, 0, 0, 0, 1, 1, 0, 0, 0, 0], + [0, 0, 0, 0, 1, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 1, 0, 0, 0, 0, 0], + [1, 1, 1, 1, 1, 1, 0, 0, 1, 1], + ] + + innitBlock1 = Block(3, 0, 0) # I + + current_board: Tetris = Tetris(innitBoard, innitBlock1) + target_board: Tetris = copy.deepcopy(current_board) + settup = [Action.ROTATE_CLOCKWISE] + for x in range(3): settup.append(Action.MOVE_RIGHT) + for x in range(18): settup.append(Action.SOFT_DROP) + for x in range(2): settup.append(Action.MOVE_LEFT) + settup.append(Action.HARD_DROP) + + for action in settup: + target_board.doAction(action) + + # Test if the actions from the transition model result in the target board + actions = transition_model(current_board, target_board) + for action in actions: + current_board.doAction(action) + + assert current_board == target_board + + \ No newline at end of file diff --git a/test/test_example.py b/test/test_example.py new file mode 100644 index 0000000..eb040c6 --- /dev/null +++ b/test/test_example.py @@ -0,0 +1,6 @@ +def inc(x): + return x + 1 + + +def test_answer(): + assert inc(3) == 4 \ No newline at end of file