From b6da87a89558dea4837df15328fa7f6953149707 Mon Sep 17 00:00:00 2001 From: Eduard Prokhorikhin Date: Fri, 12 Apr 2024 23:20:03 +0200 Subject: [PATCH 01/67] feat: changed Board class name to Tetris --- main.py | 4 +-- src/agents/agent.py | 6 ++--- src/agents/heuristic.py | 16 +++++------ src/agents/randomAgent.py | 4 +-- src/game/TetrisGameManager.py | 4 +-- src/game/{board.py => tetris.py} | 10 +++---- test/agents/test_heuristic.py | 44 +++++++++++++++--------------- test/game/test_actions.py | 32 +++++++++++----------- test/game/test_board.py | 46 ++++++++++++++++---------------- 9 files changed, 83 insertions(+), 83 deletions(-) rename src/game/{board.py => tetris.py} (98%) diff --git a/main.py b/main.py index fb2323f..975b0d6 100644 --- a/main.py +++ b/main.py @@ -1,8 +1,8 @@ from src.game.TetrisGameManager import TetrisGameManager -from src.game.board import Board +from src.game.tetris import Tetris if __name__ == "__main__": - board = Board() + board = Tetris() game = TetrisGameManager(board) game.startGame() diff --git a/src/agents/agent.py b/src/agents/agent.py index 65ad5cf..f7e9d24 100644 --- a/src/agents/agent.py +++ b/src/agents/agent.py @@ -6,7 +6,7 @@ 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.board import Action, Tetris class Agent(ABC): @@ -21,7 +21,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 +34,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. diff --git a/src/agents/heuristic.py b/src/agents/heuristic.py index ec961ab..86744ed 100644 --- a/src/agents/heuristic.py +++ b/src/agents/heuristic.py @@ -1,14 +1,14 @@ """ The heuristic module contains the heuristics used by the agents. """ -from src.game.board import Board +from src.game.board import Tetris -def utility(gameState: Board) -> int: +def utility(gameState: Tetris) -> int: """Returns the utility of the given game state.""" pass -def aggregate_heights(gameState: Board) -> int: +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): @@ -19,7 +19,7 @@ def aggregate_heights(gameState: Board) -> int: 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): @@ -30,7 +30,7 @@ def max_height(gameState: Board) -> int: return max(checkedList) -def lines_cleaned(gameState: Board) -> int: +def lines_cleaned(gameState: Tetris) -> int: """Retrurns the number of lines cleared.""" sum = 0 for row in gameState.board: @@ -39,7 +39,7 @@ def lines_cleaned(gameState: Board) -> int: 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 @@ -55,7 +55,7 @@ def bumpiness(gameState: Board) -> int: return total_bumpiness -def aggregate_height(gameState: Board) -> int: +def aggregate_height(gameState: Tetris) -> int: "Returns the sum of all column-heights" max_height = 20 total_height = 0 @@ -71,7 +71,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: diff --git a/src/agents/randomAgent.py b/src/agents/randomAgent.py index 44f2ba7..f3405e8 100644 --- a/src/agents/randomAgent.py +++ b/src/agents/randomAgent.py @@ -1,5 +1,5 @@ from src.agents.agent import Agent -from src.game.board import Action, Board, get_all_actions +from src.game.board import Action, Tetris, get_all_actions from random import choice @@ -7,7 +7,7 @@ class RandomAgent(Agent): """Random agent that selects a random move from the list of possible moves""" - def result(self, board: Board) -> Action: + def result(self, board: Tetris) -> Action: # TODO: Get all possible actions # TODO: Return a random action diff --git a/src/game/TetrisGameManager.py b/src/game/TetrisGameManager.py index 7bdb59b..4d1fd23 100644 --- a/src/game/TetrisGameManager.py +++ b/src/game/TetrisGameManager.py @@ -2,7 +2,7 @@ import time as t import sys -from src.game.board import Action, Board +from src.game.board import Action, Tetris baseScore = 100 @@ -22,7 +22,7 @@ class TetrisGameManager: 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)) diff --git a/src/game/board.py b/src/game/tetris.py similarity index 98% rename from src/game/board.py rename to src/game/tetris.py index 747251e..f72fa1e 100644 --- a/src/game/board.py +++ b/src/game/tetris.py @@ -34,7 +34,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. @@ -247,7 +247,7 @@ def _clearRow(self, rownumber: int): self.board = newMatrix self.rowsRemoved += 1 - def getPossibleBoards(self) -> list["Board"]: + def getPossibleBoards(self) -> list["Tetris"]: possibleMoves = [] # Number of rotations which gives unique block positions @@ -282,8 +282,8 @@ 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 @@ -308,7 +308,7 @@ def _checkCharacter(self, character) -> str: 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. diff --git a/test/agents/test_heuristic.py b/test/agents/test_heuristic.py index 47d5b8c..8cb7e64 100644 --- a/test/agents/test_heuristic.py +++ b/test/agents/test_heuristic.py @@ -1,9 +1,9 @@ -from src.game.board import Board +from src.game.board import Tetris from src.agents.heuristic import * def test_heuristic_height_aggregate_empty_board(): - 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], @@ -31,7 +31,7 @@ def test_heuristic_height_aggregate_empty_board(): def test_heuristic_aggregate_with_equal_heights(): - board = Board() + board = Tetris() board.board = [ [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], @@ -60,7 +60,7 @@ def test_heuristic_aggregate_with_equal_heights(): def test_heuristic_high_line_heights(): - 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], @@ -88,7 +88,7 @@ def test_heuristic_high_line_heights(): def test_heuristic_different_heights(): - 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], @@ -117,7 +117,7 @@ def test_heuristic_different_heights(): def test_max_height_empty_board(): - 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], @@ -144,7 +144,7 @@ def test_max_height_empty_board(): def test_max_height_equal_heights(): - board = Board() + board = Tetris() board.board = [ [1, 1, 1, 1, 1, 1, 1, 1, 1, 1], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], @@ -173,7 +173,7 @@ def test_max_height_equal_heights(): def test_max_height_takes_highest(): - board = Board() + board = Tetris() board.board = [ [1, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], @@ -201,7 +201,7 @@ def test_max_height_takes_highest(): ), "Expected max height of 20 for a single column with height 20" def test_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], @@ -228,7 +228,7 @@ def test_lines_cleared(): 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], @@ -255,7 +255,7 @@ def test_no_lines_cleared(): lines_cleaned(board) == 0 ) def test_twenty_lines_cleared(): - board = Board() + board = Tetris() board.board = [ [1, 1, 1, 1, 1, 1, 1, 1, 1, 1], [1, 1, 1, 1, 1, 1, 1, 1, 1, 1], @@ -282,7 +282,7 @@ def test_twenty_lines_cleared(): lines_cleaned(board) == 20 ) def test_ten_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], @@ -309,7 +309,7 @@ def test_ten_lines_cleared(): 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], @@ -337,7 +337,7 @@ def test_bumpiness_empty(): def test_bumpiness_five(): - 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], @@ -364,7 +364,7 @@ def test_bumpiness_five(): bumpiness(board) == 2 ) def test_bumpiness_nine(): - 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], @@ -391,7 +391,7 @@ def test_bumpiness_nine(): bumpiness(board) == 9 ) def test_bumpiness_with_holes(): - board = Board() + board = Tetris() board.board = [ [1, 1, 1, 1, 1, 1, 1, 1, 1, 1], [1, 0, 0, 0, 0, 0, 0, 0, 0, 0], @@ -418,7 +418,7 @@ def test_bumpiness_with_holes(): bumpiness(board) == 0 ) def test_bumpiness_40(): - board = Board() + board = Tetris() board.board = [ [1, 0, 1, 1, 1, 1, 1, 1, 1, 1], [1, 0, 0, 0, 0, 0, 0, 0, 0, 0], @@ -445,7 +445,7 @@ def test_bumpiness_40(): bumpiness(board) == 40 ) def test_aggregate_height_zero(): - 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], @@ -472,7 +472,7 @@ def test_aggregate_height_zero(): aggregate_height(board) == 0 ) def test_aggregate_height_full(): - board = Board() + board = Tetris() board.board = [ [1, 1, 1, 1, 1, 1, 1, 1, 1, 1], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], @@ -499,7 +499,7 @@ def test_aggregate_height_full(): aggregate_height(board) == 200 ) def test_aggregate_height_half(): - board = Board() + board = Tetris() board.board = [ [1, 1, 1, 1, 1, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], @@ -526,7 +526,7 @@ def test_aggregate_height_half(): aggregate_height(board) == 100 ) def test_no_holes(): - 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], @@ -553,7 +553,7 @@ def test_no_holes(): find_holes(board) == 0 ), "Expected 0 holes" def test_no_holes(): - board = Board() + board = Tetris() board.board = [ [0, 0, 0, 0, 0, 1, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], diff --git a/test/game/test_actions.py b/test/game/test_actions.py index 4ae25c6..b999962 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.board 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,7 +37,7 @@ 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], @@ -67,7 +67,7 @@ def test_hard_drop(): def test_move_right(): - board: Board = non_random_board() + board: Tetris = non_random_board() expected_block = board.block.copy() expected_block.moveRight() @@ -76,7 +76,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 +85,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,7 +94,7 @@ 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], @@ -127,7 +127,7 @@ def test_try_to_move_block_out_of_bound_left(): 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], @@ -159,7 +159,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 @@ -244,7 +244,7 @@ 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(innitBoard) expected_board = [ [0, 1, 1, 0, 0, 0, 0, 0, 0, 0], [0, 1, 1, 0, 0, 0, 0, 0, 0, 0], @@ -303,7 +303,7 @@ def test_slide_right_block_on_top_of_another_block(): [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], diff --git a/test/game/test_board.py b/test/game/test_board.py index 1de6c7a..82666c8 100644 --- a/test/game/test_board.py +++ b/test/game/test_board.py @@ -1,16 +1,16 @@ from src.game.block import Block -from src.game.board import Action, Board, transition_model +from src.game.board 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() assert isinstance(possible_boards, list) for move in possible_boards: - assert isinstance(move, Board) + assert isinstance(move, Tetris) standing_up_right = 9 laying_down_right = 7 @@ -19,23 +19,23 @@ def test_get_possible_boards_for_line(): 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 isinstance(move, Tetris) assert len(possible_moves) == 8 def test_board_equal_for_the_same_object(): - board1 = Board() + board1 = Tetris() 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 +91,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 +147,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,8 +179,8 @@ 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) @@ -188,8 +188,8 @@ def test_transition_model_for_no_transition(): 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) @@ -198,8 +198,8 @@ def test_transition_model_x_direction(): def test_transition_model_complex_target(): - 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_RIGHT, @@ -217,8 +217,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 +236,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 +254,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 +274,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] From 9d8624f2627375177d405cd3f3b6e029752bf2c6 Mon Sep 17 00:00:00 2001 From: Eduard Prokhorikhin Date: Sat, 13 Apr 2024 00:00:47 +0200 Subject: [PATCH 02/67] docs: made a agent design dokument --- docs/planning/agentDesign.md | 47 ++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 docs/planning/agentDesign.md 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. From 7b96ee390887a039384656ff8abfcb65ba657692 Mon Sep 17 00:00:00 2001 From: Sverre Nystad <89105607+SverreNystad@users.noreply.github.com> Date: Sat, 13 Apr 2024 01:42:43 +0200 Subject: [PATCH 03/67] fix: correct import path --- src/agents/agent.py | 2 +- src/agents/heuristic.py | 2 +- src/agents/randomAgent.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/agents/agent.py b/src/agents/agent.py index f7e9d24..5ec5c9e 100644 --- a/src/agents/agent.py +++ b/src/agents/agent.py @@ -6,7 +6,7 @@ class for all agents in the simulation. from abc import ABC, abstractmethod from typing import Any, Union -from src.game.board import Action, Tetris +from src.game.tetris import Action, Tetris class Agent(ABC): diff --git a/src/agents/heuristic.py b/src/agents/heuristic.py index 86744ed..ec6c6b2 100644 --- a/src/agents/heuristic.py +++ b/src/agents/heuristic.py @@ -1,6 +1,6 @@ """ The heuristic module contains the heuristics used by the agents. """ -from src.game.board import Tetris +from src.game.tetris import Tetris def utility(gameState: Tetris) -> int: diff --git a/src/agents/randomAgent.py b/src/agents/randomAgent.py index f3405e8..6c68d46 100644 --- a/src/agents/randomAgent.py +++ b/src/agents/randomAgent.py @@ -1,5 +1,5 @@ from src.agents.agent import Agent -from src.game.board import Action, Tetris, get_all_actions +from src.game.tetris import Action, Tetris, get_all_actions from random import choice From 104fce674ed186b21f15fed45d341183be4efaf7 Mon Sep 17 00:00:00 2001 From: Sverre Nystad <89105607+SverreNystad@users.noreply.github.com> Date: Sat, 13 Apr 2024 01:43:59 +0200 Subject: [PATCH 04/67] refactor: remove print statements --- src/game/tetris.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/game/tetris.py b/src/game/tetris.py index f72fa1e..628a4e3 100644 --- a/src/game/tetris.py +++ b/src/game/tetris.py @@ -147,11 +147,9 @@ def isValidBlockPosition(self, block: Block) -> bool: """ if self._outOfBounds(block): - print("[DEBUG] Out of bounds") return False if self._intersects(block): - print("[DEBUG] Intersects") return False if self.isGameOver(): From 17e30171ee95a1908baa8f7939e7500f850770e6 Mon Sep 17 00:00:00 2001 From: Sverre Nystad <89105607+SverreNystad@users.noreply.github.com> Date: Sat, 13 Apr 2024 01:45:03 +0200 Subject: [PATCH 05/67] refactor: change script name --- src/agents/agent_factory.py | 4 +++- src/agents/{randomAgent.py => random_agent.py} | 0 2 files changed, 3 insertions(+), 1 deletion(-) rename src/agents/{randomAgent.py => random_agent.py} (100%) diff --git a/src/agents/agent_factory.py b/src/agents/agent_factory.py index 5832bcb..0552a28 100644 --- a/src/agents/agent_factory.py +++ b/src/agents/agent_factory.py @@ -1,7 +1,9 @@ """ 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 + + def create_agent(agent_type: str) -> Agent: """ Create an agent of the specified type. """ diff --git a/src/agents/randomAgent.py b/src/agents/random_agent.py similarity index 100% rename from src/agents/randomAgent.py rename to src/agents/random_agent.py From 783637cdda5a3c4fe5cb7b93a226d92030ea7d08 Mon Sep 17 00:00:00 2001 From: Sverre Nystad <89105607+SverreNystad@users.noreply.github.com> Date: Sat, 13 Apr 2024 01:48:03 +0200 Subject: [PATCH 06/67] feat: add heuristicAgent --- main.py | 11 ++++++----- src/agents/agent_factory.py | 10 ++++++---- src/agents/heuristic_agent.py | 19 +++++++++++++++++++ 3 files changed, 31 insertions(+), 9 deletions(-) create mode 100644 src/agents/heuristic_agent.py diff --git a/main.py b/main.py index 975b0d6..0cef245 100644 --- a/main.py +++ b/main.py @@ -1,8 +1,9 @@ -from src.game.TetrisGameManager import TetrisGameManager from src.game.tetris import Tetris - +from src.agents.agent import Agent, play_game +from src.agents.agent_factory import create_agent if __name__ == "__main__": - board = Tetris() - game = TetrisGameManager(board) - game.startGame() + game = Tetris() + agent: Agent = create_agent("heuristic") + end_board = play_game(agent, game, 7) + print(f"There was {end_board.rowsRemoved} rows removed") diff --git a/src/agents/agent_factory.py b/src/agents/agent_factory.py index 0552a28..1d62e90 100644 --- a/src/agents/agent_factory.py +++ b/src/agents/agent_factory.py @@ -2,14 +2,16 @@ from src.agents.agent import Agent 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': + case "random": return RandomAgent() + case "heuristic": + return HeuristicAgent() case _: - raise ValueError(f'Unknown agent type: {agent_type}') \ No newline at end of file + raise ValueError(f"Unknown agent type: {agent_type}") diff --git a/src/agents/heuristic_agent.py b/src/agents/heuristic_agent.py new file mode 100644 index 0000000..34adbd0 --- /dev/null +++ b/src/agents/heuristic_agent.py @@ -0,0 +1,19 @@ +from src.agents.agent import Agent +from src.game.tetris import Action, Tetris, transition_model +from src.agents.heuristic import ( + find_holes, + aggregate_height, + max_height, + bumpiness, +) + + +class HeuristicAgent(Agent): + + def result(self, board: Tetris) -> Action: + # TODO: Get all possible boards + + # TODO: Check which board has the best outcome based on the heuristic + + # TODO: Find the actions needed to transform the current board to the new board + pass From a3a9e7eecbd72539cb533c82be25e8f754ef504e Mon Sep 17 00:00:00 2001 From: Sverre Nystad <89105607+SverreNystad@users.noreply.github.com> Date: Sat, 13 Apr 2024 01:49:11 +0200 Subject: [PATCH 07/67] fix: correct random agent tests --- test/agents/test_agent_factory.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) 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" From f82d98c78cfa59192910afb671b04496d89f4ca5 Mon Sep 17 00:00:00 2001 From: Sverre Nystad <89105607+SverreNystad@users.noreply.github.com> Date: Sat, 13 Apr 2024 01:50:29 +0200 Subject: [PATCH 08/67] fix: correct script import of tetris in the tests --- test/agents/test_heuristic.py | 84 +++++++++++++++++------------------ test/game/test_actions.py | 2 +- test/game/test_board.py | 2 +- 3 files changed, 43 insertions(+), 45 deletions(-) diff --git a/test/agents/test_heuristic.py b/test/agents/test_heuristic.py index 8cb7e64..cdd8981 100644 --- a/test/agents/test_heuristic.py +++ b/test/agents/test_heuristic.py @@ -1,4 +1,4 @@ -from src.game.board import Tetris +from src.game.tetris import Tetris from src.agents.heuristic import * @@ -199,7 +199,8 @@ def test_max_height_takes_highest(): assert ( max_height(board) == 20 ), "Expected max height of 20 for a single column with height 20" - + + def test_lines_cleared(): board = Tetris() board.board = [ @@ -224,9 +225,9 @@ 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 ) - + assert lines_cleaned(board) == 1 + + def test_no_lines_cleared(): board = Tetris() board.board = [ @@ -251,8 +252,8 @@ 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], ] - assert ( - lines_cleaned(board) == 0 ) + assert lines_cleaned(board) == 0 + def test_twenty_lines_cleared(): board = Tetris() @@ -278,9 +279,9 @@ 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 ) - + assert lines_cleaned(board) == 20 + + def test_ten_lines_cleared(): board = Tetris() board.board = [ @@ -305,8 +306,8 @@ 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 ) + assert lines_cleaned(board) == 10 + def test_bumpiness_empty(): board = Tetris() @@ -332,10 +333,9 @@ def test_bumpiness_empty(): [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], ] - assert ( - bumpiness(board) == 0 ) - - + assert bumpiness(board) == 0 + + def test_bumpiness_five(): board = Tetris() board.board = [ @@ -360,9 +360,9 @@ 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 ) - + assert bumpiness(board) == 2 + + def test_bumpiness_nine(): board = Tetris() board.board = [ @@ -387,9 +387,9 @@ 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 ) - + assert bumpiness(board) == 9 + + def test_bumpiness_with_holes(): board = Tetris() board.board = [ @@ -414,9 +414,9 @@ 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 ) - + assert bumpiness(board) == 0 + + def test_bumpiness_40(): board = Tetris() board.board = [ @@ -441,9 +441,9 @@ 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 ) - + assert bumpiness(board) == 40 + + def test_aggregate_height_zero(): board = Tetris() board.board = [ @@ -468,9 +468,9 @@ 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 ) - + assert aggregate_height(board) == 0 + + def test_aggregate_height_full(): board = Tetris() board.board = [ @@ -495,9 +495,9 @@ 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 ) - + assert aggregate_height(board) == 200 + + def test_aggregate_height_half(): board = Tetris() board.board = [ @@ -522,9 +522,9 @@ 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 ) - + assert aggregate_height(board) == 100 + + def test_no_holes(): board = Tetris() board.board = [ @@ -549,9 +549,9 @@ 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" + assert find_holes(board) == 0, "Expected 0 holes" + + def test_no_holes(): board = Tetris() board.board = [ @@ -576,6 +576,4 @@ 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 + assert find_holes(board) == 24, "Expected 24 holes" diff --git a/test/game/test_actions.py b/test/game/test_actions.py index b999962..1ceb5e4 100644 --- a/test/game/test_actions.py +++ b/test/game/test_actions.py @@ -1,4 +1,4 @@ -from src.game.board import Action, Tetris +from src.game.tetris import Action, Tetris from src.game.block import Block diff --git a/test/game/test_board.py b/test/game/test_board.py index 82666c8..8526747 100644 --- a/test/game/test_board.py +++ b/test/game/test_board.py @@ -1,5 +1,5 @@ from src.game.block import Block -from src.game.board import Action, Tetris, transition_model +from src.game.tetris import Action, Tetris, transition_model import copy From 1fa6f9cb2dfa63d6981d8d2566d80cd19fbc21a5 Mon Sep 17 00:00:00 2001 From: Eduard Prokhorikhin Date: Mon, 15 Apr 2024 16:15:37 +0200 Subject: [PATCH 09/67] fix: forgot to change an import --- src/game/TetrisGameManager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/game/TetrisGameManager.py b/src/game/TetrisGameManager.py index 4d1fd23..631f737 100644 --- a/src/game/TetrisGameManager.py +++ b/src/game/TetrisGameManager.py @@ -2,7 +2,7 @@ import time as t import sys -from src.game.board import Action, Tetris +from src.game.tetris import Action, Tetris baseScore = 100 From 8125f2afa75af882e39b54be90a1f8e3635564fd Mon Sep 17 00:00:00 2001 From: Eduard Prokhorikhin Date: Mon, 15 Apr 2024 17:06:19 +0200 Subject: [PATCH 10/67] fix: lines are now cleared correctly --- src/game/tetris.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/game/tetris.py b/src/game/tetris.py index 628a4e3..18f5963 100644 --- a/src/game/tetris.py +++ b/src/game/tetris.py @@ -232,7 +232,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 @@ -241,9 +241,10 @@ 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 = copy.deepcopy(self.board) def getPossibleBoards(self) -> list["Tetris"]: possibleMoves = [] From 32649d396b42d55e5c3304ef81062c0dbe6f8b0a Mon Sep 17 00:00:00 2001 From: Eduard Prokhorikhin Date: Mon, 15 Apr 2024 17:08:17 +0200 Subject: [PATCH 11/67] feat: transitioned input managmentm to pygame --- main.py | 14 ++++-- src/game/TetrisGameManager.py | 89 +++++++++++++---------------------- 2 files changed, 42 insertions(+), 61 deletions(-) diff --git a/main.py b/main.py index 0cef245..4c9f53f 100644 --- a/main.py +++ b/main.py @@ -1,9 +1,15 @@ 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 if __name__ == "__main__": - game = Tetris() - agent: Agent = create_agent("heuristic") - end_board = play_game(agent, game, 7) - print(f"There was {end_board.rowsRemoved} rows removed") + # game = Tetris() + # agent: Agent = create_agent("heuristic") + # end_board = play_game(agent, game, 7) + # print(f"There was {end_board.rowsRemoved} rows removed") + + board = Tetris() + manager = TetrisGameManager(board) + + manager.startGame() diff --git a/src/game/TetrisGameManager.py b/src/game/TetrisGameManager.py index 631f737..43facb7 100644 --- a/src/game/TetrisGameManager.py +++ b/src/game/TetrisGameManager.py @@ -1,4 +1,5 @@ -from pynput.keyboard import Key, Listener +import pygame +from pygame.locals import * import time as t import sys @@ -6,17 +7,7 @@ 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 - """ - - class TetrisGameManager: - currentPiece = None nextPiece = None updateTimer = 1 @@ -27,25 +18,6 @@ def __init__(self, board: Tetris): 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 +26,44 @@ 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((800, 1400)) # Create a dummy window + 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.inputHandling() 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 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() - - return True + if checkTime < newTime: + self.currentTime = newTime + # self.movePiece(Action.SOFT_DROP) def stopGame(self): - self.board.gameOver = True + pygame.quit() sys.exit() From 3ee4bc7207c1901df90758e20e9b1842606e4db2 Mon Sep 17 00:00:00 2001 From: Eduard Prokhorikhin Date: Mon, 15 Apr 2024 17:16:25 +0200 Subject: [PATCH 12/67] choire:fixed and removed some commnets --- src/game/TetrisGameManager.py | 2 +- src/game/tetris.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/game/TetrisGameManager.py b/src/game/TetrisGameManager.py index 43facb7..9578244 100644 --- a/src/game/TetrisGameManager.py +++ b/src/game/TetrisGameManager.py @@ -62,7 +62,7 @@ def checkTimer(self): newTime = int(round(t.time() * 1000)) if checkTime < newTime: self.currentTime = newTime - # self.movePiece(Action.SOFT_DROP) + self.movePiece(Action.SOFT_DROP) def stopGame(self): pygame.quit() diff --git a/src/game/tetris.py b/src/game/tetris.py index 18f5963..49e8c17 100644 --- a/src/game/tetris.py +++ b/src/game/tetris.py @@ -174,7 +174,6 @@ 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(): From ef1761c918ba37814089700a1679c4df67874e21 Mon Sep 17 00:00:00 2001 From: Eduard Prokhorikhin Date: Mon, 15 Apr 2024 17:27:04 +0200 Subject: [PATCH 13/67] fix:removed match statements for compatibility --- src/agents/agent_factory.py | 13 ++++++------- src/game/tetris.py | 30 +++++++++++++++--------------- 2 files changed, 21 insertions(+), 22 deletions(-) diff --git a/src/agents/agent_factory.py b/src/agents/agent_factory.py index 1d62e90..c8e71f9 100644 --- a/src/agents/agent_factory.py +++ b/src/agents/agent_factory.py @@ -8,10 +8,9 @@ def create_agent(agent_type: str) -> Agent: """Create an agent of the specified type.""" - match agent_type.lower(): - case "random": - return RandomAgent() - case "heuristic": - return HeuristicAgent() - case _: - raise ValueError(f"Unknown agent type: {agent_type}") + if agent_type.lower() == "random": + return RandomAgent() + elif agent_type.lower() == "heuristic": + return HeuristicAgent() + else: + raise ValueError(f"Unknown agent type: {agent_type}") diff --git a/src/game/tetris.py b/src/game/tetris.py index 49e8c17..6a65a61 100644 --- a/src/game/tetris.py +++ b/src/game/tetris.py @@ -99,23 +99,23 @@ def doAction(self, action: Action) -> None: # 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: + 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() - case Action.HARD_DROP: - while True: + elif action == Action.HARD_DROP: + while True: + if self.isValidBlockPosition(new_block): new_block.moveDown() - if not self.isValidBlockPosition(new_block): - new_block.moveUp() - break - case Action.SOFT_DROP: - new_block.moveDown() + else: + new_block.moveUp() + break + elif action == Action.SOFT_DROP: + new_block.moveDown() # Given the new block position, check if it is valid and update the board if self.isValidBlockPosition(new_block): From 4fd4f04a1ca06aa0358a00ebf4a8af6cdfa5c9b1 Mon Sep 17 00:00:00 2001 From: Jon Bergland Date: Mon, 15 Apr 2024 17:52:19 +0200 Subject: [PATCH 14/67] refactor: change all calls for board to calls for prevBoard --- src/agents/heuristic.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/agents/heuristic.py b/src/agents/heuristic.py index ec6c6b2..e6682c4 100644 --- a/src/agents/heuristic.py +++ b/src/agents/heuristic.py @@ -3,7 +3,9 @@ from src.game.tetris import Tetris -def utility(gameState: Tetris) -> int: +def utility(gameState: Tetris, aggregate_heights_weight: int, max_height_weight: int, + lines_cleared_weight: int) -> int: + """Returns the utility of the given game state.""" pass @@ -13,7 +15,7 @@ def aggregate_heights(gameState: Tetris) -> int: 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 gameState.prevBoard[i][j] > 0: if checkedList[j] == 0: checkedList[j] = gameState.ROWS - i return sum(checkedList) @@ -24,7 +26,7 @@ def max_height(gameState: Tetris) -> int: 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 gameState.prevBoard[i][j] > 0: if checkedList[j] == 0: checkedList[j] = gameState.ROWS - i return max(checkedList) @@ -33,7 +35,7 @@ def max_height(gameState: Tetris) -> int: def lines_cleaned(gameState: Tetris) -> int: """Retrurns the number of lines cleared.""" sum = 0 - for row in gameState.board: + for row in gameState.prevBoard: if all(cell == 1 for cell in row): sum += 1 return sum @@ -46,7 +48,7 @@ def bumpiness(gameState: Tetris) -> int: 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 @@ -62,7 +64,7 @@ def aggregate_height(gameState: Tetris) -> int: 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 @@ -84,9 +86,9 @@ def find_holes(gameState: Tetris) -> int: 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): + if (gameState.prevBoard[j][i] == 1) and (j < top_block): top_block = j - if (gameState.board[j][i] == 0) and (j > top_block): + if (gameState.prevBoard[j][i] == 0) and (j > top_block): holes += 1 return holes From 8cf7adf070c8de668cdd6d71bf914a5ef1b951c3 Mon Sep 17 00:00:00 2001 From: Jon Bergland Date: Mon, 15 Apr 2024 17:56:42 +0200 Subject: [PATCH 15/67] feat: implement utility-method --- src/agents/heuristic.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/agents/heuristic.py b/src/agents/heuristic.py index e6682c4..e7121fb 100644 --- a/src/agents/heuristic.py +++ b/src/agents/heuristic.py @@ -4,10 +4,17 @@ def utility(gameState: Tetris, aggregate_heights_weight: int, max_height_weight: int, - lines_cleared_weight: int) -> int: - + lines_cleared_weight: int, bumpiness_weight: int, holes_weight: int) -> int: """Returns the utility of the given game state.""" - pass + sum = 0 + + sum += aggregate_heights_weight * aggregate_heights(gameState) + sum += max_height_weight * max_height(gameState) + sum += lines_cleared_weight * lines_cleaned(gameState) + sum += bumpiness_weight * bumpiness(gameState) + sum += holes_weight * find_holes(gameState) + + return sum def aggregate_heights(gameState: Tetris) -> int: From 3220d71bcd71096358fe03b9d24cceea338a173e Mon Sep 17 00:00:00 2001 From: henrinha Date: Mon, 15 Apr 2024 18:01:07 +0200 Subject: [PATCH 16/67] feat: implement random agent --- src/agents/random_agent.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/agents/random_agent.py b/src/agents/random_agent.py index 6c68d46..baf0d9f 100644 --- a/src/agents/random_agent.py +++ b/src/agents/random_agent.py @@ -8,7 +8,9 @@ class RandomAgent(Agent): """Random agent that selects a random move from the list of possible moves""" def result(self, board: Tetris) -> Action: - # TODO: Get all possible actions - - # TODO: Return a random action - pass + + actions = get_all_actions() + + return choice(actions) + + From d01e087f72665b62b6f327f4019efb994afa2c24 Mon Sep 17 00:00:00 2001 From: Sindre Fossdal Date: Mon, 15 Apr 2024 18:11:50 +0200 Subject: [PATCH 17/67] Co-authored-by: Eduard Prokhorikhin Co-authored-by: Jon Bergland --- main.py | 14 +++++++------- src/agents/heuristic.py | 13 +++++++++++-- src/agents/heuristic_agent.py | 26 +++++++++++++++++--------- src/agents/random_agent.py | 6 ++++-- src/game/TetrisGameManager.py | 2 +- 5 files changed, 40 insertions(+), 21 deletions(-) diff --git a/main.py b/main.py index 4c9f53f..301e762 100644 --- a/main.py +++ b/main.py @@ -4,12 +4,12 @@ from src.agents.agent_factory import create_agent if __name__ == "__main__": - # game = Tetris() - # agent: Agent = create_agent("heuristic") - # end_board = play_game(agent, game, 7) - # print(f"There was {end_board.rowsRemoved} rows removed") + game = Tetris() + agent: Agent = create_agent("heuristic") + end_board = play_game(agent, game, 7) + print(f"There was {end_board.rowsRemoved} rows removed") - board = Tetris() - manager = TetrisGameManager(board) + # board = Tetris() + # manager = TetrisGameManager(board) - manager.startGame() + # manager.startGame() diff --git a/src/agents/heuristic.py b/src/agents/heuristic.py index ec6c6b2..4fddf38 100644 --- a/src/agents/heuristic.py +++ b/src/agents/heuristic.py @@ -3,9 +3,18 @@ from src.game.tetris import Tetris -def utility(gameState: Tetris) -> int: +def utility(gameState: Tetris, aggregate_heights_weight: int, max_height_weight: int, + lines_cleared_weight: int, bumpiness_weight: int, holes_weight: int) -> int: """Returns the utility of the given game state.""" - pass + sum = 0 + + sum += aggregate_heights_weight * aggregate_heights(gameState) + sum += max_height_weight * max_height(gameState) + sum += lines_cleared_weight * lines_cleaned(gameState) + sum += bumpiness_weight * bumpiness(gameState) + sum += holes_weight * find_holes(gameState) + + return sum def aggregate_heights(gameState: Tetris) -> int: diff --git a/src/agents/heuristic_agent.py b/src/agents/heuristic_agent.py index 34adbd0..e1f082f 100644 --- a/src/agents/heuristic_agent.py +++ b/src/agents/heuristic_agent.py @@ -1,19 +1,27 @@ from src.agents.agent import Agent from src.game.tetris import Action, Tetris, transition_model from src.agents.heuristic import ( - find_holes, - aggregate_height, - max_height, - bumpiness, + utility ) class HeuristicAgent(Agent): - def result(self, board: Tetris) -> Action: - # TODO: Get all possible boards + def result(self, board: Tetris) -> list[Action]: + # Get all possible boards + possible_boards = board.getPossibleBoards() - # TODO: Check which board has the best outcome based on the heuristic + best_board: Tetris + best_utility = 0 + # Check which board has the best outcome based on the heuristic + for board in possible_boards: + current_utility = utility(board, 1, 1, 1, 1,1) + + if current_utility > best_utility: + best_board = board + best_utility = current_utility - # TODO: Find the actions needed to transform the current board to the new board - pass + + # Find the actions needed to transform the current board to the new board + actions = transition_model(board, best_board) + return actions diff --git a/src/agents/random_agent.py b/src/agents/random_agent.py index 6c68d46..243a4e9 100644 --- a/src/agents/random_agent.py +++ b/src/agents/random_agent.py @@ -9,6 +9,8 @@ class RandomAgent(Agent): def result(self, board: Tetris) -> Action: # TODO: Get all possible actions - + possible_actions = get_all_actions() # TODO: Return a random action - pass + return choice(possible_actions) + + diff --git a/src/game/TetrisGameManager.py b/src/game/TetrisGameManager.py index 9578244..5e0aecd 100644 --- a/src/game/TetrisGameManager.py +++ b/src/game/TetrisGameManager.py @@ -27,7 +27,7 @@ def isGameOver(self): def startGame(self): pygame.init() - self.screen = pygame.display.set_mode((800, 1400)) # Create a dummy window + self.screen = pygame.display.set_mode((400, 800)) # Create a dummy window pygame.display.set_caption('Tetris') # Set window title clock = pygame.time.Clock() From 8a319e3532a29be7abe346af302732b59a58fd64 Mon Sep 17 00:00:00 2001 From: henrinha Date: Mon, 15 Apr 2024 18:01:07 +0200 Subject: [PATCH 18/67] feat: implement random agent --- src/agents/random_agent.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/agents/random_agent.py b/src/agents/random_agent.py index 6c68d46..baf0d9f 100644 --- a/src/agents/random_agent.py +++ b/src/agents/random_agent.py @@ -8,7 +8,9 @@ class RandomAgent(Agent): """Random agent that selects a random move from the list of possible moves""" def result(self, board: Tetris) -> Action: - # TODO: Get all possible actions - - # TODO: Return a random action - pass + + actions = get_all_actions() + + return choice(actions) + + From bd7bc636c721ca1e3dc21cd0a6e68a2cd5bd6ed0 Mon Sep 17 00:00:00 2001 From: henrinha Date: Mon, 15 Apr 2024 18:24:53 +0200 Subject: [PATCH 19/67] feat: made heuristic agent --- main.py | 14 +++++++------- src/agents/heuristic_agent.py | 22 ++++++++++++++++++---- 2 files changed, 25 insertions(+), 11 deletions(-) diff --git a/main.py b/main.py index 4c9f53f..301e762 100644 --- a/main.py +++ b/main.py @@ -4,12 +4,12 @@ from src.agents.agent_factory import create_agent if __name__ == "__main__": - # game = Tetris() - # agent: Agent = create_agent("heuristic") - # end_board = play_game(agent, game, 7) - # print(f"There was {end_board.rowsRemoved} rows removed") + game = Tetris() + agent: Agent = create_agent("heuristic") + end_board = play_game(agent, game, 7) + print(f"There was {end_board.rowsRemoved} rows removed") - board = Tetris() - manager = TetrisGameManager(board) + # board = Tetris() + # manager = TetrisGameManager(board) - manager.startGame() + # manager.startGame() diff --git a/src/agents/heuristic_agent.py b/src/agents/heuristic_agent.py index 34adbd0..a92da02 100644 --- a/src/agents/heuristic_agent.py +++ b/src/agents/heuristic_agent.py @@ -1,6 +1,7 @@ from src.agents.agent import Agent -from src.game.tetris import Action, Tetris, transition_model +from src.game.tetris import Action, Tetris, transition_model, get_all_actions from src.agents.heuristic import ( + utility, find_holes, aggregate_height, max_height, @@ -10,10 +11,23 @@ class HeuristicAgent(Agent): - def result(self, board: Tetris) -> Action: - # TODO: Get all possible boards + def result(self, board: Tetris) -> list[Action]: + + all_posible_boards = board.getPossibleBoards() + best_board: Tetris + for possible_board in all_posible_boards: + best_score = float("-inf") + + board_utility = utility(possible_board, 1,1,1,1,1) + if board_utility > best_board: + best_board = possible_board + best_score = board_utility + + + + # for alle mulige trekk, sjekk heurstikk med utility og velg den beste. # TODO: Check which board has the best outcome based on the heuristic # TODO: Find the actions needed to transform the current board to the new board - pass + return transition_model(board, best_board) From a57e8a93cacff7cfdda91523b072c4f978c02369 Mon Sep 17 00:00:00 2001 From: oystkva Date: Mon, 15 Apr 2024 20:00:18 +0200 Subject: [PATCH 20/67] fix: :arrow_down: Double hard drop fixed --- src/game/TetrisGameManager.py | 2 +- src/game/tetris.py | 12 ++++-------- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/src/game/TetrisGameManager.py b/src/game/TetrisGameManager.py index 9578244..47a6795 100644 --- a/src/game/TetrisGameManager.py +++ b/src/game/TetrisGameManager.py @@ -27,7 +27,7 @@ def isGameOver(self): def startGame(self): pygame.init() - self.screen = pygame.display.set_mode((800, 1400)) # Create a dummy window + self.screen = pygame.display.set_mode((80, 140)) # Create a dummy window pygame.display.set_caption('Tetris') # Set window title clock = pygame.time.Clock() diff --git a/src/game/tetris.py b/src/game/tetris.py index 6a65a61..a5a23cc 100644 --- a/src/game/tetris.py +++ b/src/game/tetris.py @@ -108,12 +108,9 @@ def doAction(self, action: Action) -> None: elif action == Action.ROTATE_COUNTERCLOCKWISE: new_block.rotateLeft() elif action == Action.HARD_DROP: - while True: - if self.isValidBlockPosition(new_block): - new_block.moveDown() - else: - new_block.moveUp() - break + while self.isValidBlockPosition(new_block): + new_block.moveDown() + new_block.moveUp() elif action == Action.SOFT_DROP: new_block.moveDown() @@ -125,8 +122,7 @@ def doAction(self, action: Action) -> None: # 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 + and (action == Action.SOFT_DROP or action == Action.HARD_DROP) ): self._placeBlock() self._checkGameOver() From d09624711ba9e9ff3e43503f1766b9dad6abc2e1 Mon Sep 17 00:00:00 2001 From: Sindre Fossdal Date: Mon, 15 Apr 2024 20:01:47 +0200 Subject: [PATCH 21/67] Co-authored-by: Jon Bergland --- main.py | 17 ++++++++++-- src/agents/agent.py | 2 +- src/agents/heuristic.py | 14 +++++----- src/agents/heuristic_agent.py | 16 ++++++----- src/game/tetris.py | 6 +++-- test/agents/test_heuristic_agent.py | 41 +++++++++++++++++++++++++++++ 6 files changed, 78 insertions(+), 18 deletions(-) create mode 100644 test/agents/test_heuristic_agent.py diff --git a/main.py b/main.py index 301e762..87fb9e2 100644 --- a/main.py +++ b/main.py @@ -2,12 +2,25 @@ 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 +) if __name__ == "__main__": game = Tetris() agent: Agent = create_agent("heuristic") - end_board = play_game(agent, game, 7) - print(f"There was {end_board.rowsRemoved} rows removed") + 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) diff --git a/src/agents/agent.py b/src/agents/agent.py index 5ec5c9e..951a5f4 100644 --- a/src/agents/agent.py +++ b/src/agents/agent.py @@ -58,6 +58,6 @@ def play_game(agent: Agent, board: Tetris, actions_per_drop: int = 1) -> Tetris: board.doAction(result) # Advance the game by one frame board.doAction(Action.SOFT_DROP) - board.printBoard() + #board.printBoard() return board diff --git a/src/agents/heuristic.py b/src/agents/heuristic.py index 4fddf38..e7121fb 100644 --- a/src/agents/heuristic.py +++ b/src/agents/heuristic.py @@ -22,7 +22,7 @@ def aggregate_heights(gameState: Tetris) -> int: 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 gameState.prevBoard[i][j] > 0: if checkedList[j] == 0: checkedList[j] = gameState.ROWS - i return sum(checkedList) @@ -33,7 +33,7 @@ def max_height(gameState: Tetris) -> int: 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 gameState.prevBoard[i][j] > 0: if checkedList[j] == 0: checkedList[j] = gameState.ROWS - i return max(checkedList) @@ -42,7 +42,7 @@ def max_height(gameState: Tetris) -> int: def lines_cleaned(gameState: Tetris) -> int: """Retrurns the number of lines cleared.""" sum = 0 - for row in gameState.board: + for row in gameState.prevBoard: if all(cell == 1 for cell in row): sum += 1 return sum @@ -55,7 +55,7 @@ def bumpiness(gameState: Tetris) -> int: 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 +71,7 @@ def aggregate_height(gameState: Tetris) -> int: 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 @@ -93,9 +93,9 @@ def find_holes(gameState: Tetris) -> int: 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): + if (gameState.prevBoard[j][i] == 1) and (j < top_block): top_block = j - if (gameState.board[j][i] == 0) and (j > top_block): + if (gameState.prevBoard[j][i] == 0) and (j > top_block): holes += 1 return holes diff --git a/src/agents/heuristic_agent.py b/src/agents/heuristic_agent.py index e1f082f..726c38f 100644 --- a/src/agents/heuristic_agent.py +++ b/src/agents/heuristic_agent.py @@ -12,16 +12,20 @@ def result(self, board: Tetris) -> list[Action]: possible_boards = board.getPossibleBoards() best_board: Tetris - best_utility = 0 + best_utility = float("-inf") # Check which board has the best outcome based on the heuristic - for board in possible_boards: - current_utility = utility(board, 1, 1, 1, 1,1) + for boards in possible_boards: + current_utility = utility(boards, -0.5, -1.2, 2, -0.3,-0.6) if current_utility > best_utility: - best_board = board + best_board = boards best_utility = current_utility # Find the actions needed to transform the current board to the new board - actions = transition_model(board, best_board) - return actions + actions = [] + try: + actions = transition_model(board, best_board) + return actions + except: + return actions diff --git a/src/game/tetris.py b/src/game/tetris.py index 6a65a61..488105a 100644 --- a/src/game/tetris.py +++ b/src/game/tetris.py @@ -201,6 +201,7 @@ def _placeBlock(self): self.board[i + self.block.y][ j + self.block.x ] = 1 # self.block.color + def _shiftToNewBlock(self): """Places the current block on the board and sets the next block as the current block""" @@ -258,12 +259,12 @@ def getPossibleBoards(self) -> list["Tetris"]: rotationBoard = copy.deepcopy(self) for _ in range(rotations): - for column in range(self.COLUMNS): + for column in range(0, self.COLUMNS): moveBoard = copy.deepcopy(rotationBoard) # Calibrate the to the left toLeft = moveBoard.block.x - for _ in range(toLeft): + for _ in range(toLeft + 1): moveBoard.doAction(Action.MOVE_LEFT) # Move the block to the correct column for _ in range(column): @@ -321,6 +322,7 @@ def transition_model(current_state: Tetris, target_state: Tetris) -> list[Action actions = [] if current_state == target_state: + actions.append(Action.SOFT_DROP) print("No transition needed") return actions diff --git a/test/agents/test_heuristic_agent.py b/test/agents/test_heuristic_agent.py new file mode 100644 index 0000000..59638eb --- /dev/null +++ b/test/agents/test_heuristic_agent.py @@ -0,0 +1,41 @@ +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.agent import Agent, play_game +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], + [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], + ] + block=Block(3,0,1) + board = Tetris(initBoard, block) + + agent: Agent = create_agent("heuristic") + possible_moves = board.getPossibleBoards() + result = agent.result(board) + print(result) + assert [] == result + + #end_board = play_game(agent, game, 7) \ No newline at end of file From 5cc1a4a43640463bf976fcffea12db5c3f245ea5 Mon Sep 17 00:00:00 2001 From: Maia Austigard Date: Mon, 15 Apr 2024 20:01:57 +0200 Subject: [PATCH 22/67] feat: create heuristic agent --- main.py | 11 +++-- src/agents/agent_factory.py | 1 + src/agents/heuristic_agent.py | 18 ++++--- src/agents/heuristic_trainer.py | 85 +++++++++++++++++++++++++++++++++ 4 files changed, 104 insertions(+), 11 deletions(-) create mode 100644 src/agents/heuristic_trainer.py diff --git a/main.py b/main.py index 301e762..f417bcf 100644 --- a/main.py +++ b/main.py @@ -2,14 +2,17 @@ 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_trainer import train if __name__ == "__main__": - game = Tetris() - agent: Agent = create_agent("heuristic") - end_board = play_game(agent, game, 7) - print(f"There was {end_board.rowsRemoved} rows removed") + # game = Tetris() + # agent: Agent = create_agent("heuristic") + # end_board = play_game(agent, game, 7) + # print(f"There was {end_board.rowsRemoved} rows removed") # board = Tetris() # manager = TetrisGameManager(board) # manager.startGame() + + train() diff --git a/src/agents/agent_factory.py b/src/agents/agent_factory.py index c8e71f9..8a056e4 100644 --- a/src/agents/agent_factory.py +++ b/src/agents/agent_factory.py @@ -11,6 +11,7 @@ def create_agent(agent_type: str) -> Agent: 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/heuristic_agent.py b/src/agents/heuristic_agent.py index a92da02..a5de1d2 100644 --- a/src/agents/heuristic_agent.py +++ b/src/agents/heuristic_agent.py @@ -11,18 +11,21 @@ class HeuristicAgent(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_posible_boards = board.getPossibleBoards() + + all_possible_boards = board.getPossibleBoards() best_board: Tetris - for possible_board in all_posible_boards: - best_score = float("-inf") + best_score = float("-inf") + for possible_board in all_possible_boards: - board_utility = utility(possible_board, 1,1,1,1,1) - if board_utility > best_board: + 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 - + # for alle mulige trekk, sjekk heurstikk med utility og velg den beste. @@ -31,3 +34,4 @@ def result(self, board: Tetris) -> list[Action]: # TODO: Find the actions needed to transform the current board to the new board return transition_model(board, best_board) + diff --git a/src/agents/heuristic_trainer.py b/src/agents/heuristic_trainer.py new file mode 100644 index 0000000..2772524 --- /dev/null +++ b/src/agents/heuristic_trainer.py @@ -0,0 +1,85 @@ +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 import HeuristicAgent + + +def train(): + agents = _create_heuristic_agents(10) + best_agent = None + best_agent_lines_cleared = float("-inf") + + print(len(agents)) + + for agent in agents: + game = Tetris() + end_state = play_game(agent, game) + print("[INFO] new agent train") + if end_state.rowsRemoved > best_agent_lines_cleared: + 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=True) + + +def _create_heuristic_agents(num_agents: int): + agents = [HeuristicAgent(create_random_hyperparameters()) for _ in range(num_agents)] + return agents + + +def create_random_hyperparameters(): + return [random.uniform(-1, 0), #aggregate_heights_weight + random.uniform(-1, 0), #max_height_weight + random.uniform(0, 1), #lines_cleared_weight + random.uniform(-1, 0), #bumpiness_weight + random.uniform(-1, 0) #holes_weight + ] + +def play_game(agent: Agent, board: Tetris, actions_per_drop: int = 10, max_moves: int = 1000, 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 + From 216fd653e00683cbedc9fe1be75f9cb29801a260 Mon Sep 17 00:00:00 2001 From: Sverre Nystad <89105607+SverreNystad@users.noreply.github.com> Date: Mon, 15 Apr 2024 23:16:40 +0200 Subject: [PATCH 23/67] refactor: clean code in heuristic_agent.py --- src/agents/heuristic_agent.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/agents/heuristic_agent.py b/src/agents/heuristic_agent.py index a5de1d2..4624107 100644 --- a/src/agents/heuristic_agent.py +++ b/src/agents/heuristic_agent.py @@ -20,18 +20,10 @@ def result(self, board: Tetris) -> list[Action]: 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 - - - # for alle mulige trekk, sjekk heurstikk med utility og velg den beste. - - # TODO: Check which board has the best outcome based on the heuristic - - # TODO: Find the actions needed to transform the current board to the new board return transition_model(board, best_board) From ccbb41cae0ff3a77378dcbf69942a316f6d1d6ab Mon Sep 17 00:00:00 2001 From: henrinha Date: Tue, 16 Apr 2024 17:24:18 +0200 Subject: [PATCH 24/67] feat: add methods for training agent --- src/agents/heuristic_trainer.py | 95 +++++++++++++++++++++++++++++---- 1 file changed, 86 insertions(+), 9 deletions(-) diff --git a/src/agents/heuristic_trainer.py b/src/agents/heuristic_trainer.py index 2772524..700f126 100644 --- a/src/agents/heuristic_trainer.py +++ b/src/agents/heuristic_trainer.py @@ -10,43 +10,72 @@ ) from src.agents.heuristic_agent import HeuristicAgent +#kan visualisere lines cleared hvordan den bederer seg over tid. def train(): - agents = _create_heuristic_agents(10) + current_itteration = 0 + agents = _create_heuristic_agents(500) + max_itterations = len(agents) best_agent = None - best_agent_lines_cleared = float("-inf") + best_agent_lines_cleared = 10 + min_lines_cleared = 8 + + hyperparameters_seed = [-0.7161870466697167, -1.0206888263763436, 1.1110841067034372, -0.8480056575381625, -0.09805070564448792] + epsilon = 0.001 + learning_rate = 0.01 print(len(agents)) for agent in agents: game = Tetris() - end_state = play_game(agent, game) - print("[INFO] new agent train") + 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=True) + end_state = play_game(best_agent, game, shall_render=False) + def _create_heuristic_agents(num_agents: int): agents = [HeuristicAgent(create_random_hyperparameters()) for _ in range(num_agents)] return agents +def _create_heuristic_agents_hyper(hyperparameters): + return HeuristicAgent(hyperparameters) + def create_random_hyperparameters(): return [random.uniform(-1, 0), #aggregate_heights_weight - random.uniform(-1, 0), #max_height_weight - random.uniform(0, 1), #lines_cleared_weight + random.uniform(-3, 0), #max_height_weight + random.uniform(0, 10), #lines_cleared_weight random.uniform(-1, 0), #bumpiness_weight - random.uniform(-1, 0) #holes_weight + random.uniform(-2, 0) #holes_weight ] -def play_game(agent: Agent, board: Tetris, actions_per_drop: int = 10, max_moves: int = 1000, shall_render = False) -> Tetris: +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. @@ -83,3 +112,51 @@ def play_game(agent: Agent, board: Tetris, actions_per_drop: int = 10, max_moves 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}") + + From 0d64422b22a47803380e8b47094ea9d218ca3759 Mon Sep 17 00:00:00 2001 From: Eduard Prokhorikhin Date: Tue, 16 Apr 2024 17:57:55 +0200 Subject: [PATCH 25/67] fix: added Henrik's implementation of heuristick agent as separete file and class --- src/agents/heuristic_agent_Henrik.py | 29 ++++++++++++++++++++++++++++ src/agents/heuristic_trainer.py | 6 +++--- 2 files changed, 32 insertions(+), 3 deletions(-) create mode 100644 src/agents/heuristic_agent_Henrik.py 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 index 700f126..215574c 100644 --- a/src/agents/heuristic_trainer.py +++ b/src/agents/heuristic_trainer.py @@ -8,7 +8,7 @@ max_height, bumpiness, ) -from src.agents.heuristic_agent import HeuristicAgent +from src.agents.heuristic_agent_Henrik import HeuristicAgentHenrik #kan visualisere lines cleared hvordan den bederer seg over tid. @@ -60,11 +60,11 @@ def train(): def _create_heuristic_agents(num_agents: int): - agents = [HeuristicAgent(create_random_hyperparameters()) for _ in range(num_agents)] + agents = [HeuristicAgentHenrik(create_random_hyperparameters()) for _ in range(num_agents)] return agents def _create_heuristic_agents_hyper(hyperparameters): - return HeuristicAgent(hyperparameters) + return HeuristicAgentHenrik(hyperparameters) def create_random_hyperparameters(): From ee6d68c09c5a9a5d79dd7b6564db1d230cbc6f95 Mon Sep 17 00:00:00 2001 From: Sindre Fossdal Date: Tue, 16 Apr 2024 18:01:53 +0200 Subject: [PATCH 26/67] test: fixed heurestic tests --- test/agents/test_heuristic.py | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/test/agents/test_heuristic.py b/test/agents/test_heuristic.py index cdd8981..96c40b7 100644 --- a/test/agents/test_heuristic.py +++ b/test/agents/test_heuristic.py @@ -3,8 +3,8 @@ def test_heuristic_height_aggregate_empty_board(): - board = Tetris() - 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], @@ -26,14 +26,14 @@ 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], ] - + board = Tetris(initBoard) assert aggregate_heights(board) == 0 def test_heuristic_aggregate_with_equal_heights(): - board = Tetris() + - 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], @@ -55,13 +55,14 @@ 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 def test_heuristic_high_line_heights(): - board = Tetris() - 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], @@ -83,13 +84,14 @@ 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 def test_heuristic_different_heights(): - board = Tetris() - 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], @@ -111,7 +113,7 @@ 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 From c5e67f92de07778c3ffb9eb12bdbe631a0df07a9 Mon Sep 17 00:00:00 2001 From: henrinha Date: Tue, 16 Apr 2024 18:06:16 +0200 Subject: [PATCH 27/67] refactor: changed varible-symbol in for loops --- src/agents/heuristic.py | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/src/agents/heuristic.py b/src/agents/heuristic.py index e7121fb..26eea48 100644 --- a/src/agents/heuristic.py +++ b/src/agents/heuristic.py @@ -17,25 +17,25 @@ def utility(gameState: Tetris, aggregate_heights_weight: int, max_height_weight: return sum -def aggregate_heights(gameState: Tetris) -> int: +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.prevBoard[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: 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.prevBoard[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) @@ -90,12 +90,12 @@ def find_holes(gameState: Tetris) -> int: The heuristic value """ holes = 0 - for i in range(gameState.COLUMNS): + for column in range(gameState.COLUMNS): top_block = 20 - for j in range(gameState.ROWS): - if (gameState.prevBoard[j][i] == 1) and (j < top_block): - top_block = j - if (gameState.prevBoard[j][i] == 0) and (j > top_block): + 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 From 40c449e1a22199f983287d81eec463bcb0932966 Mon Sep 17 00:00:00 2001 From: Sindre Fossdal Date: Tue, 16 Apr 2024 18:29:41 +0200 Subject: [PATCH 28/67] test: fixed heuristic tests --- test/agents/test_heuristic.py | 48 +++++++++++++++++++++-------------- 1 file changed, 29 insertions(+), 19 deletions(-) diff --git a/test/agents/test_heuristic.py b/test/agents/test_heuristic.py index 96c40b7..4236afc 100644 --- a/test/agents/test_heuristic.py +++ b/test/agents/test_heuristic.py @@ -339,8 +339,8 @@ def test_bumpiness_empty(): def test_bumpiness_five(): - board = Tetris() - 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], @@ -362,12 +362,13 @@ def test_bumpiness_five(): [0, 0, 0, 0, 0, 0, 0, 0, 1, 0], [1, 1, 1, 1, 1, 1, 1, 1, 1, 1], ] + board = Tetris(initBoard) assert bumpiness(board) == 2 def test_bumpiness_nine(): - board = Tetris() - 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], @@ -389,12 +390,13 @@ def test_bumpiness_nine(): [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [1, 0, 1, 0, 1, 0, 1, 0, 1, 0], ] + board = Tetris(initBoard) assert bumpiness(board) == 9 def test_bumpiness_with_holes(): - board = Tetris() - board.board = [ + + initBoard = [ [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], @@ -416,12 +418,13 @@ 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], ] + board = Tetris(initBoard) assert bumpiness(board) == 0 def test_bumpiness_40(): - board = Tetris() - board.board = [ + + initBoard = [ [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], @@ -443,12 +446,13 @@ def test_bumpiness_40(): [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [1, 0, 1, 0, 1, 0, 1, 0, 1, 0], ] + board = Tetris(initBoard) assert bumpiness(board) == 40 def test_aggregate_height_zero(): - board = Tetris() - 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], @@ -470,12 +474,13 @@ 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], ] + board = Tetris(initBoard) assert aggregate_height(board) == 0 def test_aggregate_height_full(): - board = Tetris() - board.board = [ + + initBoard = [ [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], @@ -497,12 +502,13 @@ 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], ] + board = Tetris(initBoard) assert aggregate_height(board) == 200 def test_aggregate_height_half(): - board = Tetris() - board.board = [ + + initBoard = [ [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], @@ -524,12 +530,13 @@ 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], ] + board = Tetris(initBoard) assert aggregate_height(board) == 100 def test_no_holes(): - board = Tetris() - 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], @@ -551,12 +558,14 @@ def test_no_holes(): [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], ] + board = Tetris(initBoard) assert find_holes(board) == 0, "Expected 0 holes" -def test_no_holes(): - board = Tetris() - board.board = [ + +def test_24_holes(): + + initBoard = [ [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], @@ -578,4 +587,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], ] + board = Tetris(initBoard) assert find_holes(board) == 24, "Expected 24 holes" From 007eb89314c9de1241f8e9dcc44d5f3a76e5432e Mon Sep 17 00:00:00 2001 From: Maia Austigard Date: Tue, 16 Apr 2024 18:30:03 +0200 Subject: [PATCH 29/67] test: change initialization of board --- test/agents/test_heuristic.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/test/agents/test_heuristic.py b/test/agents/test_heuristic.py index 96c40b7..3322f80 100644 --- a/test/agents/test_heuristic.py +++ b/test/agents/test_heuristic.py @@ -119,8 +119,7 @@ def test_heuristic_different_heights(): def test_max_height_empty_board(): - board = Tetris() - 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], @@ -142,12 +141,12 @@ 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], ] + board = Tetris(initBoard) assert max_height(board) == 0, "Expected max height of 0 for an empty board" def test_max_height_equal_heights(): - board = Tetris() - board.board = [ + initBoard = [ [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], @@ -169,14 +168,14 @@ 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 ), "Expected max height of 20 for a board with equal heights" def test_max_height_takes_highest(): - board = Tetris() - board.board = [ + initBoard = [ [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], @@ -198,14 +197,14 @@ 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 ), "Expected max height of 20 for a single column with height 20" def test_lines_cleared(): - board = Tetris() - 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], @@ -227,6 +226,7 @@ def test_lines_cleared(): [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [1, 1, 1, 1, 1, 1, 1, 1, 1, 1], ] + board = Tetris(initBoard) assert lines_cleaned(board) == 1 @@ -258,8 +258,7 @@ def test_no_lines_cleared(): def test_twenty_lines_cleared(): - board = Tetris() - board.board = [ + initBoard = [ [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], @@ -281,12 +280,12 @@ 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], ] + board = Tetris(initBoard) assert lines_cleaned(board) == 20 def test_ten_lines_cleared(): - board = Tetris() - 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], @@ -308,6 +307,7 @@ 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], ] + board = Tetris(initBoard) assert lines_cleaned(board) == 10 From 671801eb1b4f2902763e586a3591694f8fe7303d Mon Sep 17 00:00:00 2001 From: Eduard Prokhorikhin Date: Tue, 16 Apr 2024 18:42:16 +0200 Subject: [PATCH 30/67] feat: settup for genetic alg agent --- src/agents/geneticAlgAgent.py | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 src/agents/geneticAlgAgent.py diff --git a/src/agents/geneticAlgAgent.py b/src/agents/geneticAlgAgent.py new file mode 100644 index 0000000..83f61a8 --- /dev/null +++ b/src/agents/geneticAlgAgent.py @@ -0,0 +1,7 @@ +# 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 \ No newline at end of file From 9933b9e8ddcf600541148fdd77138bfcfed3d10b Mon Sep 17 00:00:00 2001 From: oystkva Date: Tue, 16 Apr 2024 18:46:14 +0200 Subject: [PATCH 31/67] fix: :pushpin: doAction fixed --- src/game/tetris.py | 23 +++++++++++------------ test/game/test_actions.py | 5 ++++- 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/src/game/tetris.py b/src/game/tetris.py index 08adc0c..ec45feb 100644 --- a/src/game/tetris.py +++ b/src/game/tetris.py @@ -106,11 +106,10 @@ def doAction(self, action: Action) -> None: elif action == Action.ROTATE_CLOCKWISE: new_block.rotateRight() elif action == Action.ROTATE_COUNTERCLOCKWISE: - new_block.rotateLeft() + new_block.rotateLeft() elif action == Action.HARD_DROP: while self.isValidBlockPosition(new_block): new_block.moveDown() - new_block.moveUp() elif action == Action.SOFT_DROP: new_block.moveDown() @@ -120,16 +119,16 @@ def doAction(self, action: Action) -> None: self._placeBlock() # 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) - ): - self._placeBlock() - self._checkGameOver() - # Store the previous board state before the new block placement - self.prevBoard = copy.deepcopy(self.board) - self._checkForFullRows() - self._shiftToNewBlock() + else: + if action in [Action.HARD_DROP, Action.SOFT_DROP]: + new_block.moveUp() + self.block = new_block + self._placeBlock() + self._checkForFullRows() + self._checkGameOver() + # Store the previous board state before the new block placement + self.prevBoard = copy.deepcopy(self.board) + self._shiftToNewBlock() def isValidBlockPosition(self, block: Block) -> bool: """ diff --git a/test/game/test_actions.py b/test/game/test_actions.py index 1ceb5e4..8e19921 100644 --- a/test/game/test_actions.py +++ b/test/game/test_actions.py @@ -62,6 +62,7 @@ def test_hard_drop(): ] board.doAction(Action.HARD_DROP) + for board_row, expected_row in zip(board.board, expected_board): assert board_row == expected_row @@ -120,9 +121,11 @@ def test_try_to_move_block_out_of_bound_left(): for _ in range(board.COLUMNS + 1): board.doAction(Action.MOVE_LEFT) + for row in board.board: + print(row) + print('\n') for board_row, expected_row in zip(board.board, expected_board): - print(len(board_row), len(expected_row)) assert board_row == expected_row From 76f60ca13c6236e9193433f3259eff1e10e1e3ce Mon Sep 17 00:00:00 2001 From: henrinha Date: Tue, 16 Apr 2024 18:48:28 +0200 Subject: [PATCH 32/67] feat: random trainer made --- src/agents/heuristic_trainer.py | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/src/agents/heuristic_trainer.py b/src/agents/heuristic_trainer.py index 215574c..9ae8b69 100644 --- a/src/agents/heuristic_trainer.py +++ b/src/agents/heuristic_trainer.py @@ -14,7 +14,7 @@ def train(): current_itteration = 0 - agents = _create_heuristic_agents(500) + agents = _create_heuristic_agents(50) max_itterations = len(agents) best_agent = None best_agent_lines_cleared = 10 @@ -57,7 +57,31 @@ def train(): game = Tetris() end_state = play_game(best_agent, game, shall_render=False) +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)] From 522ca214ff05b719e9dcdafa6bfcc5b8987bbcd8 Mon Sep 17 00:00:00 2001 From: henrinha Date: Tue, 16 Apr 2024 18:59:43 +0200 Subject: [PATCH 33/67] feat: made check average method of hyperparms --- src/agents/heuristic_trainer.py | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/src/agents/heuristic_trainer.py b/src/agents/heuristic_trainer.py index 9ae8b69..195f152 100644 --- a/src/agents/heuristic_trainer.py +++ b/src/agents/heuristic_trainer.py @@ -14,13 +14,13 @@ def train(): current_itteration = 0 - agents = _create_heuristic_agents(50) + agents = _create_heuristic_agents(30) max_itterations = len(agents) best_agent = None best_agent_lines_cleared = 10 min_lines_cleared = 8 - hyperparameters_seed = [-0.7161870466697167, -1.0206888263763436, 1.1110841067034372, -0.8480056575381625, -0.09805070564448792] + hyperparameters_seed = [-0.9550579397805573, -1.713732853744936, 0.48480501821908994, -0.8785318347320727, -1.828473435227082] epsilon = 0.001 learning_rate = 0.01 @@ -56,6 +56,29 @@ def train(): 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 From a4e095960a1a3534ea0f63237171ffc7659f7d16 Mon Sep 17 00:00:00 2001 From: oystkva Date: Tue, 16 Apr 2024 19:30:21 +0200 Subject: [PATCH 34/67] fix: :construction: Board and heuristic agent result tests fixed --- test/agents/test_heuristic_agent.py | 38 +++++++++++++++++++++++++---- test/game/test_actions.py | 3 --- test/game/test_board.py | 8 +++--- 3 files changed, 38 insertions(+), 11 deletions(-) diff --git a/test/agents/test_heuristic_agent.py b/test/agents/test_heuristic_agent.py index 59638eb..e7dfa4f 100644 --- a/test/agents/test_heuristic_agent.py +++ b/test/agents/test_heuristic_agent.py @@ -1,12 +1,11 @@ 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.agent import Agent, play_game +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], @@ -29,13 +28,42 @@ def test_result_heuristic_agent(): [1, 1, 1, 1, 1, 0, 0, 1, 1, 1], [1, 1, 1, 1, 1, 0, 1, 1, 1, 1], ] - block=Block(3,0,1) + + expected_board = [ + [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, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 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], + ] + + block = Block(3,0,1) board = Tetris(initBoard, block) - agent: Agent = create_agent("heuristic") + agent: HeuristicAgent = create_agent("heuristic") possible_moves = board.getPossibleBoards() result = agent.result(board) + for i in range(len(result)-1): + board.doAction(result[i]) print(result) - assert [] == 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 8e19921..3be22d0 100644 --- a/test/game/test_actions.py +++ b/test/game/test_actions.py @@ -121,9 +121,6 @@ def test_try_to_move_block_out_of_bound_left(): for _ in range(board.COLUMNS + 1): board.doAction(Action.MOVE_LEFT) - for row in board.board: - print(row) - print('\n') for board_row, expected_row in zip(board.board, expected_board): assert board_row == expected_row diff --git a/test/game/test_board.py b/test/game/test_board.py index 8526747..4e20900 100644 --- a/test/game/test_board.py +++ b/test/game/test_board.py @@ -8,11 +8,13 @@ def test_get_possible_boards_for_line(): i_block = Block(0, 3, 0) 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, Tetris) - standing_up_right = 9 + standing_up_right = 10 laying_down_right = 7 assert len(possible_boards) == standing_up_right + laying_down_right @@ -26,7 +28,7 @@ def test_get_possible_moves_for_square(): for move in possible_moves: assert isinstance(move, Tetris) - assert len(possible_moves) == 8 + assert len(possible_moves) == 9 def test_board_equal_for_the_same_object(): @@ -184,7 +186,7 @@ def test_transition_model_for_no_transition(): 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(): From 4e510d73eec63f2987e155f0c12ee0ae5adc4536 Mon Sep 17 00:00:00 2001 From: Maia Austigard Date: Tue, 16 Apr 2024 19:59:33 +0200 Subject: [PATCH 35/67] feat: start implementing geneticAlgAgent Co-authored-by: Jon Bergland --- src/agents/agent.py | 8 ++++-- src/agents/geneticAlgAgentJon.py | 46 ++++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 2 deletions(-) create mode 100644 src/agents/geneticAlgAgentJon.py diff --git a/src/agents/agent.py b/src/agents/agent.py index 951a5f4..cc5838b 100644 --- a/src/agents/agent.py +++ b/src/agents/agent.py @@ -34,7 +34,7 @@ def result(board: Tetris) -> Union[Action, list[Action]]: pass -def play_game(agent: Agent, board: Tetris, actions_per_drop: int = 1) -> Tetris: +def play_game(agent: Agent, board: Tetris, max_count: int, actions_per_drop: int = 1) -> Tetris: """ Plays a game of Tetris with the given agent. @@ -46,7 +46,9 @@ def play_game(agent: Agent, board: Tetris, actions_per_drop: int = 1) -> Tetris: Returns: The final state of the board after the game is over. """ - while not board.isGameOver(): + count = 0 + + while not board.isGameOver() or count < max_count: # Get the result of the agent's action for _ in range(actions_per_drop): result = agent.result(board) @@ -56,6 +58,8 @@ def play_game(agent: Agent, board: Tetris, actions_per_drop: int = 1) -> Tetris: board.doAction(action) else: board.doAction(result) + + count += 1 # Advance the game by one frame board.doAction(Action.SOFT_DROP) #board.printBoard() diff --git a/src/agents/geneticAlgAgentJon.py b/src/agents/geneticAlgAgentJon.py new file mode 100644 index 0000000..46e1ca4 --- /dev/null +++ b/src/agents/geneticAlgAgentJon.py @@ -0,0 +1,46 @@ +import random +from src.game.tetris import * +from src.agents.agent_factory import create_agent +from src.agents.agent import Agent, play_game +# From paper: https://codemyroad.wordpress.com/2013/04/14/tetris-ai-the-near-perfect-player/ +# the weigts the author got: +# a x (Aggregate Height) + b x (Complete Lines) + c x (Holes) + d x (Bumpiness) +# a = -0.510066 b = 0.760666 c = -0.35663 d = -0.184483 +# TODO Read the part of the article about the genetic algorithm +# TODO Create a fitness function + +# TODO Create a genetic algorithm based on the + +# List over vectors with boards attached: +# [(List over parameters, board), ...] + +# TODO create init-method that creates agents with random vectors +# TODO create run_games-method that goes through the agents, and play 100 games each, return average lines cleared +# TODO create method for fetching a random 10%, and finds the two with highest lines cleared, and makes a child (with 5% chance of mutation) +# TODO create method that makes 30% new agents from existing agents (last method), replace worst 30% with the new agents + +list = [] + +for _ in range(0, 100): + agg_height = random.random(-1, 0) + max_height = random.random(-1, 0) + lines_cleared = random.random(0, 1) + bumpiness = random.random(-1, 0) + holes = random.random(-1, 0) + + game = Tetris() + agent: Agent = create_agent("heuristic") + total_cleared = 0 + for _ in range(0, 100): + board = play_game(agent, game, 5) + total_cleared += board.rowsRemoved + list.append = ([agg_height, max_height, lines_cleared, bumpiness, holes], total_cleared/100) + + + +def fitness_crossover(pop1: tuple(list[int], int), pop2: tuple(list[int], int)) -> tuple(list[int], int): + return_pop: tuple(list[int], int) + # Combines the two vectors proportionaly by how many lines they cleared + child_pop = pop1[1] * pop1[0] + pop2[1] * pop2[0] + + return tuple(child_pop, 0) \ No newline at end of file From 16fa16100f1ef78be1815e80ea158ceb930e79c1 Mon Sep 17 00:00:00 2001 From: oystkva Date: Mon, 22 Apr 2024 17:12:05 +0200 Subject: [PATCH 36/67] feat: :art: Block rotations and spawn area of board implemented as original game; doAction() made pure; the block placement, checks and new block functionality previously located in the doAction() method parsed to its own method updateBoard() --- .devcontainer/devcontainer.json | 41 ++++ docs - Shortcut.lnk | Bin 0 -> 1670 bytes docs/guide/env.md | 39 ++++ main.py | 26 +-- src/agents/agent.py | 19 +- src/agents/heuristic.py | 33 +-- src/agents/heuristic_agent.py | 14 +- src/agents/randomAgent.py | 14 ++ src/game/TetrisGameManager.py | 2 + src/game/block.py | 40 +++- src/game/board.py | 344 ++++++++++++++++++++++++++++ src/game/tetris.py | 52 +++-- test/agents/test_heuristic.py | 65 +++++- test/agents/test_heuristic_agent.py | 14 +- test/game/test_actions.py | 97 +++++--- test/game/test_board.py | 14 ++ test/test_example.py | 6 + 17 files changed, 712 insertions(+), 108 deletions(-) create mode 100644 .devcontainer/devcontainer.json create mode 100644 docs - Shortcut.lnk create mode 100644 docs/guide/env.md create mode 100644 src/agents/randomAgent.py create mode 100644 src/game/board.py create mode 100644 test/test_example.py diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..9af928d --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,41 @@ +// For format details, see https://aka.ms/devcontainer.json. For config options, see the +// README at: https://github.com/devcontainers/templates/tree/main/src/python +{ + "name": "Python 3", + // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile + "image": "mcr.microsoft.com/devcontainers/python:1-3.12-bullseye", + "settings": { + "terminal.integrated.shell.linux": "/bin/bash" + }, + // Add the IDs of extensions you want installed when the container is created. + "customizations": { + "vscode": { + "extensions": [ + "streetsidesoftware.code-spell-checker", + "ms-azuretools.vscode-docker", + "DavidAnson.vscode-markdownlint", + "esbenp.prettier-vscode", + "ms-python.python", + "ms-python.vscode-pylance", + "ms-python.debugpy" + ] + } + }, + // install the pip packages on container creation + "postCreateCommand": "pip3 install --user -r requirements.txt", + + // Features to add to the dev container. More info: https://containers.dev/features. + // "features": {}, + + // Use 'forwardPorts' to make a list of ports inside the container available locally. + // "forwardPorts": [], + + // Use 'postCreateCommand' to run commands after the container is created. + // "postCreateCommand": "pip3 install --user -r requirements.txt", + + // Configure tool-specific properties. + // "customizations": {}, + + // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. + // "remoteUser": "root" +} diff --git a/docs - Shortcut.lnk b/docs - Shortcut.lnk new file mode 100644 index 0000000000000000000000000000000000000000..882ff5d770ca35f18c1a2d822667c687082bc785 GIT binary patch literal 1670 zcmb_cZA@EL7=FNN7}E)%Ziu>dGrE}#xNQMzBZgVzW1AK!kQ=xv1iH3c;X;cDCN$AR z5ysMGhRpe~R2^cBCKAJReopb1S(KS*CVyN=G=}NO{8-c&^?C2TEhs!t-pv+y=E5wq z$>^zUJ2o|(^UpaJy^*{Z&gNjdqC9skN_}1}vuL?92&XY92^A|#1TY{8mOWUa}?$8h6^RclKu`9bL51F;^sO# z8Z5tK>XGR@=tKJlI&iz?*WFlkW7==-E=kK}hPP#P=GhQ*3}HV8&|%=UT6hQ4nMtS3 z-xI8kMFxVvofhU*f!AA->W9WzetScZOw6KQ95Qhe&&1f;3k4Dp|-<`1(i4Gq|FqE3BCX(OPCFV-889mOis)hIj=@sAXIW*yTV<()kNT95d8Z^0DasTh_JL zKKO;QQ_VR~B}y^&edvh8fYu-ptj;(Xsh_F{ey|xDhAym?u_pswFI>=rq;Z>$K!kFkAf#}eyaP^ zKHB*znL$a(0=(KzO1X8#W#l9s%W)ej_=}c#7mwiKFV8>)$y&y-SWUZ|qjK8i(GUcM zzxgnUuW^b5qg=e{&86C+yvd8%?c>MJzj_1h#=9gLVp%g~W*65`u9i-fsA*55yrOcz zO!&h58ge{JwmZl>oxLvhW3$|I8JJ?${RaaGVymD|{k`$H&b(BX-FMRTR&m(u8 P)K6~xv3_ff*wf$N)S*}* literal 0 HcmV?d00001 diff --git a/docs/guide/env.md b/docs/guide/env.md new file mode 100644 index 0000000..2126630 --- /dev/null +++ b/docs/guide/env.md @@ -0,0 +1,39 @@ +# Development Environment + +We will be using vscode, and Dev Containers to develop the project. This will allow us to have a consistent development environment across all developers. + +## Getting Started + +* Have [Docker](https://docs.docker.com/get-docker/) installed + * Verify that docker is installed by running `docker --version` +* Install the [Dev Container](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers) extension for vscode +* Pull the latest changes from the repository +* Go to the "Remote Explorer" tab in vscode +* Click on the "Reopen in Container" button for the project +* Done! You should now have a fully functional development environment + +## Extensions + +The following extensions come with the dev container: + +* Prettier +* Code spell checker (to keep the documentation and code clean) +* Docker (in case we will be using docker more in the future) +* Markdownlint (to keep the documentation consistent and clean) +* The default python extensions + * Python + * Pylance + * Debugpy + +You can add more extensions by going to the "Extensions" tab in vscode and clicking on the "Install in Container" button for the extensions you cant live without. + +## Dependencies +When you build the container, the dependencies will be installed. If you use a new pip dependency, make sure to add it to the `requirements.txt` file. This can be done by running +```bash +pip freeze > requirements.txt +``` +in the dev container, and then committing the changes. + +## Structural Changes + +In case of structural changes to the project, like adding a new service, or changing `devcontainer.json`, you will need to rebuild the dev container. You can do this by clicking on the "Rebuild Container" button in the "Remote Explorer" tab in vscode. diff --git a/main.py b/main.py index 2c5e19a..2a42663 100644 --- a/main.py +++ b/main.py @@ -5,27 +5,25 @@ from src.agents.heuristic import ( utility ) -from src.agents.heuristic_trainer import train if __name__ == "__main__": - game = Tetris() - agent: Agent = create_agent("heuristic") - sum_rows_removed = 0 - for i in range(10): - end_board = play_game(agent, game, 7) - end_board.printBoard() - sum_rows_removed += end_board.rowsRemoved + # game = Tetris() + # agent: Agent = create_agent("heuristic") + # sum_rows_removed = 0 + # for i in range(1): + # end_board = play_game(agent, game, 7) + # end_board.printBoard() + # sum_rows_removed += end_board.rowsRemoved + # print(f"Rows removed: {end_board.rowsRemoved}") - print(f"Average rows removed: {sum_rows_removed / 10}") + # print(f"Average rows removed: {sum_rows_removed}") # possible_moves = game.getPossibleBoards() # for boards in possible_moves: # print(utility(boards, 0, -1, 0, 0, 0)) # boards.printBoard() - # board = Tetris() - # manager = TetrisGameManager(board) + board = Tetris() + manager = TetrisGameManager(board) - # manager.startGame() - - #train() + manager.startGame() diff --git a/src/agents/agent.py b/src/agents/agent.py index 951a5f4..9bdf60b 100644 --- a/src/agents/agent.py +++ b/src/agents/agent.py @@ -48,16 +48,19 @@ def play_game(agent: Agent, board: Tetris, actions_per_drop: int = 1) -> Tetris: """ while not board.isGameOver(): # 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) + result = agent.result(board) + # Perform the action(s) on the board + if isinstance(result, list): + for action in result: + board.doAction(action) + board.printBoard() + else: + board.doAction(result) + board.printBoard() # Advance the game by one frame board.doAction(Action.SOFT_DROP) + if board.blockHasLanded: + board.updateBoard() #board.printBoard() return board diff --git a/src/agents/heuristic.py b/src/agents/heuristic.py index 26eea48..0394496 100644 --- a/src/agents/heuristic.py +++ b/src/agents/heuristic.py @@ -3,17 +3,24 @@ from src.game.tetris import Tetris -def utility(gameState: Tetris, aggregate_heights_weight: int, max_height_weight: int, - lines_cleared_weight: int, bumpiness_weight: int, holes_weight: int) -> int: +def utility(gameState: Tetris, aggregate_heights_weight: float, max_height_weight: float, + lines_cleared_weight: float, bumpiness_weight: float, holes_weight: float) -> float: """Returns the utility of the given game state.""" sum = 0 - sum += aggregate_heights_weight * aggregate_heights(gameState) sum += max_height_weight * max_height(gameState) sum += lines_cleared_weight * lines_cleaned(gameState) sum += bumpiness_weight * bumpiness(gameState) 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 @@ -22,7 +29,7 @@ def aggregate_heights(gameState: Tetris) -> int: checkedList = [0 for i in range(gameState.COLUMNS)] for row in range(gameState.ROWS): for column in range(gameState.COLUMNS): - if gameState.prevBoard[row][column] != 0: + if gameState.board[row][column] != 0: if checkedList[column] == 0: checkedList[column] = gameState.ROWS - row return sum(checkedList) @@ -33,7 +40,7 @@ def max_height(gameState: Tetris) -> int: checkedList = [0 for i in range(gameState.COLUMNS)] for row in range(gameState.ROWS): for column in range(gameState.COLUMNS): - if gameState.prevBoard[row][column] > 0: + if gameState.board[row][column] > 0: if checkedList[column] == 0: checkedList[column] = gameState.ROWS - row return max(checkedList) @@ -42,7 +49,7 @@ def max_height(gameState: Tetris) -> int: def lines_cleaned(gameState: Tetris) -> int: """Retrurns the number of lines cleared.""" sum = 0 - for row in gameState.prevBoard: + for row in gameState.board: if all(cell == 1 for cell in row): sum += 1 return sum @@ -51,11 +58,11 @@ def lines_cleaned(gameState: Tetris) -> int: def bumpiness(gameState: Tetris) -> int: """Returns the sum of the absolute height between all the columns""" total_bumpiness = 0 - max_height = 20 + 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.prevBoard[row][column] > 0: + for row in range(gameState.SPAWN_ROWS, gameState.ROWS): + if gameState.board[row][column] > 0: if columnHeightMap[column] == 0: columnHeightMap[column] = max_height - row @@ -66,7 +73,7 @@ def bumpiness(gameState: Tetris) -> int: 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): @@ -91,11 +98,11 @@ def find_holes(gameState: Tetris) -> int: """ holes = 0 for column in range(gameState.COLUMNS): - top_block = 20 + top_block = gameState.ROWS for row in range(gameState.ROWS): - if (gameState.prevBoard[row][column] == 1) and (row < top_block): + if (gameState.board[row][column] == 1) and (row < top_block): top_block = row - if (gameState.prevBoard[row][column] == 0) and (row > top_block): + if (gameState.board[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 index 2728cce..64cdd5e 100644 --- a/src/agents/heuristic_agent.py +++ b/src/agents/heuristic_agent.py @@ -11,17 +11,17 @@ def result(self, board: Tetris) -> list[Action]: # Get all possible boards possible_boards = board.getPossibleBoards() - best_board: Tetris - best_utility = float("-inf") + best_board = possible_boards[0] + best_utility = utility(best_board, -0.8, -1.2, 3, -0.3,-3) # Check which board has the best outcome based on the heuristic - for boards in possible_boards: - current_utility = utility(boards, -0.5, -1.2, 2, -0.3,-0.6) + for candidate_board in possible_boards[1:]: + current_utility = utility(candidate_board, -0.8, -1.2, 4, -0.3,-0.6) + if current_utility > best_utility: - best_board = boards + best_board = candidate_board best_utility = current_utility - - + # Find the actions needed to transform the current board to the new board actions = [] try: diff --git a/src/agents/randomAgent.py b/src/agents/randomAgent.py new file mode 100644 index 0000000..44f2ba7 --- /dev/null +++ b/src/agents/randomAgent.py @@ -0,0 +1,14 @@ +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/game/TetrisGameManager.py b/src/game/TetrisGameManager.py index 47a6795..eca44af 100644 --- a/src/game/TetrisGameManager.py +++ b/src/game/TetrisGameManager.py @@ -34,6 +34,8 @@ def startGame(self): while not self.board.gameOver: self.inputHandling() + if self.board.blockHasLanded: + self.board.updateBoard() # Update the board after a block has landed and spawn a new block self.checkTimer() pygame.display.update() clock.tick(60) # Cap the frame rate to 60 FPS 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/board.py new file mode 100644 index 0000000..747251e --- /dev/null +++ b/src/game/board.py @@ -0,0 +1,344 @@ +import random +import copy + +from enum import Enum, auto + +from src.game.block import Block + + +class Action(Enum): + """Enumeration for the possible actions that can be performed on the board""" + + MOVE_LEFT = auto() + """ Move the block to the left """ + MOVE_RIGHT = auto() + """ Move the block to the right """ + ROTATE_CLOCKWISE = auto() + """ Rotate the block clockwise """ + ROTATE_COUNTERCLOCKWISE = auto() + """ Rotate the block counterclockwise """ + HARD_DROP = auto() + """ Instantly drop the block to the lowest possible position """ + SOFT_DROP = auto() + """ Move the block one step down """ + + +def get_all_actions() -> list[Action]: + return [ + Action.MOVE_LEFT, + Action.MOVE_RIGHT, + Action.ROTATE_CLOCKWISE, + Action.ROTATE_COUNTERCLOCKWISE, + Action.HARD_DROP, + Action.SOFT_DROP, + ] + + +class Board: + """ + Represents the Tetris game board, handling block placements, movements, and rotations, as well as checking for game over conditions. + + Attributes: + ROWS (int): Number of rows in the game board. + COLUMNS (int): Number of columns in the game board. + gameOver (bool): Flag indicating if the game is over. + rowsRemoved (int): Count of the total rows removed during the game. + board (list[list[int]]): The game board matrix, where 0 indicates an empty cell and 1 indicates a filled cell. + prevBoard (list[list[int]]): A copy of the game board before the latest block placement, used to check for valid movements and intersections. + block (Block): The current block being controlled by the player. + nextBlock (Block): The next block that will be introduced to the board after the current block is placed. + """ + + ROWS = 20 + COLUMNS = 10 + START_X = 3 + START_Y = 0 + + def __init__(self, board: list[list[int]] = None, block: 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: + self.board = self._initBoard() + else: + self.board = board + if block == None: + self.block = Block(self.START_X, self.START_Y, 0) + else: + self.block = block + self.prevBoard = copy.deepcopy(self.board) + + self._placeBlock() + + self.prevBlock = self.block.copy() + self.nextBlock = Block(self.START_X, self.START_Y, random.randint(0, 6)) + + def _initBoard(self) -> list[list[int]]: + """Initializes an empty the board""" + board = [] + for r in range(0, self.ROWS): + row = [] + for c in range(0, self.COLUMNS): + row.append(0) + board.append(row) + return board + + def getBoard(self) -> list[list[int]]: + return copy.deepcopy(self.board) + + def doAction(self, action: Action) -> 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. + """ + + # 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: + 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() + + # 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 + ): + self._placeBlock() + self._checkGameOver() + # Store the previous board state before the new block placement + self.prevBoard = copy.deepcopy(self.board) + self._checkForFullRows() + self._shiftToNewBlock() + + def isValidBlockPosition(self, block: Block) -> bool: + """ + Checks if the given block's position is valid (not out of bounds, not intersecting with existing blocks, and not causing a game over). + + Args: + block (Block): The block to check. + + 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 + + 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(): + if ( + row + block.y > self.ROWS - 1 + or row + block.y < 0 + or column + block.x > self.COLUMNS - 1 + or column + block.x < 0 + ): + return True + + return False + + 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: + return True + return False + + def isGameOver(self): + return self.gameOver + + def _placeBlock(self): + """Places the current block on the board""" + self.board = copy.deepcopy(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 + + def _shiftToNewBlock(self): + """Places the current block on the board and sets the next block as the current block""" + self.prevBlock = self.block.copy() + self.block = self.nextBlock + self.nextBlock = Block(self.START_X, self.START_Y, random.randint(0, 6)) + 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 + + def _checkGameOver(self): + """Checks if the game is over""" + for cell in self.board[0]: + if cell > 0: + self.gameOver = True + break + + def _checkForFullRows(self) -> int: + """Checks the board for full rows and removes them, returning the number of rows removed""" + amount = 0 + fullRows = [] + # Find all full rows + for rowIndex, row in enumerate(self.board): + # Check if the row is full + if 0 not in row: + fullRows.append(rowIndex) + # Remove all full rows + for rowIndex in reversed(fullRows): + self._clearRow(rowIndex) + amount += 1 + return amount + + 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)]) + self.board = newMatrix + self.rowsRemoved += 1 + + def getPossibleBoards(self) -> list["Board"]: + possibleMoves = [] + + # Number of rotations which gives unique block positions + if self.block.type < 3: + rotations = 2 + elif self.block.type < 6: + rotations = 4 + else: + rotations = 1 + + rotationBoard = copy.deepcopy(self) + for _ in range(rotations): + for column in range(self.COLUMNS): + moveBoard = copy.deepcopy(rotationBoard) + + # Calibrate the to the left + toLeft = moveBoard.block.x + for _ in range(toLeft): + moveBoard.doAction(Action.MOVE_LEFT) + # Move the block to the correct column + for _ in range(column): + moveBoard.doAction(Action.MOVE_RIGHT) + + moveBoard.doAction(Action.HARD_DROP) + if not moveBoard.isValidBlockPosition(moveBoard.block): + continue + + if moveBoard not in possibleMoves: + possibleMoves.append(moveBoard) + + rotationBoard.doAction(Action.ROTATE_CLOCKWISE) + + return possibleMoves + + def __eq__(self, other: "Board") -> bool: + if not isinstance(other, Board): + 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 + + def printBoard(self): + print("_______________________________________") + for row in self.board: + print("|" + " ".join(self._checkCharacter(cell) for cell in row) + "|") + + print("‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾") + + def _checkCharacter(self, character) -> str: + if character == 1: + return "■" + else: + return "▧" + + +def transition_model(current_state: Board, target_state: Board) -> list[Action]: + """ + Calculates the sequence of actions required to transition from the current board state to the target board state. + + Args: + current_state (Board): The current state of the Tetris board. + target_state (Board): The desired target state of the board. + + Returns: + list[Action]: A list of actions that need to be performed to achieve the target state. + """ + + actions = [] + + if current_state == target_state: + print("No transition needed") + return actions + + # Find where the last block is in the target state + target_block = target_state.prevBlock + + # Find the correct rotation + needed_rotation = target_block.rotation - current_state.block.rotation + actions += [Action.ROTATE_CLOCKWISE] * needed_rotation + + # Move the block to the correct x and y position + if current_state.block.x < target_block.x: + 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 + actions.append(Action.HARD_DROP) + + return actions diff --git a/src/game/tetris.py b/src/game/tetris.py index ec45feb..3e4e6e2 100644 --- a/src/game/tetris.py +++ b/src/game/tetris.py @@ -49,7 +49,8 @@ class Tetris: 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 @@ -61,11 +62,11 @@ def __init__(self, board: list[list[int]] = None, block: Block = None): 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 @@ -75,6 +76,7 @@ def __init__(self, board: list[list[int]] = None, block: Block = None): 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""" @@ -113,22 +115,27 @@ def doAction(self, action: Action) -> None: elif action == Action.SOFT_DROP: new_block.moveDown() - # Given the new block position, check if it is valid and update the board + # For blocks reaching the bottom of the board, place the block and introduce a new one + if ( + 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() - # For blocks reaching the bottom of the board, place the block and introduce a new one - else: - if action in [Action.HARD_DROP, Action.SOFT_DROP]: - new_block.moveUp() - self.block = new_block - self._placeBlock() - self._checkForFullRows() - self._checkGameOver() - # Store the previous board state before the new block placement - self.prevBoard = copy.deepcopy(self.board) - self._shiftToNewBlock() + + def updateBoard(self): + self.blockHasLanded = False + self._checkForFullRows() + self._checkGameOver() + # Store the previous board state before the new block placement + self.prevBoard = copy.deepcopy(self.board) + if self.isGameOver(): + return + self._shiftToNewBlock() def isValidBlockPosition(self, block: Block) -> bool: """ @@ -212,10 +219,11 @@ def _shiftToNewBlock(self): 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""" @@ -254,11 +262,11 @@ def getPossibleBoards(self) -> list["Tetris"]: rotationBoard = copy.deepcopy(self) for _ in range(rotations): - for column in range(0, self.COLUMNS): + for column in range(0, self.COLUMNS + (4 - self.block.getRightmostImageCoordinate())): moveBoard = copy.deepcopy(rotationBoard) # Calibrate the to the left - toLeft = moveBoard.block.x + toLeft = moveBoard.block.x + moveBoard.block.getLeftmostImageCoordinate() for _ in range(toLeft + 1): moveBoard.doAction(Action.MOVE_LEFT) # Move the block to the correct column @@ -322,7 +330,7 @@ def transition_model(current_state: Tetris, target_state: Tetris) -> list[Action 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 diff --git a/test/agents/test_heuristic.py b/test/agents/test_heuristic.py index 25f69fe..51b2a1c 100644 --- a/test/agents/test_heuristic.py +++ b/test/agents/test_heuristic.py @@ -25,6 +25,9 @@ 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], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], ] board = Tetris(initBoard) assert aggregate_heights(board) == 0 @@ -51,6 +54,9 @@ def test_heuristic_aggregate_with_equal_heights(): [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 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], [1, 1, 1, 1, 1, 1, 1, 1, 1, 0], [1, 1, 1, 1, 1, 1, 1, 1, 1, 0], @@ -80,6 +86,9 @@ 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], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 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], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], @@ -102,6 +111,9 @@ def test_heuristic_different_heights(): [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 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, 0], [0, 0, 0, 0, 0, 0, 0, 1, 0, 0], @@ -140,6 +152,9 @@ 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], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [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) == 0, "Expected max height of 0 for an empty board" @@ -147,6 +162,9 @@ def test_max_height_empty_board(): def test_max_height_equal_heights(): 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], @@ -176,6 +194,9 @@ def test_max_height_equal_heights(): def test_max_height_takes_highest(): 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], @@ -224,6 +245,9 @@ def test_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], [1, 1, 1, 1, 1, 1, 1, 1, 1, 1], ] board = Tetris(initBoard) @@ -253,12 +277,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 def test_twenty_lines_cleared(): 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], @@ -296,6 +326,9 @@ def test_ten_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], [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], @@ -334,6 +367,9 @@ def test_bumpiness_empty(): [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 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 @@ -359,6 +395,9 @@ def test_bumpiness_five(): [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 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, 1], ] @@ -388,6 +427,9 @@ def test_bumpiness_nine(): [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 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, 0, 1, 0, 1, 0, 1, 0], ] board = Tetris(initBoard) @@ -397,6 +439,9 @@ def test_bumpiness_nine(): def test_bumpiness_with_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], [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], @@ -425,6 +470,9 @@ def test_bumpiness_with_holes(): def test_bumpiness_40(): 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], @@ -473,6 +521,9 @@ 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], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], ] board = Tetris(initBoard) assert aggregate_height(board) == 0 @@ -481,6 +532,9 @@ def test_aggregate_height_zero(): def test_aggregate_height_full(): 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], @@ -509,6 +563,9 @@ def test_aggregate_height_full(): def test_aggregate_height_half(): 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], @@ -557,15 +614,21 @@ def test_no_holes(): [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], ] board = Tetris(initBoard) - assert find_holes(board) == 0, "Expected 0 holes" + 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], diff --git a/test/agents/test_heuristic_agent.py b/test/agents/test_heuristic_agent.py index e7dfa4f..bcf749a 100644 --- a/test/agents/test_heuristic_agent.py +++ b/test/agents/test_heuristic_agent.py @@ -23,6 +23,9 @@ def test_result_heuristic_agent(): [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 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], @@ -30,9 +33,12 @@ def test_result_heuristic_agent(): ] expected_board = [ - [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, 1, 1, 0, 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, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], @@ -52,7 +58,7 @@ def test_result_heuristic_agent(): [1, 1, 1, 1, 1, 0, 1, 1, 1, 1], ] - block = Block(3,0,1) + block = Block(3, 0, 1) board = Tetris(initBoard, block) agent: HeuristicAgent = create_agent("heuristic") diff --git a/test/game/test_actions.py b/test/game/test_actions.py index 3be22d0..5efba73 100644 --- a/test/game/test_actions.py +++ b/test/game/test_actions.py @@ -39,8 +39,6 @@ def test_move_left(): def test_hard_drop(): 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,10 +53,15 @@ 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) @@ -97,10 +100,13 @@ def test_rotate_counter_clockwise(): def test_try_to_move_block_out_of_bound_left(): 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], @@ -129,10 +135,13 @@ def test_try_to_move_block_out_of_bound_left(): def test_try_to_move_block_out_of_bound_right(): 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], @@ -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: Tetris = 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,6 +317,9 @@ 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], @@ -305,8 +327,11 @@ def test_slide_right_block_on_top_of_another_block(): ] 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], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 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,6 +406,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, 1, 1, 0, 0, 0, 0], diff --git a/test/game/test_board.py b/test/game/test_board.py index 4e20900..b635be6 100644 --- a/test/game/test_board.py +++ b/test/game/test_board.py @@ -211,6 +211,7 @@ def test_transition_model_complex_target(): ] for action in actual_actions: target_board.doAction(action) + target_board.updateBoard() actions = transition_model(current_board, target_board) assert isinstance(actions, list) @@ -229,6 +230,7 @@ def test_transition_model_left_movement(): ] for action in actual_actions: target_board.doAction(action) + target_board.updateBoard() actions = transition_model(current_board, target_board) assert isinstance(actions, list) @@ -248,10 +250,14 @@ def test_transition_model_execution(): ] for action in actual_actions: target_board.doAction(action) + if target_board.blockHasLanded: + target_board.updateBoard() actions = transition_model(current_board, target_board) for action in actions: current_board.doAction(action) + if current_board.blockHasLanded: + current_board.updateBoard() assert current_board == target_board @@ -268,10 +274,14 @@ def test_transition_model_execution_complex(): ] for action in actual_actions: target_board.doAction(action) + if target_board.blockHasLanded: + target_board.updateBoard() actions = transition_model(current_board, target_board) for action in actions: current_board.doAction(action) + if current_board.blockHasLanded: + current_board.updateBoard() assert current_board == target_board @@ -283,8 +293,12 @@ def test_transition_model_execution_of_invalid_move_sequence(): actual_actions += [Action.HARD_DROP] for action in actual_actions: target_board.doAction(action) + if target_board.blockHasLanded: + target_board.updateBoard() actions = transition_model(current_board, target_board) for action in actions: current_board.doAction(action) + if current_board.blockHasLanded: + current_board.updateBoard() assert current_board == target_board 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 From db75d86dd06a9fa2a6c1ce841cb6214c7c39814c Mon Sep 17 00:00:00 2001 From: Eduard Prokhorikhin Date: Mon, 22 Apr 2024 18:48:49 +0200 Subject: [PATCH 37/67] fix: changed up the tests and underlying code so that they are correct --- src/agents/heuristic.py | 10 +++++----- src/game/tetris.py | 2 +- test/agents/test_heuristic_agent.py | 13 ++++++------- test/game/test_board.py | 20 ++++---------------- 4 files changed, 16 insertions(+), 29 deletions(-) diff --git a/src/agents/heuristic.py b/src/agents/heuristic.py index 0394496..13bd8e9 100644 --- a/src/agents/heuristic.py +++ b/src/agents/heuristic.py @@ -29,7 +29,7 @@ def aggregate_heights(gameState: Tetris) -> int: checkedList = [0 for i in range(gameState.COLUMNS)] for row in range(gameState.ROWS): for column in range(gameState.COLUMNS): - if gameState.board[row][column] != 0: + if gameState.prevBoard[row][column] != 0: if checkedList[column] == 0: checkedList[column] = gameState.ROWS - row return sum(checkedList) @@ -40,7 +40,7 @@ def max_height(gameState: Tetris) -> int: checkedList = [0 for i in range(gameState.COLUMNS)] for row in range(gameState.ROWS): for column in range(gameState.COLUMNS): - if gameState.board[row][column] > 0: + if gameState.prevBoard[row][column] > 0: if checkedList[column] == 0: checkedList[column] = gameState.ROWS - row return max(checkedList) @@ -62,7 +62,7 @@ def bumpiness(gameState: Tetris) -> int: 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.SPAWN_ROWS, gameState.ROWS): - if gameState.board[row][column] > 0: + if gameState.prevBoard[row][column] > 0: if columnHeightMap[column] == 0: columnHeightMap[column] = max_height - row @@ -100,9 +100,9 @@ def find_holes(gameState: Tetris) -> int: for column in range(gameState.COLUMNS): top_block = gameState.ROWS for row in range(gameState.ROWS): - if (gameState.board[row][column] == 1) and (row < top_block): + if (gameState.prevBoard[row][column] == 1) and (row < top_block): top_block = row - if (gameState.board[row][column] == 0) and (row > top_block): + if (gameState.prevBoard[row][column] == 0) and (row > top_block): holes += 1 return holes diff --git a/src/game/tetris.py b/src/game/tetris.py index 3e4e6e2..44c0961 100644 --- a/src/game/tetris.py +++ b/src/game/tetris.py @@ -341,7 +341,7 @@ def transition_model(current_state: Tetris, target_state: Tetris) -> 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_heuristic_agent.py b/test/agents/test_heuristic_agent.py index bcf749a..c88a4f5 100644 --- a/test/agents/test_heuristic_agent.py +++ b/test/agents/test_heuristic_agent.py @@ -33,8 +33,8 @@ def test_result_heuristic_agent(): ] expected_board = [ - [0, 0, 0, 1, 1, 0, 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, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], @@ -53,18 +53,17 @@ def test_result_heuristic_agent(): [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], + [1, 1, 0, 0, 0, 0, 1, 0, 1, 1], + [1, 1, 1, 1, 1, 1, 1, 1, 1, 1], + [1, 1, 1, 1, 1, 1, 1, 1, 1, 1], ] block = Block(3, 0, 1) board = Tetris(initBoard, block) agent: HeuristicAgent = create_agent("heuristic") - possible_moves = board.getPossibleBoards() result = agent.result(board) - for i in range(len(result)-1): + for i in range(len(result)): board.doAction(result[i]) print(result) for row in board.board: diff --git a/test/game/test_board.py b/test/game/test_board.py index b635be6..5ce765d 100644 --- a/test/game/test_board.py +++ b/test/game/test_board.py @@ -196,11 +196,13 @@ def test_transition_model_x_direction(): 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: Tetris = Tetris() + 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, @@ -211,7 +213,6 @@ def test_transition_model_complex_target(): ] for action in actual_actions: target_board.doAction(action) - target_board.updateBoard() actions = transition_model(current_board, target_board) assert isinstance(actions, list) @@ -230,7 +231,6 @@ def test_transition_model_left_movement(): ] for action in actual_actions: target_board.doAction(action) - target_board.updateBoard() actions = transition_model(current_board, target_board) assert isinstance(actions, list) @@ -250,14 +250,10 @@ def test_transition_model_execution(): ] for action in actual_actions: target_board.doAction(action) - if target_board.blockHasLanded: - target_board.updateBoard() actions = transition_model(current_board, target_board) for action in actions: current_board.doAction(action) - if current_board.blockHasLanded: - current_board.updateBoard() assert current_board == target_board @@ -274,14 +270,10 @@ def test_transition_model_execution_complex(): ] for action in actual_actions: target_board.doAction(action) - if target_board.blockHasLanded: - target_board.updateBoard() actions = transition_model(current_board, target_board) for action in actions: current_board.doAction(action) - if current_board.blockHasLanded: - current_board.updateBoard() assert current_board == target_board @@ -293,12 +285,8 @@ def test_transition_model_execution_of_invalid_move_sequence(): actual_actions += [Action.HARD_DROP] for action in actual_actions: target_board.doAction(action) - if target_board.blockHasLanded: - target_board.updateBoard() actions = transition_model(current_board, target_board) for action in actions: current_board.doAction(action) - if current_board.blockHasLanded: - current_board.updateBoard() assert current_board == target_board From c5ab56e22ca824d2a66ceb2597fb4417db695d91 Mon Sep 17 00:00:00 2001 From: Eduard Prokhorikhin Date: Mon, 22 Apr 2024 19:31:57 +0200 Subject: [PATCH 38/67] feat: added basic pygame visuals --- src/game/TetrisGameManager.py | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/src/game/TetrisGameManager.py b/src/game/TetrisGameManager.py index eca44af..2c81b91 100644 --- a/src/game/TetrisGameManager.py +++ b/src/game/TetrisGameManager.py @@ -1,3 +1,4 @@ +from copy import deepcopy import pygame from pygame.locals import * import time as t @@ -7,6 +8,18 @@ baseScore = 100 +# pygame visuals setup +BLOCK_SIZE = 30 +WIDTH = 10 +HEIGHT = 23 +START_HEIGHT = 3 +SCREEN_WIDTH = WIDTH * BLOCK_SIZE +SCREEN_HEIGHT = (HEIGHT - START_HEIGHT) * BLOCK_SIZE + +BLACK = (0, 0, 0) +WHITE = (255, 255, 255) +BLUE = (0, 0, 255) + class TetrisGameManager: currentPiece = None nextPiece = None @@ -27,12 +40,14 @@ def isGameOver(self): def startGame(self): pygame.init() - self.screen = pygame.display.set_mode((80, 140)) # Create a dummy window + self.screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT)) + # self.screen = pygame.display.set_mode((400, 1080)) # Create a dummy window pygame.display.set_caption('Tetris') # Set window title clock = pygame.time.Clock() while not self.board.gameOver: + self.draw_board(self.board.board) self.inputHandling() if self.board.blockHasLanded: self.board.updateBoard() # Update the board after a block has landed and spawn a new block @@ -65,6 +80,17 @@ def checkTimer(self): if checkTime < newTime: self.currentTime = newTime self.movePiece(Action.SOFT_DROP) + + def draw_board(self, board): + self.screen.fill(BLACK) + temp = deepcopy(board) + temp = temp[START_HEIGHT:] + for y in range(HEIGHT-START_HEIGHT): + for x in range(WIDTH): + if temp[y][x] == 1: + pygame.draw.rect(self.screen, BLUE, (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) + def stopGame(self): pygame.quit() From 52bb3739df7b2ae42200dfbb4429513bdf2d0a29 Mon Sep 17 00:00:00 2001 From: Maia Austigard Date: Mon, 22 Apr 2024 20:05:17 +0200 Subject: [PATCH 39/67] feat: continue working on genetic algorithm agent Co-authored-by: Jon Bergland --- main.py | 27 ++-- src/agents/geneticAlgAgent.py | 3 +- src/agents/geneticAlgAgentJon.py | 130 +++++++++++++++--- src/agents/heuristic.py | 4 +- src/agents/heuristic_with_parameters_agent.py | 44 ++++++ 5 files changed, 176 insertions(+), 32 deletions(-) create mode 100644 src/agents/heuristic_with_parameters_agent.py diff --git a/main.py b/main.py index 2c5e19a..b5a5ec5 100644 --- a/main.py +++ b/main.py @@ -6,17 +6,24 @@ utility ) from src.agents.heuristic_trainer import train +from src.agents.geneticAlgAgentJon import GeneticAlgAgentJM if __name__ == "__main__": - game = Tetris() - agent: Agent = create_agent("heuristic") - sum_rows_removed = 0 - for i in range(10): - end_board = play_game(agent, game, 7) - end_board.printBoard() - sum_rows_removed += end_board.rowsRemoved + algAgent = GeneticAlgAgentJM() + algAgent.number_of_selection(1) + print(algAgent.getBestPop) - print(f"Average rows removed: {sum_rows_removed / 10}") + + + # game = Tetris() + # agent: Agent = create_agent("heuristic") + # sum_rows_removed = 0 + # for i in range(10): + # end_board = play_game(agent, game, 7) + # end_board.printBoard() + # sum_rows_removed += end_board.rowsRemoved + + # print(f"Average rows removed: {sum_rows_removed / 10}") # possible_moves = game.getPossibleBoards() # for boards in possible_moves: @@ -28,4 +35,6 @@ # manager.startGame() - #train() + # train() + + diff --git a/src/agents/geneticAlgAgent.py b/src/agents/geneticAlgAgent.py index 83f61a8..2511b93 100644 --- a/src/agents/geneticAlgAgent.py +++ b/src/agents/geneticAlgAgent.py @@ -4,4 +4,5 @@ # a = -0.510066 b = 0.760666 c = -0.35663 d = -0.184483 # TODO Read the part of the article about the genetic algorithm # TODO Create a fitness function -# TODO Create a genetic algorithm based on the \ No newline at end of file +# TODO Create a genetic algorithm based on the + diff --git a/src/agents/geneticAlgAgentJon.py b/src/agents/geneticAlgAgentJon.py index 46e1ca4..d2b2326 100644 --- a/src/agents/geneticAlgAgentJon.py +++ b/src/agents/geneticAlgAgentJon.py @@ -1,7 +1,8 @@ import random from src.game.tetris import * from src.agents.agent_factory import create_agent -from src.agents.agent import Agent, play_game +from src.agents.agent import Agent +from src.agents.heuristic_with_parameters_agent import * # From paper: https://codemyroad.wordpress.com/2013/04/14/tetris-ai-the-near-perfect-player/ # the weigts the author got: # a x (Aggregate Height) + b x (Complete Lines) + c x (Holes) + d x (Bumpiness) @@ -19,28 +20,117 @@ # TODO create method for fetching a random 10%, and finds the two with highest lines cleared, and makes a child (with 5% chance of mutation) # TODO create method that makes 30% new agents from existing agents (last method), replace worst 30% with the new agents -list = [] +class GeneticAlgAgentJM: + agents: list[list[list[float], float]] = [] -for _ in range(0, 100): - agg_height = random.random(-1, 0) - max_height = random.random(-1, 0) - lines_cleared = random.random(0, 1) - bumpiness = random.random(-1, 0) - holes = random.random(-1, 0) + def number_of_selection(self, number_of_selections: int): + self.agents = self.initAgents() + for i in range(0, number_of_selections): + # Select new pops + self.agents = self.replace_30_percent(self.agents) + + # Run new test + for i in range(len(self.agents)): + param_list = self.agents[i][0] + average_cleared = self.play_game(param_list[0], param_list[1], param_list[2], param_list[3], param_list[4]) + self.agents[i][1] = average_cleared + + + + def initAgents(self) -> list[list[list[float], float]]: + number_of_agents = 10 + for _ in range(0, number_of_agents): + agg_height = random.randrange(-1000, 0)/1000 + max_height = random.randrange(-1000, 0)/1000 + lines_cleared = random.randrange(0, 1000)/1000 + bumpiness = random.randrange(-1000, 0)/1000 + holes = random.randrange(-1000, 0)/1000 + + # agents = [] + average_cleared = self.play_game(agg_height, max_height, lines_cleared, bumpiness, holes) / number_of_agents + self.agents.append = ([agg_height, max_height, lines_cleared, bumpiness, holes], average_cleared) + print(_) + + # return agents - game = Tetris() - agent: Agent = create_agent("heuristic") - total_cleared = 0 - for _ in range(0, 100): - board = play_game(agent, game, 5) - total_cleared += board.rowsRemoved - list.append = ([agg_height, max_height, lines_cleared, bumpiness, holes], total_cleared/100) + + def play_game(self, agg_height, max_height, lines_cleared, bumpiness, holes): + + board = Tetris() + agent: Agent = HeuristicWithParametersAgent([agg_height, max_height, lines_cleared, bumpiness, holes]) + total_cleared = 0 + number_of_rounds = 10 + for _ in range(0, number_of_rounds): + + max_moves = number_of_rounds + move = 0 + actions_per_drop = 7 + + while not board.isGameOver() and move < max_moves: + # Get the result of the agent's action + for _ in range(actions_per_drop): + result = agent.result(board) + # Perform the action(s) on the board + if isinstance(result, list): + for action in result: + board.doAction(action) + else: + board.doAction(result) + + move += 1 + # Advance the game by one frame + board.doAction(Action.SOFT_DROP) + #board.printBoard() + + total_cleared += board.rowsRemoved + + return total_cleared + + def fitness_crossover(self, pop1: list[list[float], float], pop2: list[list[float], float]) -> list[list[float], float]: + # Combines the two vectors proportionaly by how many lines they cleared + child_pop = pop1[1] * pop1[0] + pop2[1] * pop2[0] + + return list(child_pop, 0) + + # TODO create method for fetching a random 10%, and finds the two with highest lines cleared, and makes a child (with 5% chance of mutation) + def paring_pop(self, pop_list: list[list[list[float], float]]) -> list[list[float], float]: + # Gets the number of pops to select + num_pops_to_select: int(len(pop_list) * 0.1) + + # Get a sample of pops based on the previous number + random_pop_sample: random.sample(pop_list, num_pops_to_select) + + # Gets the two pops with the highest lines cleared + highest_values: sorted(random_pop_sample, key=lambda x: x[1], reverse=True)[:2] + + # Gets the child pop of the two pops + new_pop = self.fitness_crossover(highest_values[0], highest_values[1]) + + # Mutate 5% of children pops + if random.random(0,1) < 0.2: + random_parameter = int(random.randint(0,4)) + new_pop[0][random_parameter] = (random.randrange(-200, 200)/1000) * new_pop[0][random_parameter] + + return new_pop -def fitness_crossover(pop1: tuple(list[int], int), pop2: tuple(list[int], int)) -> tuple(list[int], int): - return_pop: tuple(list[int], int) - # Combines the two vectors proportionaly by how many lines they cleared - child_pop = pop1[1] * pop1[0] + pop2[1] * pop2[0] + def replace_30_percent(self, pop_list: list[list[list[float], float]]) -> list[list[float], float]: + new_list: list[list[list[float], float]] + + # Number of pops needed for 30% of total number + num_pops_needed: int(len(pop_list) * 0.3) + + for _ in range(0, num_pops_needed): + new_list.append(self.paring_pop(pop_list)) + + pop_list: sorted(pop_list, key=lambda x: x[1], reverse=False)[:num_pops_needed] + + pop_list.append(new_list) + + return pop_list + - return tuple(child_pop, 0) \ No newline at end of file + def getBestPop(self) -> list[list[float], float]: + pop_list: sorted(pop_list, key, key=lambda x: x[1], reverse=False) + return pop_list[0] diff --git a/src/agents/heuristic.py b/src/agents/heuristic.py index 26eea48..992a3e8 100644 --- a/src/agents/heuristic.py +++ b/src/agents/heuristic.py @@ -3,8 +3,8 @@ from src.game.tetris import Tetris -def utility(gameState: Tetris, aggregate_heights_weight: int, max_height_weight: int, - lines_cleared_weight: int, bumpiness_weight: int, holes_weight: int) -> int: +def utility(gameState: Tetris, aggregate_heights_weight: float, max_height_weight: float, + lines_cleared_weight: float, bumpiness_weight: float, holes_weight: float) -> int: """Returns the utility of the given game state.""" sum = 0 diff --git a/src/agents/heuristic_with_parameters_agent.py b/src/agents/heuristic_with_parameters_agent.py new file mode 100644 index 0000000..0a2e8cd --- /dev/null +++ b/src/agents/heuristic_with_parameters_agent.py @@ -0,0 +1,44 @@ +from src.agents.agent import Agent +from src.game.tetris import Action, Tetris, transition_model, get_all_actions +from src.agents.heuristic import ( + utility +) + +class HeuristicWithParametersAgent(Agent): + + aggregate_heights_weight: float + max_height_weight: float + lines_cleared_weight: float + bumpiness_weight: float + holes_weight: float + + def __init__(self, params: list[float]): + self.aggregate_heights_weight = params[0] + self.max_height_weight = params[1] + self.lines_cleared_weight = params[2] + self.bumpiness_weight = params[3] + self.holes_weight = params[4] + + def result(self, board: Tetris) -> list[Action]: + # Get all possible boards + possible_boards = board.getPossibleBoards() + + best_board: Tetris + best_utility = float("-inf") + # Check which board has the best outcome based on the heuristic + for boards in possible_boards: + current_utility = utility(boards, self.aggregate_heights_weight, self.max_height_weight, + self.lines_cleared_weight, self.bumpiness_weight, self.holes_weight) + + if current_utility > best_utility: + best_board = boards + best_utility = current_utility + + + # Find the actions needed to transform the current board to the new board + actions = [] + try: + actions = transition_model(board, best_board) + return actions + except: + return actions From 9641f419c91f76c4e38953ff650c9593b88485da Mon Sep 17 00:00:00 2001 From: Eduard Prokhorikhin Date: Tue, 23 Apr 2024 10:37:04 +0200 Subject: [PATCH 40/67] fix:removed old files that were no longer in use --- src/agents/randomAgent.py | 14 -- src/game/board.py | 344 -------------------------------------- 2 files changed, 358 deletions(-) delete mode 100644 src/agents/randomAgent.py delete mode 100644 src/game/board.py 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/game/board.py b/src/game/board.py deleted file mode 100644 index 747251e..0000000 --- a/src/game/board.py +++ /dev/null @@ -1,344 +0,0 @@ -import random -import copy - -from enum import Enum, auto - -from src.game.block import Block - - -class Action(Enum): - """Enumeration for the possible actions that can be performed on the board""" - - MOVE_LEFT = auto() - """ Move the block to the left """ - MOVE_RIGHT = auto() - """ Move the block to the right """ - ROTATE_CLOCKWISE = auto() - """ Rotate the block clockwise """ - ROTATE_COUNTERCLOCKWISE = auto() - """ Rotate the block counterclockwise """ - HARD_DROP = auto() - """ Instantly drop the block to the lowest possible position """ - SOFT_DROP = auto() - """ Move the block one step down """ - - -def get_all_actions() -> list[Action]: - return [ - Action.MOVE_LEFT, - Action.MOVE_RIGHT, - Action.ROTATE_CLOCKWISE, - Action.ROTATE_COUNTERCLOCKWISE, - Action.HARD_DROP, - Action.SOFT_DROP, - ] - - -class Board: - """ - Represents the Tetris game board, handling block placements, movements, and rotations, as well as checking for game over conditions. - - Attributes: - ROWS (int): Number of rows in the game board. - COLUMNS (int): Number of columns in the game board. - gameOver (bool): Flag indicating if the game is over. - rowsRemoved (int): Count of the total rows removed during the game. - board (list[list[int]]): The game board matrix, where 0 indicates an empty cell and 1 indicates a filled cell. - prevBoard (list[list[int]]): A copy of the game board before the latest block placement, used to check for valid movements and intersections. - block (Block): The current block being controlled by the player. - nextBlock (Block): The next block that will be introduced to the board after the current block is placed. - """ - - ROWS = 20 - COLUMNS = 10 - START_X = 3 - START_Y = 0 - - def __init__(self, board: list[list[int]] = None, block: 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: - self.board = self._initBoard() - else: - self.board = board - if block == None: - self.block = Block(self.START_X, self.START_Y, 0) - else: - self.block = block - self.prevBoard = copy.deepcopy(self.board) - - self._placeBlock() - - self.prevBlock = self.block.copy() - self.nextBlock = Block(self.START_X, self.START_Y, random.randint(0, 6)) - - def _initBoard(self) -> list[list[int]]: - """Initializes an empty the board""" - board = [] - for r in range(0, self.ROWS): - row = [] - for c in range(0, self.COLUMNS): - row.append(0) - board.append(row) - return board - - def getBoard(self) -> list[list[int]]: - return copy.deepcopy(self.board) - - def doAction(self, action: Action) -> 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. - """ - - # 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: - 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() - - # 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 - ): - self._placeBlock() - self._checkGameOver() - # Store the previous board state before the new block placement - self.prevBoard = copy.deepcopy(self.board) - self._checkForFullRows() - self._shiftToNewBlock() - - def isValidBlockPosition(self, block: Block) -> bool: - """ - Checks if the given block's position is valid (not out of bounds, not intersecting with existing blocks, and not causing a game over). - - Args: - block (Block): The block to check. - - 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 - - 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(): - if ( - row + block.y > self.ROWS - 1 - or row + block.y < 0 - or column + block.x > self.COLUMNS - 1 - or column + block.x < 0 - ): - return True - - return False - - 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: - return True - return False - - def isGameOver(self): - return self.gameOver - - def _placeBlock(self): - """Places the current block on the board""" - self.board = copy.deepcopy(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 - - def _shiftToNewBlock(self): - """Places the current block on the board and sets the next block as the current block""" - self.prevBlock = self.block.copy() - self.block = self.nextBlock - self.nextBlock = Block(self.START_X, self.START_Y, random.randint(0, 6)) - 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 - - def _checkGameOver(self): - """Checks if the game is over""" - for cell in self.board[0]: - if cell > 0: - self.gameOver = True - break - - def _checkForFullRows(self) -> int: - """Checks the board for full rows and removes them, returning the number of rows removed""" - amount = 0 - fullRows = [] - # Find all full rows - for rowIndex, row in enumerate(self.board): - # Check if the row is full - if 0 not in row: - fullRows.append(rowIndex) - # Remove all full rows - for rowIndex in reversed(fullRows): - self._clearRow(rowIndex) - amount += 1 - return amount - - 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)]) - self.board = newMatrix - self.rowsRemoved += 1 - - def getPossibleBoards(self) -> list["Board"]: - possibleMoves = [] - - # Number of rotations which gives unique block positions - if self.block.type < 3: - rotations = 2 - elif self.block.type < 6: - rotations = 4 - else: - rotations = 1 - - rotationBoard = copy.deepcopy(self) - for _ in range(rotations): - for column in range(self.COLUMNS): - moveBoard = copy.deepcopy(rotationBoard) - - # Calibrate the to the left - toLeft = moveBoard.block.x - for _ in range(toLeft): - moveBoard.doAction(Action.MOVE_LEFT) - # Move the block to the correct column - for _ in range(column): - moveBoard.doAction(Action.MOVE_RIGHT) - - moveBoard.doAction(Action.HARD_DROP) - if not moveBoard.isValidBlockPosition(moveBoard.block): - continue - - if moveBoard not in possibleMoves: - possibleMoves.append(moveBoard) - - rotationBoard.doAction(Action.ROTATE_CLOCKWISE) - - return possibleMoves - - def __eq__(self, other: "Board") -> bool: - if not isinstance(other, Board): - 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 - - def printBoard(self): - print("_______________________________________") - for row in self.board: - print("|" + " ".join(self._checkCharacter(cell) for cell in row) + "|") - - print("‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾") - - def _checkCharacter(self, character) -> str: - if character == 1: - return "■" - else: - return "▧" - - -def transition_model(current_state: Board, target_state: Board) -> list[Action]: - """ - Calculates the sequence of actions required to transition from the current board state to the target board state. - - Args: - current_state (Board): The current state of the Tetris board. - target_state (Board): The desired target state of the board. - - Returns: - list[Action]: A list of actions that need to be performed to achieve the target state. - """ - - actions = [] - - if current_state == target_state: - print("No transition needed") - return actions - - # Find where the last block is in the target state - target_block = target_state.prevBlock - - # Find the correct rotation - needed_rotation = target_block.rotation - current_state.block.rotation - actions += [Action.ROTATE_CLOCKWISE] * needed_rotation - - # Move the block to the correct x and y position - if current_state.block.x < target_block.x: - 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 - actions.append(Action.HARD_DROP) - - return actions From 6704d2db24b88f22388a2f912fcda988afaabf37 Mon Sep 17 00:00:00 2001 From: Eduard Prokhorikhin Date: Tue, 23 Apr 2024 11:31:02 +0200 Subject: [PATCH 41/67] feat: added backbone for multiple corols --- src/agents/heuristic.py | 7 ++++--- src/game/TetrisGameManager.py | 11 ++++++----- src/game/tetris.py | 6 +++--- 3 files changed, 13 insertions(+), 11 deletions(-) diff --git a/src/agents/heuristic.py b/src/agents/heuristic.py index 13bd8e9..0870416 100644 --- a/src/agents/heuristic.py +++ b/src/agents/heuristic.py @@ -46,11 +46,12 @@ def max_height(gameState: Tetris) -> int: return max(checkedList) +# 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 @@ -70,7 +71,7 @@ def bumpiness(gameState: Tetris) -> int: total_bumpiness += abs(columnHeightMap[key] - columnHeightMap[key + 1]) return total_bumpiness - +# Henrik exluse ??? def aggregate_height(gameState: Tetris) -> int: "Returns the sum of all column-heights" max_height = gameState.ROWS @@ -100,7 +101,7 @@ def find_holes(gameState: Tetris) -> int: 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): + 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 diff --git a/src/game/TetrisGameManager.py b/src/game/TetrisGameManager.py index 2c81b91..a7b6a96 100644 --- a/src/game/TetrisGameManager.py +++ b/src/game/TetrisGameManager.py @@ -16,6 +16,7 @@ SCREEN_WIDTH = WIDTH * BLOCK_SIZE SCREEN_HEIGHT = (HEIGHT - START_HEIGHT) * BLOCK_SIZE +# Colors BLACK = (0, 0, 0) WHITE = (255, 255, 255) BLUE = (0, 0, 255) @@ -47,7 +48,7 @@ def startGame(self): clock = pygame.time.Clock() while not self.board.gameOver: - self.draw_board(self.board.board) + 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 @@ -81,13 +82,13 @@ def checkTimer(self): self.currentTime = newTime self.movePiece(Action.SOFT_DROP) - def draw_board(self, board): + def draw_board(self, gameState: Tetris): self.screen.fill(BLACK) - temp = deepcopy(board) - temp = temp[START_HEIGHT:] + temp = deepcopy(gameState) + temp_board = temp.board[START_HEIGHT:] for y in range(HEIGHT-START_HEIGHT): for x in range(WIDTH): - if temp[y][x] == 1: + if temp_board[y][x] == 1: pygame.draw.rect(self.screen, BLUE, (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) diff --git a/src/game/tetris.py b/src/game/tetris.py index 44c0961..83efd88 100644 --- a/src/game/tetris.py +++ b/src/game/tetris.py @@ -202,7 +202,7 @@ def _placeBlock(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 _shiftToNewBlock(self): @@ -215,7 +215,7 @@ 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""" @@ -304,7 +304,7 @@ def printBoard(self): print("‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾") def _checkCharacter(self, character) -> str: - if character == 1: + if character >= 1: return "■" else: return "▧" From 1eae354be38304ff5114a55778c05caadff39314 Mon Sep 17 00:00:00 2001 From: Eduard Prokhorikhin Date: Tue, 23 Apr 2024 11:31:42 +0200 Subject: [PATCH 42/67] feat: fixed test to work with the color settup --- test/agents/test_heuristic_agent.py | 6 +++--- test/game/test_actions.py | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/test/agents/test_heuristic_agent.py b/test/agents/test_heuristic_agent.py index c88a4f5..b89a954 100644 --- a/test/agents/test_heuristic_agent.py +++ b/test/agents/test_heuristic_agent.py @@ -53,9 +53,9 @@ def test_result_heuristic_agent(): [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, 1, 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, 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) diff --git a/test/game/test_actions.py b/test/game/test_actions.py index 5efba73..0305f55 100644 --- a/test/game/test_actions.py +++ b/test/game/test_actions.py @@ -344,8 +344,8 @@ 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, 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], @@ -411,8 +411,8 @@ def test_slide_right_block_on_under_another_block(): [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) From 32d03bab3fd95cc7058db2f848b0fa63258500fe Mon Sep 17 00:00:00 2001 From: Eduard Prokhorikhin Date: Tue, 23 Apr 2024 12:13:48 +0200 Subject: [PATCH 43/67] feat: Added support for colors in pygame gui --- main.py | 2 +- src/game/TetrisGameManager.py | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/main.py b/main.py index 2a42663..de1a8ea 100644 --- a/main.py +++ b/main.py @@ -26,4 +26,4 @@ board = Tetris() manager = TetrisGameManager(board) - manager.startGame() + manager.startGame() \ No newline at end of file diff --git a/src/game/TetrisGameManager.py b/src/game/TetrisGameManager.py index a7b6a96..3ae6243 100644 --- a/src/game/TetrisGameManager.py +++ b/src/game/TetrisGameManager.py @@ -5,11 +5,12 @@ import sys from src.game.tetris import Action, Tetris +from src.game.block import COLORS baseScore = 100 # pygame visuals setup -BLOCK_SIZE = 30 +BLOCK_SIZE = 40 WIDTH = 10 HEIGHT = 23 START_HEIGHT = 3 @@ -19,7 +20,6 @@ # Colors BLACK = (0, 0, 0) WHITE = (255, 255, 255) -BLUE = (0, 0, 255) class TetrisGameManager: currentPiece = None @@ -88,8 +88,8 @@ def draw_board(self, gameState: Tetris): temp_board = temp.board[START_HEIGHT:] for y in range(HEIGHT-START_HEIGHT): for x in range(WIDTH): - if temp_board[y][x] == 1: - pygame.draw.rect(self.screen, BLUE, (x * BLOCK_SIZE, y * BLOCK_SIZE, BLOCK_SIZE, BLOCK_SIZE)) + 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) From a1878602a3963a7398bbc792b6f39acc9350cd08 Mon Sep 17 00:00:00 2001 From: Eduard Prokhorikhin Date: Tue, 23 Apr 2024 13:17:04 +0200 Subject: [PATCH 44/67] feat: made a demo mode for agents --- main.py | 3 ++- src/agents/agent.py | 32 ++++++++++++++++++++++++++++++-- src/game/TetrisGameManager.py | 20 +++++++++++++++++++- 3 files changed, 51 insertions(+), 4 deletions(-) diff --git a/main.py b/main.py index de1a8ea..4b2fc37 100644 --- a/main.py +++ b/main.py @@ -25,5 +25,6 @@ board = Tetris() manager = TetrisGameManager(board) + agent = create_agent("heuristic") - manager.startGame() \ No newline at end of file + manager.startDemo(agent) \ No newline at end of file diff --git a/src/agents/agent.py b/src/agents/agent.py index 9bdf60b..59c7e9e 100644 --- a/src/agents/agent.py +++ b/src/agents/agent.py @@ -7,6 +7,7 @@ class for all agents in the simulation. from typing import Any, Union from src.game.tetris import Action, Tetris +from time import sleep class Agent(ABC): @@ -53,10 +54,10 @@ def play_game(agent: Agent, board: Tetris, actions_per_drop: int = 1) -> Tetris: if isinstance(result, list): for action in result: board.doAction(action) - board.printBoard() + # board.printBoard() else: board.doAction(result) - board.printBoard() + # board.printBoard() # Advance the game by one frame board.doAction(Action.SOFT_DROP) if board.blockHasLanded: @@ -64,3 +65,30 @@ def play_game(agent: Agent, board: Tetris, actions_per_drop: int = 1) -> Tetris: #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) + # Perform the action(s) on the board + if isinstance(result, list): + for action in result: + board.doAction(action) + sleep(0.2) + # board.printBoard() + else: + board.doAction(result) + sleep(0.1) + # board.printBoard() + # Advance the game by one frame + board.doAction(Action.SOFT_DROP) + if board.blockHasLanded: + board.updateBoard() + # board.printBoard() diff --git a/src/game/TetrisGameManager.py b/src/game/TetrisGameManager.py index 3ae6243..8989563 100644 --- a/src/game/TetrisGameManager.py +++ b/src/game/TetrisGameManager.py @@ -4,6 +4,7 @@ import time as t import sys +from src.agents.agent import Agent, playGameDemoStepByStep from src.game.tetris import Action, Tetris from src.game.block import COLORS @@ -42,7 +43,6 @@ def isGameOver(self): def startGame(self): pygame.init() self.screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT)) - # self.screen = pygame.display.set_mode((400, 1080)) # Create a dummy window pygame.display.set_caption('Tetris') # Set window title clock = pygame.time.Clock() @@ -58,6 +58,24 @@ def startGame(self): 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: From 409fd661b1b388269ab759f67d486869593981fa Mon Sep 17 00:00:00 2001 From: Eduard Prokhorikhin Date: Tue, 23 Apr 2024 15:41:37 +0200 Subject: [PATCH 45/67] fix: utility now calculated of properly --- src/agents/agent.py | 2 +- src/agents/heuristic_agent.py | 18 ++++++++---------- src/game/tetris.py | 1 + 3 files changed, 10 insertions(+), 11 deletions(-) diff --git a/src/agents/agent.py b/src/agents/agent.py index 59c7e9e..59878c4 100644 --- a/src/agents/agent.py +++ b/src/agents/agent.py @@ -81,7 +81,7 @@ def playGameDemoStepByStep(agent: Agent, board: Tetris) -> Tetris: if isinstance(result, list): for action in result: board.doAction(action) - sleep(0.2) + sleep(0.3) # board.printBoard() else: board.doAction(result) diff --git a/src/agents/heuristic_agent.py b/src/agents/heuristic_agent.py index 64cdd5e..6ae948c 100644 --- a/src/agents/heuristic_agent.py +++ b/src/agents/heuristic_agent.py @@ -11,21 +11,19 @@ def result(self, board: Tetris) -> list[Action]: # Get all possible boards possible_boards = board.getPossibleBoards() - best_board = possible_boards[0] - best_utility = utility(best_board, -0.8, -1.2, 3, -0.3,-3) + best_board = None + best_utility = float('-inf') # Check which board has the best outcome based on the heuristic - for candidate_board in possible_boards[1:]: - current_utility = utility(candidate_board, -0.8, -1.2, 4, -0.3,-0.6) + 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 = [] - try: - actions = transition_model(board, best_board) - return actions - except: - return actions + actions = transition_model(board, best_board) + return actions + diff --git a/src/game/tetris.py b/src/game/tetris.py index 83efd88..9b6bd67 100644 --- a/src/game/tetris.py +++ b/src/game/tetris.py @@ -277,6 +277,7 @@ def getPossibleBoards(self) -> list["Tetris"]: if not moveBoard.isValidBlockPosition(moveBoard.block): continue + moveBoard.prevBoard = copy.deepcopy(moveBoard.board) if moveBoard not in possibleMoves: possibleMoves.append(moveBoard) From fd2983fa355ab28a84b8febe7cc66b259de202be Mon Sep 17 00:00:00 2001 From: Eduard Prokhorikhin Date: Tue, 23 Apr 2024 16:50:19 +0200 Subject: [PATCH 46/67] feat: added demo mode for agent --- src/agents/agent.py | 13 ++++++------- src/game/tetris.py | 10 ++++++++-- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/src/agents/agent.py b/src/agents/agent.py index 59878c4..c6bc0cf 100644 --- a/src/agents/agent.py +++ b/src/agents/agent.py @@ -77,18 +77,17 @@ def playGameDemoStepByStep(agent: Agent, board: Tetris) -> Tetris: # 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) - sleep(0.3) - # board.printBoard() + board.doAction(action, demo=True) else: - board.doAction(result) - sleep(0.1) - # board.printBoard() + board.doAction(action, demo=True) # Advance the game by one frame board.doAction(Action.SOFT_DROP) if board.blockHasLanded: board.updateBoard() - # board.printBoard() diff --git a/src/game/tetris.py b/src/game/tetris.py index 9b6bd67..08cd5a2 100644 --- a/src/game/tetris.py +++ b/src/game/tetris.py @@ -2,9 +2,12 @@ import copy 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""" @@ -91,12 +94,13 @@ def _initBoard(self) -> list[list[int]]: def getBoard(self) -> list[list[int]]: return copy.deepcopy(self.board) - 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 @@ -125,6 +129,8 @@ def doAction(self, action: Action) -> None: if self.isValidBlockPosition(new_block): self.block = new_block self._placeBlock() + if demo: + sleep(DEMO_SLEEP) def updateBoard(self): @@ -327,7 +333,7 @@ def transition_model(current_state: Tetris, target_state: Tetris) -> list[Action if current_state == target_state: actions.append(Action.SOFT_DROP) - print("No transition needed") + # print("No transition needed") return actions # Find where the last block is in the target state From 6d3846aa1e4b70aaeee1750dd74449b70438e5e2 Mon Sep 17 00:00:00 2001 From: Maia Austigard Date: Tue, 23 Apr 2024 17:20:21 +0200 Subject: [PATCH 47/67] fix: optimize code --- main.py | 9 ++--- src/agents/agent.py | 8 ++-- src/agents/geneticAlgAgentJon.py | 67 +++++++++++++++++--------------- 3 files changed, 44 insertions(+), 40 deletions(-) diff --git a/main.py b/main.py index b5a5ec5..75e2897 100644 --- a/main.py +++ b/main.py @@ -9,11 +9,6 @@ from src.agents.geneticAlgAgentJon import GeneticAlgAgentJM if __name__ == "__main__": - algAgent = GeneticAlgAgentJM() - algAgent.number_of_selection(1) - print(algAgent.getBestPop) - - # game = Tetris() # agent: Agent = create_agent("heuristic") @@ -37,4 +32,8 @@ # train() + + algAgent = GeneticAlgAgentJM() + algAgent.number_of_selection(1) + print(algAgent.getBestPop()) diff --git a/src/agents/agent.py b/src/agents/agent.py index cc5838b..d46cd55 100644 --- a/src/agents/agent.py +++ b/src/agents/agent.py @@ -34,7 +34,7 @@ def result(board: Tetris) -> Union[Action, list[Action]]: pass -def play_game(agent: Agent, board: Tetris, max_count: int, actions_per_drop: int = 1) -> Tetris: +def play_game(agent: Agent, board: Tetris, actions_per_drop: int = 1) -> Tetris: """ Plays a game of Tetris with the given agent. @@ -46,9 +46,9 @@ def play_game(agent: Agent, board: Tetris, max_count: int, actions_per_drop: int Returns: The final state of the board after the game is over. """ - count = 0 + #count = 0 - while not board.isGameOver() or count < max_count: + while not board.isGameOver(): # Get the result of the agent's action for _ in range(actions_per_drop): result = agent.result(board) @@ -59,7 +59,7 @@ def play_game(agent: Agent, board: Tetris, max_count: int, actions_per_drop: int else: board.doAction(result) - count += 1 + #count += 1 # Advance the game by one frame board.doAction(Action.SOFT_DROP) #board.printBoard() diff --git a/src/agents/geneticAlgAgentJon.py b/src/agents/geneticAlgAgentJon.py index d2b2326..9171c3a 100644 --- a/src/agents/geneticAlgAgentJon.py +++ b/src/agents/geneticAlgAgentJon.py @@ -24,9 +24,10 @@ class GeneticAlgAgentJM: agents: list[list[list[float], float]] = [] def number_of_selection(self, number_of_selections: int): - self.agents = self.initAgents() + self.initAgents() for i in range(0, number_of_selections): # Select new pops + print(len(self.agents)) self.agents = self.replace_30_percent(self.agents) # Run new test @@ -36,9 +37,8 @@ def number_of_selection(self, number_of_selections: int): self.agents[i][1] = average_cleared - def initAgents(self) -> list[list[list[float], float]]: - number_of_agents = 10 + number_of_agents = 20 for _ in range(0, number_of_agents): agg_height = random.randrange(-1000, 0)/1000 max_height = random.randrange(-1000, 0)/1000 @@ -48,7 +48,7 @@ def initAgents(self) -> list[list[list[float], float]]: # agents = [] average_cleared = self.play_game(agg_height, max_height, lines_cleared, bumpiness, holes) / number_of_agents - self.agents.append = ([agg_height, max_height, lines_cleared, bumpiness, holes], average_cleared) + self.agents.append([[agg_height, max_height, lines_cleared, bumpiness, holes], average_cleared]) print(_) # return agents @@ -87,50 +87,55 @@ def play_game(self, agg_height, max_height, lines_cleared, bumpiness, holes): return total_cleared - def fitness_crossover(self, pop1: list[list[float], float], pop2: list[list[float], float]) -> list[list[float], float]: - # Combines the two vectors proportionaly by how many lines they cleared - child_pop = pop1[1] * pop1[0] + pop2[1] * pop2[0] - - return list(child_pop, 0) + def replace_30_percent(self, pop_list: list[list[list[float], float]]) -> list[list[float], float]: + new_list = []#: list[list[list[float], float]] + - # TODO create method for fetching a random 10%, and finds the two with highest lines cleared, and makes a child (with 5% chance of mutation) + # Number of pops needed for 30% of total number + num_pops_needed = int(len(pop_list) * 0.3) + + for _ in range(0, num_pops_needed): + new_list.append(self.paring_pop(pop_list)) # liste.append(liste[liste, float]) = liste[liste[liste, float]] + + pop_list: sorted(pop_list, key=lambda x: x[1], reverse=False)[:num_pops_needed] + + pop_list.extend(new_list) + + return pop_list + + + # TODO create method for fetching a random 10%, and finds the two with highest lines cleared, and makes a child (with 5% chance of mutation) def paring_pop(self, pop_list: list[list[list[float], float]]) -> list[list[float], float]: # Gets the number of pops to select - num_pops_to_select: int(len(pop_list) * 0.1) + num_pops_to_select = int(len(pop_list) * 0.1) # Get a sample of pops based on the previous number - random_pop_sample: random.sample(pop_list, num_pops_to_select) + random_pop_sample = random.sample(pop_list, num_pops_to_select) # Gets the two pops with the highest lines cleared - highest_values: sorted(random_pop_sample, key=lambda x: x[1], reverse=True)[:2] + highest_values = sorted(random_pop_sample, key=lambda x: x[1], reverse=True)[:2] # Gets the child pop of the two pops - new_pop = self.fitness_crossover(highest_values[0], highest_values[1]) + new_pop = self.fitness_crossover(highest_values[0], highest_values[1]) # liste[liste, float] # Mutate 5% of children pops - if random.random(0,1) < 0.2: + if random.randrange(0,1000)/1000 < 0.2: random_parameter = int(random.randint(0,4)) new_pop[0][random_parameter] = (random.randrange(-200, 200)/1000) * new_pop[0][random_parameter] - return new_pop - - - def replace_30_percent(self, pop_list: list[list[list[float], float]]) -> list[list[float], float]: - new_list: list[list[list[float], float]] - - # Number of pops needed for 30% of total number - num_pops_needed: int(len(pop_list) * 0.3) - - for _ in range(0, num_pops_needed): - new_list.append(self.paring_pop(pop_list)) - - pop_list: sorted(pop_list, key=lambda x: x[1], reverse=False)[:num_pops_needed] + return new_pop # liste[liste, float] - pop_list.append(new_list) - return pop_list + def fitness_crossover(self, pop1: list[list[float], float], pop2: list[list[float], float]) -> list[list[float], float]: + # Combines the two vectors proportionaly by how many lines they cleared + parent_pop1 = [h * pop1[1] for h in pop1[0]] + parent_pop2 = [h * pop2[1] for h in pop2[0]] + child_pop = [h1 + h2 for h1, h2 in zip(parent_pop1, parent_pop2)] + return [child_pop, 0.0] # liste[liste, float] + def getBestPop(self) -> list[list[float], float]: - pop_list: sorted(pop_list, key, key=lambda x: x[1], reverse=False) + pop_list = self.agents + pop_list = sorted(pop_list, key=lambda x: x[1], reverse=False) return pop_list[0] From 06fed5edbafdbc8225c6fa03e4ae2696e89fdaff Mon Sep 17 00:00:00 2001 From: Jon Bergland <144770553+JonBergland@users.noreply.github.com> Date: Tue, 19 Mar 2024 17:39:29 +0100 Subject: [PATCH 48/67] Revert "3 make tetris board" --- .devcontainer/devcontainer.json | 41 ++++++++++++++++++++++++++ docs/guide/{devContainer.md => env.md} | 0 docs/guide/venv.md | 25 ---------------- docs/planning/12.03.2024.md | 14 --------- 4 files changed, 41 insertions(+), 39 deletions(-) create mode 100644 .devcontainer/devcontainer.json rename docs/guide/{devContainer.md => env.md} (100%) delete mode 100644 docs/guide/venv.md delete mode 100644 docs/planning/12.03.2024.md diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..9af928d --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,41 @@ +// For format details, see https://aka.ms/devcontainer.json. For config options, see the +// README at: https://github.com/devcontainers/templates/tree/main/src/python +{ + "name": "Python 3", + // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile + "image": "mcr.microsoft.com/devcontainers/python:1-3.12-bullseye", + "settings": { + "terminal.integrated.shell.linux": "/bin/bash" + }, + // Add the IDs of extensions you want installed when the container is created. + "customizations": { + "vscode": { + "extensions": [ + "streetsidesoftware.code-spell-checker", + "ms-azuretools.vscode-docker", + "DavidAnson.vscode-markdownlint", + "esbenp.prettier-vscode", + "ms-python.python", + "ms-python.vscode-pylance", + "ms-python.debugpy" + ] + } + }, + // install the pip packages on container creation + "postCreateCommand": "pip3 install --user -r requirements.txt", + + // Features to add to the dev container. More info: https://containers.dev/features. + // "features": {}, + + // Use 'forwardPorts' to make a list of ports inside the container available locally. + // "forwardPorts": [], + + // Use 'postCreateCommand' to run commands after the container is created. + // "postCreateCommand": "pip3 install --user -r requirements.txt", + + // Configure tool-specific properties. + // "customizations": {}, + + // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. + // "remoteUser": "root" +} diff --git a/docs/guide/devContainer.md b/docs/guide/env.md similarity index 100% rename from docs/guide/devContainer.md rename to docs/guide/env.md diff --git a/docs/guide/venv.md b/docs/guide/venv.md deleted file mode 100644 index 3689853..0000000 --- a/docs/guide/venv.md +++ /dev/null @@ -1,25 +0,0 @@ -# Development Environment - -We will be utilizing a virtual environment to develop the project. This will allow us to have a consistent development environment across all developers.In this case we will be using `venv` to create the virtual environment. - -## Getting Started - -* Have [Python](https://www.python.org/downloads/) installed - * Verify that python is installed by running `python --version` -* Pip install the virtual environment package - * Verify that pip is installed by running `pip --version` - * Install the virtual environment package by running `pip install virtualenv` -* In the root of the project, create a virtual environment by running `python -m venv .venv` -* Activate the virtual environment - * On Windows, run `.venv\Scripts\activate` - * On Mac/Linux, run `source .venv/bin/activate` -* Done! You should now have a fully functional development environment -* To deactivate the virtual environment, run `deactivate` - -## Dependencies -Once you have entered venv you need to make sure the dependencies are installed by running `pip install -r requirements.txt`. -If you use a new pip dependency, make sure to add it to the `requirements.txt` file. This can be done by running: -```bash -pip freeze > requirements.txt -``` -after you pip installed it locally, and then committing the changes. diff --git a/docs/planning/12.03.2024.md b/docs/planning/12.03.2024.md deleted file mode 100644 index 6884f3a..0000000 --- a/docs/planning/12.03.2024.md +++ /dev/null @@ -1,14 +0,0 @@ -# Cogito work night nr. 3 - -## Agenda - -- Venv -- [Mer klarere plan for Tetris](#tetris) -- Progge -- Hva skjer neste uke? - - -## Tetris -- **Headless:** ingen grafikk ting skjer sekvensielt. En viss boardstate og en ny block -> velger presist hvor den skal plasseres (blant mulige plasser) uten tidsbegrensning. - For development purpuses har vi en to print outs av brettet. Første viser brettet med den uplasserte blokken (øverst i midten), andre viser brettet etter at blokken er plassert. Bruker de objektene som vi har blitt enige om. Tanken er at vi kan bruke dette til å teste ut forskjellige algoritmer for å plassere blokken (ai/algorytme velger posisjonen). -- **Grafisk:** pygame. Adapsjon av samme objekter som headless bare at vi nå styrer hvor blokken skal plasseres dynamisk. Blokken faller nedover med en viss hastighet og vi kan flytte den rundt med en viss hastighet (feks. et tastetrykk = forflytter blokken en rute). For å la agenten spille må vi lage et oversettelses lag mellom headless og grafisk, hvor vi kan sende input til grafisk for å manuvrere blokken til samme posisjon som headless ville plassert den. \ No newline at end of file From 45202d98e5ff82c7d818b02eec0fb360a646da3b Mon Sep 17 00:00:00 2001 From: Maia Austigard Date: Tue, 16 Apr 2024 19:59:33 +0200 Subject: [PATCH 49/67] feat: start implementing geneticAlgAgent Co-authored-by: Jon Bergland --- src/agents/agent.py | 8 ++++-- src/agents/geneticAlgAgentJon.py | 46 ++++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 2 deletions(-) create mode 100644 src/agents/geneticAlgAgentJon.py diff --git a/src/agents/agent.py b/src/agents/agent.py index 951a5f4..cc5838b 100644 --- a/src/agents/agent.py +++ b/src/agents/agent.py @@ -34,7 +34,7 @@ def result(board: Tetris) -> Union[Action, list[Action]]: pass -def play_game(agent: Agent, board: Tetris, actions_per_drop: int = 1) -> Tetris: +def play_game(agent: Agent, board: Tetris, max_count: int, actions_per_drop: int = 1) -> Tetris: """ Plays a game of Tetris with the given agent. @@ -46,7 +46,9 @@ def play_game(agent: Agent, board: Tetris, actions_per_drop: int = 1) -> Tetris: Returns: The final state of the board after the game is over. """ - while not board.isGameOver(): + count = 0 + + while not board.isGameOver() or count < max_count: # Get the result of the agent's action for _ in range(actions_per_drop): result = agent.result(board) @@ -56,6 +58,8 @@ def play_game(agent: Agent, board: Tetris, actions_per_drop: int = 1) -> Tetris: board.doAction(action) else: board.doAction(result) + + count += 1 # Advance the game by one frame board.doAction(Action.SOFT_DROP) #board.printBoard() diff --git a/src/agents/geneticAlgAgentJon.py b/src/agents/geneticAlgAgentJon.py new file mode 100644 index 0000000..46e1ca4 --- /dev/null +++ b/src/agents/geneticAlgAgentJon.py @@ -0,0 +1,46 @@ +import random +from src.game.tetris import * +from src.agents.agent_factory import create_agent +from src.agents.agent import Agent, play_game +# From paper: https://codemyroad.wordpress.com/2013/04/14/tetris-ai-the-near-perfect-player/ +# the weigts the author got: +# a x (Aggregate Height) + b x (Complete Lines) + c x (Holes) + d x (Bumpiness) +# a = -0.510066 b = 0.760666 c = -0.35663 d = -0.184483 +# TODO Read the part of the article about the genetic algorithm +# TODO Create a fitness function + +# TODO Create a genetic algorithm based on the + +# List over vectors with boards attached: +# [(List over parameters, board), ...] + +# TODO create init-method that creates agents with random vectors +# TODO create run_games-method that goes through the agents, and play 100 games each, return average lines cleared +# TODO create method for fetching a random 10%, and finds the two with highest lines cleared, and makes a child (with 5% chance of mutation) +# TODO create method that makes 30% new agents from existing agents (last method), replace worst 30% with the new agents + +list = [] + +for _ in range(0, 100): + agg_height = random.random(-1, 0) + max_height = random.random(-1, 0) + lines_cleared = random.random(0, 1) + bumpiness = random.random(-1, 0) + holes = random.random(-1, 0) + + game = Tetris() + agent: Agent = create_agent("heuristic") + total_cleared = 0 + for _ in range(0, 100): + board = play_game(agent, game, 5) + total_cleared += board.rowsRemoved + list.append = ([agg_height, max_height, lines_cleared, bumpiness, holes], total_cleared/100) + + + +def fitness_crossover(pop1: tuple(list[int], int), pop2: tuple(list[int], int)) -> tuple(list[int], int): + return_pop: tuple(list[int], int) + # Combines the two vectors proportionaly by how many lines they cleared + child_pop = pop1[1] * pop1[0] + pop2[1] * pop2[0] + + return tuple(child_pop, 0) \ No newline at end of file From 1916547e8fe96bd2e1db1749cad612897a75d882 Mon Sep 17 00:00:00 2001 From: Maia Austigard Date: Mon, 22 Apr 2024 20:05:17 +0200 Subject: [PATCH 50/67] feat: continue working on genetic algorithm agent Co-authored-by: Jon Bergland --- main.py | 27 ++-- src/agents/geneticAlgAgent.py | 3 +- src/agents/geneticAlgAgentJon.py | 130 +++++++++++++++--- src/agents/heuristic.py | 4 +- src/agents/heuristic_with_parameters_agent.py | 44 ++++++ 5 files changed, 176 insertions(+), 32 deletions(-) create mode 100644 src/agents/heuristic_with_parameters_agent.py diff --git a/main.py b/main.py index 2c5e19a..b5a5ec5 100644 --- a/main.py +++ b/main.py @@ -6,17 +6,24 @@ utility ) from src.agents.heuristic_trainer import train +from src.agents.geneticAlgAgentJon import GeneticAlgAgentJM if __name__ == "__main__": - game = Tetris() - agent: Agent = create_agent("heuristic") - sum_rows_removed = 0 - for i in range(10): - end_board = play_game(agent, game, 7) - end_board.printBoard() - sum_rows_removed += end_board.rowsRemoved + algAgent = GeneticAlgAgentJM() + algAgent.number_of_selection(1) + print(algAgent.getBestPop) - print(f"Average rows removed: {sum_rows_removed / 10}") + + + # game = Tetris() + # agent: Agent = create_agent("heuristic") + # sum_rows_removed = 0 + # for i in range(10): + # end_board = play_game(agent, game, 7) + # end_board.printBoard() + # sum_rows_removed += end_board.rowsRemoved + + # print(f"Average rows removed: {sum_rows_removed / 10}") # possible_moves = game.getPossibleBoards() # for boards in possible_moves: @@ -28,4 +35,6 @@ # manager.startGame() - #train() + # train() + + diff --git a/src/agents/geneticAlgAgent.py b/src/agents/geneticAlgAgent.py index 83f61a8..2511b93 100644 --- a/src/agents/geneticAlgAgent.py +++ b/src/agents/geneticAlgAgent.py @@ -4,4 +4,5 @@ # a = -0.510066 b = 0.760666 c = -0.35663 d = -0.184483 # TODO Read the part of the article about the genetic algorithm # TODO Create a fitness function -# TODO Create a genetic algorithm based on the \ No newline at end of file +# TODO Create a genetic algorithm based on the + diff --git a/src/agents/geneticAlgAgentJon.py b/src/agents/geneticAlgAgentJon.py index 46e1ca4..d2b2326 100644 --- a/src/agents/geneticAlgAgentJon.py +++ b/src/agents/geneticAlgAgentJon.py @@ -1,7 +1,8 @@ import random from src.game.tetris import * from src.agents.agent_factory import create_agent -from src.agents.agent import Agent, play_game +from src.agents.agent import Agent +from src.agents.heuristic_with_parameters_agent import * # From paper: https://codemyroad.wordpress.com/2013/04/14/tetris-ai-the-near-perfect-player/ # the weigts the author got: # a x (Aggregate Height) + b x (Complete Lines) + c x (Holes) + d x (Bumpiness) @@ -19,28 +20,117 @@ # TODO create method for fetching a random 10%, and finds the two with highest lines cleared, and makes a child (with 5% chance of mutation) # TODO create method that makes 30% new agents from existing agents (last method), replace worst 30% with the new agents -list = [] +class GeneticAlgAgentJM: + agents: list[list[list[float], float]] = [] -for _ in range(0, 100): - agg_height = random.random(-1, 0) - max_height = random.random(-1, 0) - lines_cleared = random.random(0, 1) - bumpiness = random.random(-1, 0) - holes = random.random(-1, 0) + def number_of_selection(self, number_of_selections: int): + self.agents = self.initAgents() + for i in range(0, number_of_selections): + # Select new pops + self.agents = self.replace_30_percent(self.agents) + + # Run new test + for i in range(len(self.agents)): + param_list = self.agents[i][0] + average_cleared = self.play_game(param_list[0], param_list[1], param_list[2], param_list[3], param_list[4]) + self.agents[i][1] = average_cleared + + + + def initAgents(self) -> list[list[list[float], float]]: + number_of_agents = 10 + for _ in range(0, number_of_agents): + agg_height = random.randrange(-1000, 0)/1000 + max_height = random.randrange(-1000, 0)/1000 + lines_cleared = random.randrange(0, 1000)/1000 + bumpiness = random.randrange(-1000, 0)/1000 + holes = random.randrange(-1000, 0)/1000 + + # agents = [] + average_cleared = self.play_game(agg_height, max_height, lines_cleared, bumpiness, holes) / number_of_agents + self.agents.append = ([agg_height, max_height, lines_cleared, bumpiness, holes], average_cleared) + print(_) + + # return agents - game = Tetris() - agent: Agent = create_agent("heuristic") - total_cleared = 0 - for _ in range(0, 100): - board = play_game(agent, game, 5) - total_cleared += board.rowsRemoved - list.append = ([agg_height, max_height, lines_cleared, bumpiness, holes], total_cleared/100) + + def play_game(self, agg_height, max_height, lines_cleared, bumpiness, holes): + + board = Tetris() + agent: Agent = HeuristicWithParametersAgent([agg_height, max_height, lines_cleared, bumpiness, holes]) + total_cleared = 0 + number_of_rounds = 10 + for _ in range(0, number_of_rounds): + + max_moves = number_of_rounds + move = 0 + actions_per_drop = 7 + + while not board.isGameOver() and move < max_moves: + # Get the result of the agent's action + for _ in range(actions_per_drop): + result = agent.result(board) + # Perform the action(s) on the board + if isinstance(result, list): + for action in result: + board.doAction(action) + else: + board.doAction(result) + + move += 1 + # Advance the game by one frame + board.doAction(Action.SOFT_DROP) + #board.printBoard() + + total_cleared += board.rowsRemoved + + return total_cleared + + def fitness_crossover(self, pop1: list[list[float], float], pop2: list[list[float], float]) -> list[list[float], float]: + # Combines the two vectors proportionaly by how many lines they cleared + child_pop = pop1[1] * pop1[0] + pop2[1] * pop2[0] + + return list(child_pop, 0) + + # TODO create method for fetching a random 10%, and finds the two with highest lines cleared, and makes a child (with 5% chance of mutation) + def paring_pop(self, pop_list: list[list[list[float], float]]) -> list[list[float], float]: + # Gets the number of pops to select + num_pops_to_select: int(len(pop_list) * 0.1) + + # Get a sample of pops based on the previous number + random_pop_sample: random.sample(pop_list, num_pops_to_select) + + # Gets the two pops with the highest lines cleared + highest_values: sorted(random_pop_sample, key=lambda x: x[1], reverse=True)[:2] + + # Gets the child pop of the two pops + new_pop = self.fitness_crossover(highest_values[0], highest_values[1]) + + # Mutate 5% of children pops + if random.random(0,1) < 0.2: + random_parameter = int(random.randint(0,4)) + new_pop[0][random_parameter] = (random.randrange(-200, 200)/1000) * new_pop[0][random_parameter] + + return new_pop -def fitness_crossover(pop1: tuple(list[int], int), pop2: tuple(list[int], int)) -> tuple(list[int], int): - return_pop: tuple(list[int], int) - # Combines the two vectors proportionaly by how many lines they cleared - child_pop = pop1[1] * pop1[0] + pop2[1] * pop2[0] + def replace_30_percent(self, pop_list: list[list[list[float], float]]) -> list[list[float], float]: + new_list: list[list[list[float], float]] + + # Number of pops needed for 30% of total number + num_pops_needed: int(len(pop_list) * 0.3) + + for _ in range(0, num_pops_needed): + new_list.append(self.paring_pop(pop_list)) + + pop_list: sorted(pop_list, key=lambda x: x[1], reverse=False)[:num_pops_needed] + + pop_list.append(new_list) + + return pop_list + - return tuple(child_pop, 0) \ No newline at end of file + def getBestPop(self) -> list[list[float], float]: + pop_list: sorted(pop_list, key, key=lambda x: x[1], reverse=False) + return pop_list[0] diff --git a/src/agents/heuristic.py b/src/agents/heuristic.py index 26eea48..992a3e8 100644 --- a/src/agents/heuristic.py +++ b/src/agents/heuristic.py @@ -3,8 +3,8 @@ from src.game.tetris import Tetris -def utility(gameState: Tetris, aggregate_heights_weight: int, max_height_weight: int, - lines_cleared_weight: int, bumpiness_weight: int, holes_weight: int) -> int: +def utility(gameState: Tetris, aggregate_heights_weight: float, max_height_weight: float, + lines_cleared_weight: float, bumpiness_weight: float, holes_weight: float) -> int: """Returns the utility of the given game state.""" sum = 0 diff --git a/src/agents/heuristic_with_parameters_agent.py b/src/agents/heuristic_with_parameters_agent.py new file mode 100644 index 0000000..0a2e8cd --- /dev/null +++ b/src/agents/heuristic_with_parameters_agent.py @@ -0,0 +1,44 @@ +from src.agents.agent import Agent +from src.game.tetris import Action, Tetris, transition_model, get_all_actions +from src.agents.heuristic import ( + utility +) + +class HeuristicWithParametersAgent(Agent): + + aggregate_heights_weight: float + max_height_weight: float + lines_cleared_weight: float + bumpiness_weight: float + holes_weight: float + + def __init__(self, params: list[float]): + self.aggregate_heights_weight = params[0] + self.max_height_weight = params[1] + self.lines_cleared_weight = params[2] + self.bumpiness_weight = params[3] + self.holes_weight = params[4] + + def result(self, board: Tetris) -> list[Action]: + # Get all possible boards + possible_boards = board.getPossibleBoards() + + best_board: Tetris + best_utility = float("-inf") + # Check which board has the best outcome based on the heuristic + for boards in possible_boards: + current_utility = utility(boards, self.aggregate_heights_weight, self.max_height_weight, + self.lines_cleared_weight, self.bumpiness_weight, self.holes_weight) + + if current_utility > best_utility: + best_board = boards + best_utility = current_utility + + + # Find the actions needed to transform the current board to the new board + actions = [] + try: + actions = transition_model(board, best_board) + return actions + except: + return actions From 1719762b7b1bc7f03f030215d1560441eee496e8 Mon Sep 17 00:00:00 2001 From: Maia Austigard Date: Tue, 23 Apr 2024 17:20:21 +0200 Subject: [PATCH 51/67] fix: optimize code --- main.py | 9 ++--- src/agents/agent.py | 8 ++-- src/agents/geneticAlgAgentJon.py | 67 +++++++++++++++++--------------- 3 files changed, 44 insertions(+), 40 deletions(-) diff --git a/main.py b/main.py index b5a5ec5..75e2897 100644 --- a/main.py +++ b/main.py @@ -9,11 +9,6 @@ from src.agents.geneticAlgAgentJon import GeneticAlgAgentJM if __name__ == "__main__": - algAgent = GeneticAlgAgentJM() - algAgent.number_of_selection(1) - print(algAgent.getBestPop) - - # game = Tetris() # agent: Agent = create_agent("heuristic") @@ -37,4 +32,8 @@ # train() + + algAgent = GeneticAlgAgentJM() + algAgent.number_of_selection(1) + print(algAgent.getBestPop()) diff --git a/src/agents/agent.py b/src/agents/agent.py index cc5838b..d46cd55 100644 --- a/src/agents/agent.py +++ b/src/agents/agent.py @@ -34,7 +34,7 @@ def result(board: Tetris) -> Union[Action, list[Action]]: pass -def play_game(agent: Agent, board: Tetris, max_count: int, actions_per_drop: int = 1) -> Tetris: +def play_game(agent: Agent, board: Tetris, actions_per_drop: int = 1) -> Tetris: """ Plays a game of Tetris with the given agent. @@ -46,9 +46,9 @@ def play_game(agent: Agent, board: Tetris, max_count: int, actions_per_drop: int Returns: The final state of the board after the game is over. """ - count = 0 + #count = 0 - while not board.isGameOver() or count < max_count: + while not board.isGameOver(): # Get the result of the agent's action for _ in range(actions_per_drop): result = agent.result(board) @@ -59,7 +59,7 @@ def play_game(agent: Agent, board: Tetris, max_count: int, actions_per_drop: int else: board.doAction(result) - count += 1 + #count += 1 # Advance the game by one frame board.doAction(Action.SOFT_DROP) #board.printBoard() diff --git a/src/agents/geneticAlgAgentJon.py b/src/agents/geneticAlgAgentJon.py index d2b2326..9171c3a 100644 --- a/src/agents/geneticAlgAgentJon.py +++ b/src/agents/geneticAlgAgentJon.py @@ -24,9 +24,10 @@ class GeneticAlgAgentJM: agents: list[list[list[float], float]] = [] def number_of_selection(self, number_of_selections: int): - self.agents = self.initAgents() + self.initAgents() for i in range(0, number_of_selections): # Select new pops + print(len(self.agents)) self.agents = self.replace_30_percent(self.agents) # Run new test @@ -36,9 +37,8 @@ def number_of_selection(self, number_of_selections: int): self.agents[i][1] = average_cleared - def initAgents(self) -> list[list[list[float], float]]: - number_of_agents = 10 + number_of_agents = 20 for _ in range(0, number_of_agents): agg_height = random.randrange(-1000, 0)/1000 max_height = random.randrange(-1000, 0)/1000 @@ -48,7 +48,7 @@ def initAgents(self) -> list[list[list[float], float]]: # agents = [] average_cleared = self.play_game(agg_height, max_height, lines_cleared, bumpiness, holes) / number_of_agents - self.agents.append = ([agg_height, max_height, lines_cleared, bumpiness, holes], average_cleared) + self.agents.append([[agg_height, max_height, lines_cleared, bumpiness, holes], average_cleared]) print(_) # return agents @@ -87,50 +87,55 @@ def play_game(self, agg_height, max_height, lines_cleared, bumpiness, holes): return total_cleared - def fitness_crossover(self, pop1: list[list[float], float], pop2: list[list[float], float]) -> list[list[float], float]: - # Combines the two vectors proportionaly by how many lines they cleared - child_pop = pop1[1] * pop1[0] + pop2[1] * pop2[0] - - return list(child_pop, 0) + def replace_30_percent(self, pop_list: list[list[list[float], float]]) -> list[list[float], float]: + new_list = []#: list[list[list[float], float]] + - # TODO create method for fetching a random 10%, and finds the two with highest lines cleared, and makes a child (with 5% chance of mutation) + # Number of pops needed for 30% of total number + num_pops_needed = int(len(pop_list) * 0.3) + + for _ in range(0, num_pops_needed): + new_list.append(self.paring_pop(pop_list)) # liste.append(liste[liste, float]) = liste[liste[liste, float]] + + pop_list: sorted(pop_list, key=lambda x: x[1], reverse=False)[:num_pops_needed] + + pop_list.extend(new_list) + + return pop_list + + + # TODO create method for fetching a random 10%, and finds the two with highest lines cleared, and makes a child (with 5% chance of mutation) def paring_pop(self, pop_list: list[list[list[float], float]]) -> list[list[float], float]: # Gets the number of pops to select - num_pops_to_select: int(len(pop_list) * 0.1) + num_pops_to_select = int(len(pop_list) * 0.1) # Get a sample of pops based on the previous number - random_pop_sample: random.sample(pop_list, num_pops_to_select) + random_pop_sample = random.sample(pop_list, num_pops_to_select) # Gets the two pops with the highest lines cleared - highest_values: sorted(random_pop_sample, key=lambda x: x[1], reverse=True)[:2] + highest_values = sorted(random_pop_sample, key=lambda x: x[1], reverse=True)[:2] # Gets the child pop of the two pops - new_pop = self.fitness_crossover(highest_values[0], highest_values[1]) + new_pop = self.fitness_crossover(highest_values[0], highest_values[1]) # liste[liste, float] # Mutate 5% of children pops - if random.random(0,1) < 0.2: + if random.randrange(0,1000)/1000 < 0.2: random_parameter = int(random.randint(0,4)) new_pop[0][random_parameter] = (random.randrange(-200, 200)/1000) * new_pop[0][random_parameter] - return new_pop - - - def replace_30_percent(self, pop_list: list[list[list[float], float]]) -> list[list[float], float]: - new_list: list[list[list[float], float]] - - # Number of pops needed for 30% of total number - num_pops_needed: int(len(pop_list) * 0.3) - - for _ in range(0, num_pops_needed): - new_list.append(self.paring_pop(pop_list)) - - pop_list: sorted(pop_list, key=lambda x: x[1], reverse=False)[:num_pops_needed] + return new_pop # liste[liste, float] - pop_list.append(new_list) - return pop_list + def fitness_crossover(self, pop1: list[list[float], float], pop2: list[list[float], float]) -> list[list[float], float]: + # Combines the two vectors proportionaly by how many lines they cleared + parent_pop1 = [h * pop1[1] for h in pop1[0]] + parent_pop2 = [h * pop2[1] for h in pop2[0]] + child_pop = [h1 + h2 for h1, h2 in zip(parent_pop1, parent_pop2)] + return [child_pop, 0.0] # liste[liste, float] + def getBestPop(self) -> list[list[float], float]: - pop_list: sorted(pop_list, key, key=lambda x: x[1], reverse=False) + pop_list = self.agents + pop_list = sorted(pop_list, key=lambda x: x[1], reverse=False) return pop_list[0] From c5d917f0f43fe762165a7d8f0939c47fd07c7ad0 Mon Sep 17 00:00:00 2001 From: Maia Austigard Date: Tue, 23 Apr 2024 17:57:17 +0200 Subject: [PATCH 52/67] refactor: optimize code Co-authored-by: Jon Bergland Co-authored-by: henrinha --- main.py | 2 +- src/agents/geneticAlgAgentJon.py | 31 +++++++++++-------------------- 2 files changed, 12 insertions(+), 21 deletions(-) diff --git a/main.py b/main.py index 75e2897..a771d8c 100644 --- a/main.py +++ b/main.py @@ -34,6 +34,6 @@ algAgent = GeneticAlgAgentJM() - algAgent.number_of_selection(1) + algAgent.number_of_selection(2) print(algAgent.getBestPop()) diff --git a/src/agents/geneticAlgAgentJon.py b/src/agents/geneticAlgAgentJon.py index 9171c3a..30980ab 100644 --- a/src/agents/geneticAlgAgentJon.py +++ b/src/agents/geneticAlgAgentJon.py @@ -35,7 +35,8 @@ def number_of_selection(self, number_of_selections: int): param_list = self.agents[i][0] average_cleared = self.play_game(param_list[0], param_list[1], param_list[2], param_list[3], param_list[4]) self.agents[i][1] = average_cleared - + print(self.getBestPop()) + def initAgents(self) -> list[list[list[float], float]]: number_of_agents = 20 @@ -46,12 +47,9 @@ def initAgents(self) -> list[list[list[float], float]]: bumpiness = random.randrange(-1000, 0)/1000 holes = random.randrange(-1000, 0)/1000 - # agents = [] - average_cleared = self.play_game(agg_height, max_height, lines_cleared, bumpiness, holes) / number_of_agents + average_cleared = self.play_game(agg_height, max_height, lines_cleared, bumpiness, holes) self.agents.append([[agg_height, max_height, lines_cleared, bumpiness, holes], average_cleared]) print(_) - - # return agents def play_game(self, agg_height, max_height, lines_cleared, bumpiness, holes): @@ -84,20 +82,16 @@ def play_game(self, agg_height, max_height, lines_cleared, bumpiness, holes): total_cleared += board.rowsRemoved - return total_cleared + return total_cleared / number_of_rounds def replace_30_percent(self, pop_list: list[list[list[float], float]]) -> list[list[float], float]: - new_list = []#: list[list[list[float], float]] - - # Number of pops needed for 30% of total number num_pops_needed = int(len(pop_list) * 0.3) - for _ in range(0, num_pops_needed): - new_list.append(self.paring_pop(pop_list)) # liste.append(liste[liste, float]) = liste[liste[liste, float]] + new_list = [self.paring_pop(pop_list) for _ in range(num_pops_needed)] - pop_list: sorted(pop_list, key=lambda x: x[1], reverse=False)[:num_pops_needed] + pop_list = sorted(pop_list, key=lambda x: x[1], reverse=False)[num_pops_needed:] pop_list.extend(new_list) @@ -116,26 +110,23 @@ def paring_pop(self, pop_list: list[list[list[float], float]]) -> list[list[floa highest_values = sorted(random_pop_sample, key=lambda x: x[1], reverse=True)[:2] # Gets the child pop of the two pops - new_pop = self.fitness_crossover(highest_values[0], highest_values[1]) # liste[liste, float] + new_pop = self.fitness_crossover(highest_values[0], highest_values[1]) # Mutate 5% of children pops if random.randrange(0,1000)/1000 < 0.2: random_parameter = int(random.randint(0,4)) new_pop[0][random_parameter] = (random.randrange(-200, 200)/1000) * new_pop[0][random_parameter] - return new_pop # liste[liste, float] + return new_pop def fitness_crossover(self, pop1: list[list[float], float], pop2: list[list[float], float]) -> list[list[float], float]: # Combines the two vectors proportionaly by how many lines they cleared - parent_pop1 = [h * pop1[1] for h in pop1[0]] - parent_pop2 = [h * pop2[1] for h in pop2[0]] - child_pop = [h1 + h2 for h1, h2 in zip(parent_pop1, parent_pop2)] - - return [child_pop, 0.0] # liste[liste, float] + child_pop = [h1 * pop1[1] + h2 * pop2[1] for h1, h2 in zip(pop1[0], pop2[0])] + return [child_pop, 0.0] def getBestPop(self) -> list[list[float], float]: pop_list = self.agents - pop_list = sorted(pop_list, key=lambda x: x[1], reverse=False) + pop_list = sorted(pop_list, key=lambda x: x[1], reverse=True) return pop_list[0] From 384e97e0ef4bea38bc77e25f577259bb47952fb5 Mon Sep 17 00:00:00 2001 From: Maia Austigard Date: Tue, 23 Apr 2024 18:11:35 +0200 Subject: [PATCH 53/67] refactor: optimize code --- src/agents/geneticAlgAgentJon.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/agents/geneticAlgAgentJon.py b/src/agents/geneticAlgAgentJon.py index 30980ab..4880d45 100644 --- a/src/agents/geneticAlgAgentJon.py +++ b/src/agents/geneticAlgAgentJon.py @@ -35,7 +35,8 @@ def number_of_selection(self, number_of_selections: int): param_list = self.agents[i][0] average_cleared = self.play_game(param_list[0], param_list[1], param_list[2], param_list[3], param_list[4]) self.agents[i][1] = average_cleared - print(self.getBestPop()) + + print(self.getBestPop()) def initAgents(self) -> list[list[list[float], float]]: From 70e7e561b063331e91104354d805314118c206e5 Mon Sep 17 00:00:00 2001 From: Jon Bergland Date: Tue, 23 Apr 2024 18:56:43 +0200 Subject: [PATCH 54/67] refactor: optimize the equals function in tetris.py --- src/game/tetris.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/game/tetris.py b/src/game/tetris.py index ec45feb..f05c093 100644 --- a/src/game/tetris.py +++ b/src/game/tetris.py @@ -1,5 +1,6 @@ import random import copy +import numpy as np from enum import Enum, auto @@ -281,12 +282,12 @@ def __eq__(self, other: "Tetris") -> bool: return False # Check if the blocks are the same - for r in range(self.ROWS): - for c in range(self.COLUMNS): - if self.board[r][c] != other.board[r][c]: - return False + # for r in range(self.ROWS): + # for c in range(self.COLUMNS): + # if self.board[r][c] != other.board[r][c]: + # return False - return True + return np.array_equal(self.board, other.board) def printBoard(self): print("_______________________________________") From d145c8c2ca13f48fbc63c93731b9ddd00618936e Mon Sep 17 00:00:00 2001 From: Maia Austigard Date: Tue, 23 Apr 2024 18:58:14 +0200 Subject: [PATCH 55/67] feat: add normalizing of new vector --- src/agents/geneticAlgAgentJon.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/agents/geneticAlgAgentJon.py b/src/agents/geneticAlgAgentJon.py index 4880d45..d28393c 100644 --- a/src/agents/geneticAlgAgentJon.py +++ b/src/agents/geneticAlgAgentJon.py @@ -1,4 +1,5 @@ import random +import numpy as np from src.game.tetris import * from src.agents.agent_factory import create_agent from src.agents.agent import Agent @@ -58,7 +59,7 @@ def play_game(self, agg_height, max_height, lines_cleared, bumpiness, holes): board = Tetris() agent: Agent = HeuristicWithParametersAgent([agg_height, max_height, lines_cleared, bumpiness, holes]) total_cleared = 0 - number_of_rounds = 10 + number_of_rounds = 20 for _ in range(0, number_of_rounds): max_moves = number_of_rounds @@ -117,7 +118,9 @@ def paring_pop(self, pop_list: list[list[list[float], float]]) -> list[list[floa if random.randrange(0,1000)/1000 < 0.2: random_parameter = int(random.randint(0,4)) new_pop[0][random_parameter] = (random.randrange(-200, 200)/1000) * new_pop[0][random_parameter] - + + new_pop[0] = (new_pop[0] / np.linalg.norm(new_pop[0])).tolist() + return new_pop From 76913533e43b7bd543c5dc898cf6950afc8370aa Mon Sep 17 00:00:00 2001 From: Maia Austigard Date: Tue, 23 Apr 2024 19:42:37 +0200 Subject: [PATCH 56/67] feat: add copy-method for Tetris-object --- src/game/tetris.py | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/src/game/tetris.py b/src/game/tetris.py index f05c093..94a179d 100644 --- a/src/game/tetris.py +++ b/src/game/tetris.py @@ -55,7 +55,7 @@ class Tetris: START_X = 3 START_Y = 0 - def __init__(self, board: list[list[int]] = None, block: Block = None): + def __init__(self, board: list[list[int]] = None, block: Block = None, nextBlock: Block = None): """ Initializes a new game board instance, setting up an empty board, placing the first block, and selecting the next block. """ @@ -70,8 +70,12 @@ def __init__(self, board: list[list[int]] = None, block: Block = None): self.block = Block(self.START_X, self.START_Y, 0) else: self.block = block - self.prevBoard = copy.deepcopy(self.board) + if nextBlock == None: + self.nextBlock = Block(self.START_X, self.START_Y, 0) + else: + self.nextBlock = nextBlock + self.prevBoard = copy.deepcopy(self.board) self._placeBlock() self.prevBlock = self.block.copy() @@ -253,10 +257,10 @@ def getPossibleBoards(self) -> list["Tetris"]: else: rotations = 1 - rotationBoard = copy.deepcopy(self) + rotationBoard = self.copy() for _ in range(rotations): for column in range(0, self.COLUMNS): - moveBoard = copy.deepcopy(rotationBoard) + moveBoard = rotationBoard.copy() # Calibrate the to the left toLeft = moveBoard.block.x @@ -281,13 +285,11 @@ def __eq__(self, other: "Tetris") -> bool: if not isinstance(other, Tetris): return False - # Check if the blocks are the same - # for r in range(self.ROWS): - # for c in range(self.COLUMNS): - # if self.board[r][c] != other.board[r][c]: - # return False - return np.array_equal(self.board, other.board) + + def copy(self) -> "Tetris": + tetris = Tetris(self.board, self.block, self.nextBlock) + return tetris def printBoard(self): print("_______________________________________") From adedbfda37f770444e3229784a982f06b458ab04 Mon Sep 17 00:00:00 2001 From: Maia Austigard Date: Tue, 23 Apr 2024 20:04:12 +0200 Subject: [PATCH 57/67] feat: add deep_copy-method for copying board --- src/game/tetris.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/src/game/tetris.py b/src/game/tetris.py index 94a179d..fd12e10 100644 --- a/src/game/tetris.py +++ b/src/game/tetris.py @@ -92,7 +92,20 @@ def _initBoard(self) -> list[list[int]]: return board def getBoard(self) -> list[list[int]]: - return copy.deepcopy(self.board) + return self.deep_copy_list_of_lists(self.board) + + def deep_copy_list_of_lists(self, original): + if not isinstance(original, list): + return original # Base case: return non-list elements directly + + copied = [] + for sublist in original: + if isinstance(sublist, list): + copied.append(self.deep_copy_list_of_lists(sublist)) # Recursively deep copy nested lists + else: + raise TypeError("Input must be a list of lists of integers") + + return copied def doAction(self, action: Action) -> None: """ @@ -288,7 +301,7 @@ def __eq__(self, other: "Tetris") -> bool: return np.array_equal(self.board, other.board) def copy(self) -> "Tetris": - tetris = Tetris(self.board, self.block, self.nextBlock) + tetris = Tetris(self.deep_copy_list_of_lists(self.board), self.block.copy(), self.nextBlock.copy()) return tetris def printBoard(self): From 39fcfd880508e56916f4ac72d7659660a86d482f Mon Sep 17 00:00:00 2001 From: Jon Bergland Date: Tue, 23 Apr 2024 23:46:58 +0200 Subject: [PATCH 58/67] refactor: change from copy.deepcopy to self defined copy-method --- src/game/tetris.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/game/tetris.py b/src/game/tetris.py index fd12e10..4098197 100644 --- a/src/game/tetris.py +++ b/src/game/tetris.py @@ -1,5 +1,4 @@ import random -import copy import numpy as np from enum import Enum, auto @@ -66,20 +65,21 @@ def __init__(self, board: list[list[int]] = None, block: Block = None, nextBlock self.board = self._initBoard() else: self.board = board + if block == None: self.block = Block(self.START_X, self.START_Y, 0) else: self.block = block + if nextBlock == None: - self.nextBlock = Block(self.START_X, self.START_Y, 0) + self.nextBlock = Block(self.START_X, self.START_Y, random.randint(0, 6)) else: self.nextBlock = nextBlock - self.prevBoard = copy.deepcopy(self.board) + self.prevBoard = self.deep_copy_list_of_lists(self.board) self._placeBlock() self.prevBlock = self.block.copy() - self.nextBlock = Block(self.START_X, self.START_Y, random.randint(0, 6)) def _initBoard(self) -> list[list[int]]: """Initializes an empty the board""" @@ -103,7 +103,7 @@ def deep_copy_list_of_lists(self, original): if isinstance(sublist, list): copied.append(self.deep_copy_list_of_lists(sublist)) # Recursively deep copy nested lists else: - raise TypeError("Input must be a list of lists of integers") + copied.append(sublist) return copied @@ -145,7 +145,7 @@ def doAction(self, action: Action) -> None: self._checkForFullRows() self._checkGameOver() # Store the previous board state before the new block placement - self.prevBoard = copy.deepcopy(self.board) + self.prevBoard = self.deep_copy_list_of_lists(self.board) self._shiftToNewBlock() def isValidBlockPosition(self, block: Block) -> bool: @@ -207,7 +207,7 @@ def isGameOver(self): def _placeBlock(self): """Places the current block on the board""" - self.board = copy.deepcopy(self.prevBoard) + self.board = self.deep_copy_list_of_lists(self.prevBoard) for i in range(4): for j in range(4): if i * 4 + j in self.block.image(): @@ -257,7 +257,7 @@ def _clearRow(self, rownumber: int): newMatrix.insert(0, [0 for _ in range(self.COLUMNS)]) self.board = newMatrix self.rowsRemoved += 1 - self.prevBoard = copy.deepcopy(self.board) + self.prevBoard = self.deep_copy_list_of_lists(self.board) def getPossibleBoards(self) -> list["Tetris"]: possibleMoves = [] @@ -301,7 +301,7 @@ def __eq__(self, other: "Tetris") -> bool: return np.array_equal(self.board, other.board) def copy(self) -> "Tetris": - tetris = Tetris(self.deep_copy_list_of_lists(self.board), self.block.copy(), self.nextBlock.copy()) + tetris = Tetris(self.deep_copy_list_of_lists(self.prevBoard), self.block.copy(), self.nextBlock.copy()) return tetris def printBoard(self): From b75e8f7b666a2da6e7cc08b5cf90b70c63c123c2 Mon Sep 17 00:00:00 2001 From: Jon Bergland Date: Wed, 24 Apr 2024 00:14:41 +0200 Subject: [PATCH 59/67] refactor: make use of list comprehensions in deepcopy-method in tetris.py --- src/game/tetris.py | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/src/game/tetris.py b/src/game/tetris.py index 4098197..d77c351 100644 --- a/src/game/tetris.py +++ b/src/game/tetris.py @@ -1,4 +1,5 @@ import random +from copy import copy import numpy as np from enum import Enum, auto @@ -94,17 +95,8 @@ def _initBoard(self) -> list[list[int]]: def getBoard(self) -> list[list[int]]: return self.deep_copy_list_of_lists(self.board) - def deep_copy_list_of_lists(self, original): - if not isinstance(original, list): - return original # Base case: return non-list elements directly - - copied = [] - for sublist in original: - if isinstance(sublist, list): - copied.append(self.deep_copy_list_of_lists(sublist)) # Recursively deep copy nested lists - else: - copied.append(sublist) - + def deep_copy_list_of_lists(self, original: list[list[int]]) -> list[list[int]]: + copied = [row[:] for row in original] return copied def doAction(self, action: Action) -> None: From cb5d5393a1659946bc830c08a5e1a82371da1915 Mon Sep 17 00:00:00 2001 From: Jon Bergland Date: Wed, 24 Apr 2024 23:10:49 +0200 Subject: [PATCH 60/67] refactor: optimize _outOfBounds, _intersects, isValidBlockPosition and equals methods --- src/game/tetris.py | 33 ++++++++++----------------------- 1 file changed, 10 insertions(+), 23 deletions(-) diff --git a/src/game/tetris.py b/src/game/tetris.py index d77c351..5336a63 100644 --- a/src/game/tetris.py +++ b/src/game/tetris.py @@ -150,28 +150,19 @@ def isValidBlockPosition(self, block: Block) -> bool: Returns: bool: True if the block's position is valid, False otherwise. """ - - if self._outOfBounds(block): - return False - - if self._intersects(block): - return False - - if self.isGameOver(): - return False - - return True + return not (self._outOfBounds(block) or self._intersects(block) or self.isGameOver()) def _outOfBounds(self, block: Block) -> bool: """Checks if the block is out of bounds""" for row in range(4): for column in range(4): if row * 4 + column in block.image(): + block_x, block_y = block.x + column, block.y + row if ( - row + block.y > self.ROWS - 1 - or row + block.y < 0 - or column + block.x > self.COLUMNS - 1 - or column + block.x < 0 + block_y > self.ROWS - 1 + or block_y < 0 + or block_x > self.COLUMNS - 1 + or block_x < 0 ): return True @@ -184,13 +175,9 @@ def _intersects(self, block: Block) -> bool: if row * 4 + column in block.image(): # Check if the block intersects with the board # That is, if the block is on top of another block that is not itself - blockOverlaps = self.prevBoard[row + block.y][column + block.x] > 0 - isItSelf = ( - block.x + column == self.block.x - and block.y + row == self.block.y - ) - - if blockOverlaps and not isItSelf: + block_x, block_y = block.x + column, block.y + row + prev_value = self.prevBoard[block_y][block_x] + if prev_value > 0 and (block_x, block_y) != (self.block.x, self.block.y): return True return False @@ -290,7 +277,7 @@ def __eq__(self, other: "Tetris") -> bool: if not isinstance(other, Tetris): return False - return np.array_equal(self.board, other.board) + return self.board == other.board def copy(self) -> "Tetris": tetris = Tetris(self.deep_copy_list_of_lists(self.prevBoard), self.block.copy(), self.nextBlock.copy()) From ee028a23f52d9c32e4fffcbb44c169bd0885a15b Mon Sep 17 00:00:00 2001 From: Jon Bergland Date: Thu, 25 Apr 2024 00:12:00 +0200 Subject: [PATCH 61/67] feat: add tests for equal method in tetris --- test/game/test_board.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/test/game/test_board.py b/test/game/test_board.py index 4e20900..4bdcca9 100644 --- a/test/game/test_board.py +++ b/test/game/test_board.py @@ -35,6 +35,11 @@ def test_board_equal_for_the_same_object(): board1 = Tetris() assert board1 == board1 +def test_board_equal_for_equal_different_objects(): + board1 = Tetris() + board2 = board1.copy() + assert board1 == board1 + def test_clear_row(): board: Tetris = Tetris() From 6eaeca737fea2faf49dc0b19136e9b404a08f423 Mon Sep 17 00:00:00 2001 From: Jon Bergland Date: Thu, 25 Apr 2024 01:04:50 +0200 Subject: [PATCH 62/67] refactor: combine aggregate height, max_height and bumpiness --- src/agents/heuristic.py | 36 ++++++++++++++++++++++++++++++----- test/agents/test_heuristic.py | 27 +++++++++++++------------- 2 files changed, 45 insertions(+), 18 deletions(-) diff --git a/src/agents/heuristic.py b/src/agents/heuristic.py index 992a3e8..bd7b143 100644 --- a/src/agents/heuristic.py +++ b/src/agents/heuristic.py @@ -7,15 +7,42 @@ def utility(gameState: Tetris, aggregate_heights_weight: float, max_height_weigh lines_cleared_weight: float, bumpiness_weight: float, holes_weight: float) -> int: """Returns the utility of the given game state.""" sum = 0 + aggregate, max_height, bumpiness = calculate_heights(gameState) - sum += aggregate_heights_weight * aggregate_heights(gameState) - sum += max_height_weight * max_height(gameState) + sum += aggregate_heights_weight * aggregate + sum += max_height_weight * max_height sum += lines_cleared_weight * lines_cleaned(gameState) - sum += bumpiness_weight * bumpiness(gameState) + sum += bumpiness_weight * bumpiness sum += holes_weight * find_holes(gameState) return sum +def calculate_heights(gameState: Tetris) -> tuple[int, int, int]: + """Calculates the sum and maximum height of the columns in the game state.""" + #sum_heights = 0 + max_height = 0 + checked_list = [0] * gameState.COLUMNS + + + total_bumpiness = 0 + columnHeightMap = {0: 0, 1: 0, 2: 0, 3: 0, 4: 0, 5: 0, 6: 0, 7: 0, 8: 0, 9: 0} + + + + for row in range(gameState.ROWS - 1, -1, -1): + for column in range(gameState.COLUMNS): + if gameState.prevBoard[row][column] != 0: + height = gameState.ROWS - row + checked_list[column] = height + max_height = max(max_height, height) + columnHeightMap[column] = gameState.ROWS - row + + + for key in range(gameState.COLUMNS - 1): + total_bumpiness += abs(columnHeightMap[key] - columnHeightMap[key + 1]) + + + return sum(checked_list), max_height , total_bumpiness def aggregate_heights(gameState: Tetris) -> int: """Returns the sum of the heights of the columns in the game state.""" @@ -51,13 +78,12 @@ def lines_cleaned(gameState: Tetris) -> int: def bumpiness(gameState: Tetris) -> int: """Returns the sum of the absolute height between all the columns""" total_bumpiness = 0 - max_height = 20 columnHeightMap = {0: 0, 1: 0, 2: 0, 3: 0, 4: 0, 5: 0, 6: 0, 7: 0, 8: 0, 9: 0} for column in range(gameState.COLUMNS): for row in range(gameState.ROWS): if gameState.prevBoard[row][column] > 0: if columnHeightMap[column] == 0: - columnHeightMap[column] = max_height - row + columnHeightMap[column] = gameState.ROWS - row for key in range(gameState.COLUMNS - 1): total_bumpiness += abs(columnHeightMap[key] - columnHeightMap[key + 1]) diff --git a/test/agents/test_heuristic.py b/test/agents/test_heuristic.py index 25f69fe..8a9eb53 100644 --- a/test/agents/test_heuristic.py +++ b/test/agents/test_heuristic.py @@ -27,11 +27,10 @@ def test_heuristic_height_aggregate_empty_board(): [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], ] board = Tetris(initBoard) - assert aggregate_heights(board) == 0 + assert calculate_heights(board)[0] == 0, "Expected aggregate height of 0 for an empty board" def test_heuristic_aggregate_with_equal_heights(): - initBoard = [ [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], @@ -57,7 +56,7 @@ def test_heuristic_aggregate_with_equal_heights(): ] board = Tetris(initBoard) expected = 3 * 9 - assert aggregate_heights(board) == expected + assert calculate_heights(board)[0] == expected def test_heuristic_high_line_heights(): @@ -86,7 +85,7 @@ def test_heuristic_high_line_heights(): ] board = Tetris(initBoard) expected = 3 * 9 - assert aggregate_heights(board) == expected + assert calculate_heights(board)[0] == expected def test_heuristic_different_heights(): @@ -115,7 +114,7 @@ def test_heuristic_different_heights(): ] board = Tetris(initBoard) expected = 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10 - assert aggregate_heights(board) == expected + assert calculate_heights(board)[0] == expected def test_max_height_empty_board(): @@ -142,7 +141,7 @@ def test_max_height_empty_board(): [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], ] board = Tetris(initBoard) - assert max_height(board) == 0, "Expected max height of 0 for an empty board" + assert calculate_heights(board)[1] == 0, "Expected max height of 0 for an empty board" def test_max_height_equal_heights(): @@ -170,7 +169,7 @@ def test_max_height_equal_heights(): ] board = Tetris(initBoard) assert ( - max_height(board) == 20 + calculate_heights(board)[1] == 20 ), "Expected max height of 20 for a board with equal heights" @@ -199,7 +198,7 @@ def test_max_height_takes_highest(): ] board = Tetris(initBoard) assert ( - max_height(board) == 20 + calculate_heights(board)[1] == 20 ), "Expected max height of 20 for a single column with height 20" @@ -335,7 +334,9 @@ def test_bumpiness_empty(): [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], ] - assert bumpiness(board) == 0 + + assert calculate_heights(board)[2] == 0 + #assert bumpiness(board) == 0 def test_bumpiness_five(): @@ -363,7 +364,7 @@ def test_bumpiness_five(): [1, 1, 1, 1, 1, 1, 1, 1, 1, 1], ] board = Tetris(initBoard) - assert bumpiness(board) == 2 + assert calculate_heights(board)[2] == 2 def test_bumpiness_nine(): @@ -391,7 +392,7 @@ def test_bumpiness_nine(): [1, 0, 1, 0, 1, 0, 1, 0, 1, 0], ] board = Tetris(initBoard) - assert bumpiness(board) == 9 + assert calculate_heights(board)[2] == 9 def test_bumpiness_with_holes(): @@ -419,7 +420,7 @@ def test_bumpiness_with_holes(): [1, 1, 1, 0, 1, 0, 1, 0, 1, 0], ] board = Tetris(initBoard) - assert bumpiness(board) == 0 + assert calculate_heights(board)[2] == 0 def test_bumpiness_40(): @@ -447,7 +448,7 @@ def test_bumpiness_40(): [1, 0, 1, 0, 1, 0, 1, 0, 1, 0], ] board = Tetris(initBoard) - assert bumpiness(board) == 40 + assert calculate_heights(board)[2] == 40 def test_aggregate_height_zero(): From 0d155aba4ef34984b49bcd3abb11f83b498fb00d Mon Sep 17 00:00:00 2001 From: Eduard Prokhorikhin Date: Sat, 27 Apr 2024 19:49:39 +0200 Subject: [PATCH 63/67] Reapply "3 make tetris board" This reverts commit 06fed5edbafdbc8225c6fa03e4ae2696e89fdaff. --- .devcontainer/devcontainer.json | 41 -------------------------- docs/guide/{env.md => devContainer.md} | 0 docs/guide/venv.md | 25 ++++++++++++++++ docs/planning/12.03.2024.md | 14 +++++++++ 4 files changed, 39 insertions(+), 41 deletions(-) delete mode 100644 .devcontainer/devcontainer.json rename docs/guide/{env.md => devContainer.md} (100%) create mode 100644 docs/guide/venv.md create mode 100644 docs/planning/12.03.2024.md 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/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 From b4e8badc5166095166fd9734c4dfe594076fcf8f Mon Sep 17 00:00:00 2001 From: Eduard Prokhorikhin Date: Sat, 27 Apr 2024 20:01:52 +0200 Subject: [PATCH 64/67] fix: fixed deepcopy referance in getPossibleBoards --- src/game/tetris.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/game/tetris.py b/src/game/tetris.py index 8e2682e..a6dfb10 100644 --- a/src/game/tetris.py +++ b/src/game/tetris.py @@ -280,7 +280,7 @@ def getPossibleBoards(self) -> list["Tetris"]: if not moveBoard.isValidBlockPosition(moveBoard.block): continue - moveBoard.prevBoard = copy.deepcopy(moveBoard.board) + moveBoard.prevBoard = moveBoard.deep_copy_list_of_lists(moveBoard.board) if moveBoard not in possibleMoves: possibleMoves.append(moveBoard) From 22b514b930f5083fb78ebf96d964671c5f2c74a1 Mon Sep 17 00:00:00 2001 From: Eduard Prokhorikhin Date: Sat, 27 Apr 2024 22:58:25 +0200 Subject: [PATCH 65/67] feat: add test code in main.py --- main.py | 30 ++++++++++++++++++++++-------- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/main.py b/main.py index 5868091..9877935 100644 --- a/main.py +++ b/main.py @@ -8,6 +8,16 @@ 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__": # game = Tetris() @@ -25,16 +35,20 @@ # print(utility(boards, 0, -1, 0, 0, 0)) # boards.printBoard() - board = Tetris() - manager = TetrisGameManager(board) - agent = create_agent("heuristic") + # board = Tetris() + # manager = TetrisGameManager(board) + # agent = create_agent("heuristic") - # manager.startGame() + # # manager.startGame() - # train() + # # train() - algAgent = GeneticAlgAgentJM() - algAgent.number_of_selection(2) - print(algAgent.getBestPop()) + # algAgent = GeneticAlgAgentJM() + # algAgent.number_of_selection(2) + # print(algAgent.getBestPop()) + + test() + + # cProfile.run('main()', 'restats') From 43908eb74f7dfe66c554d614534485ce70ecbcf0 Mon Sep 17 00:00:00 2001 From: Eduard Prokhorikhin Date: Sun, 28 Apr 2024 17:21:50 +0200 Subject: [PATCH 66/67] test: added more complex test for getPossibleBoards --- test/game/test_board.py | 211 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 210 insertions(+), 1 deletion(-) diff --git a/test/game/test_board.py b/test/game/test_board.py index e1b4c84..cae5a08 100644 --- a/test/game/test_board.py +++ b/test/game/test_board.py @@ -29,7 +29,216 @@ def test_get_possible_moves_for_square(): 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 = Tetris() From e5e4a7263dd013610de441fb1b47da77315277f9 Mon Sep 17 00:00:00 2001 From: Eduard Prokhorikhin Date: Mon, 29 Apr 2024 12:44:17 +0200 Subject: [PATCH 67/67] test: added complex test for transition model --- test/game/test_board.py | 50 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/test/game/test_board.py b/test/game/test_board.py index cae5a08..885fb8e 100644 --- a/test/game/test_board.py +++ b/test/game/test_board.py @@ -504,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