Skip to content

Commit

Permalink
Merged dev into main as game completed
Browse files Browse the repository at this point in the history
Co-authored-by: SindreFossdal <[email protected]>
  • Loading branch information
Eduard-Prokhorikhin and SindreFossdal committed Apr 12, 2024
2 parents e91d84b + c7baeaa commit 309d19c
Show file tree
Hide file tree
Showing 19 changed files with 2,063 additions and 30 deletions.
30 changes: 15 additions & 15 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ name: Tetris AI CI

on:
push:
branches: [ "dev" ]
branches: ["main", "dev"]
pull_request:
branches: [ "dev" ]
branches: ["main", "dev"]

permissions:
contents: read
Expand All @@ -17,16 +17,16 @@ jobs:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v3
- name: Set up Python 3.12
uses: actions/setup-python@v3
with:
python-version: "3.12"
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install pytest
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
- name: Test with pytest
run: |
pytest --rootdir=test
- uses: actions/checkout@v3
- name: Set up Python 3.12
uses: actions/setup-python@v3
with:
python-version: "3.12"
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install pytest
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
- name: Test with pytest
run: |
pytest --rootdir=test
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -158,3 +158,5 @@ cython_debug/
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/

.vscode/
11 changes: 9 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,16 @@

<!-- TODO -->

## Installation
## Setup
### Prerequisites
- Ensure that git is installed on your machine. [Download Git](https://git-scm.com/downloads)
- Ensure Python 3.12 or newer is installed on your machine. [Download Python](https://www.python.org/downloads/)

<!-- TODO -->
### Clone the repository
```bash
git clone https://github.com/CogitoNTNU/TetrisAI.git
cd TetrisAI
```

## Contributors

Expand Down
8 changes: 8 additions & 0 deletions main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
from src.game.TetrisGameManager import TetrisGameManager
from src.game.board import Board


if __name__ == "__main__":
board = Board()
game = TetrisGameManager(board)
game.startGame()
12 changes: 5 additions & 7 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
gitdb==4.0.11
GitPython==3.1.41
colorama==0.4.6
iniconfig==2.0.0
packaging==23.2
packaging==24.0
pluggy==1.4.0
pygame==2.5.2
pytest==8.0.2
setuptools==69.0.3
smmap==5.0.1
wheel==0.42.0
pynput==1.7.6
pytest==8.1.1
six==1.16.0
63 changes: 63 additions & 0 deletions src/agents/agent.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
""""
This module contains the Agent class, which is the base
class for all agents in the simulation.
"""

from abc import ABC, abstractmethod
from typing import Any, Union

from src.game.board import Action, Board


class Agent(ABC):
"""Base class for all agents in the simulation."""

@classmethod
def __instancecheck__(cls, instance: Any) -> bool:
return cls.__subclasscheck__(type(instance))

@classmethod
def __subclasscheck__(cls, subclass: Any) -> bool:
return hasattr(subclass, "result") and callable(subclass.result)

@abstractmethod
def result(board: Board) -> Union[Action, list[Action]]:
"""
Determines the next move for the agent based on the current state of the board.
Args:
board (Board): The current state of the board.
Returns:
The next move for the agent. This can be a single action or a list of actions.
"""
pass


def play_game(agent: Agent, board: Board, actions_per_drop: int = 1) -> Board:
"""
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.
"""
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)
# Advance the game by one frame
board.doAction(Action.SOFT_DROP)
board.printBoard()

return board
13 changes: 13 additions & 0 deletions src/agents/agent_factory.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
""" This module contains the factory function for creating agents. """

from src.agents.agent import Agent
from src.agents.randomAgent import RandomAgent

def create_agent(agent_type: str) -> Agent:
""" Create an agent of the specified type. """

match agent_type.lower():
case 'random':
return RandomAgent()
case _:
raise ValueError(f'Unknown agent type: {agent_type}')
92 changes: 92 additions & 0 deletions src/agents/heuristic.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
""" The heuristic module contains the heuristics used by the agents. """

from src.game.board import Board


def utility(gameState: Board) -> int:
"""Returns the utility of the given game state."""
pass


def aggregate_heights(gameState: Board) -> int:
"""Returns the sum of the heights of the columns in the game state."""
checkedList = [0 for i in range(gameState.COLUMNS)]
for i in range(gameState.ROWS):
for j in range(gameState.COLUMNS):
if gameState.board[i][j] > 0:
if checkedList[j] == 0:
checkedList[j] = gameState.ROWS - i
return sum(checkedList)


def max_height(gameState: Board) -> int:
"""Returns the maximum height of the columns in the game state."""
checkedList = [0 for i in range(gameState.COLUMNS)]
for i in range(gameState.ROWS):
for j in range(gameState.COLUMNS):
if gameState.board[i][j] > 0:
if checkedList[j] == 0:
checkedList[j] = gameState.ROWS - i
return max(checkedList)


def lines_cleaned(gameState: Board) -> int:
"""Retrurns the number of lines cleared."""
sum = 0
for row in gameState.board:
if all(cell == 1 for cell in row):
sum += 1
return sum


def bumpiness(gameState: Board) -> 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.board[row][column] > 0:
if columnHeightMap[column] == 0:
columnHeightMap[column] = max_height - row

for key in range(gameState.COLUMNS - 1):
total_bumpiness += abs(columnHeightMap[key] - columnHeightMap[key + 1])
return total_bumpiness


def aggregate_height(gameState: Board) -> int:
"Returns the sum of all column-heights"
max_height = 20
total_height = 0
columnHeightMap = {0: 0, 1: 0, 2: 0, 3: 0, 4: 0, 5: 0, 6: 0, 7: 0, 8: 0, 9: 0}
for column in range(gameState.COLUMNS):
for row in range(gameState.ROWS):
if gameState.board[row][column] > 0:
if columnHeightMap[column] == 0:
columnHeightMap[column] = max_height - row

for key in range(gameState.COLUMNS):
total_height += columnHeightMap[key]
return total_height


def find_holes(gameState: Board) -> int:
"""Returns number of empty cells on the board.
Args:
gameState (Board): the state to check
Returns:
The heuristic value
"""
holes = 0
for i in range(gameState.COLUMNS):
top_block = 20
for j in range(gameState.ROWS):
if (gameState.board[j][i] == 1) and (j < top_block):
top_block = j
if (gameState.board[j][i] == 0) and (j > top_block):
holes += 1

return holes
14 changes: 14 additions & 0 deletions src/agents/randomAgent.py
Original file line number Diff line number Diff line change
@@ -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
94 changes: 94 additions & 0 deletions src/game/TetrisGameManager.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
from pynput.keyboard import Key, Listener
import time as t
import sys

from src.game.board import Action, Board

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
streak = 1

def __init__(self, board: Board):
self.board = board
self.score = 0
self.currentTime = int(round(t.time() * 1000))

self.switcher = {
Key.down: Action.SOFT_DROP,
Key.left: Action.MOVE_LEFT,
Key.right: Action.MOVE_RIGHT,
Key.space: Action.HARD_DROP,
Key.up: Action.ROTATE_CLOCKWISE,
}

def onPress(self, key):
# Default action if key not found
default_action = lambda: "Key not recognized"

# Get the function to execute based on the key, or default action
action = self.switcher.get(key, default_action)
self.movePiece(action)

def onRelease(self, key):
pass

def movePiece(self, direction: Action):
self.board.doAction(direction)
self.board.printBoard()

def isGameOver(self):
return self.board.isGameOver()

def startGame(self):
self.currentPiece = self.newPiece()
self.nextPiece = self.newPiece()
self.board.printBoard()

listener = Listener(on_press=self.onPress, on_release=self.onRelease)
listener.start()

while not self.board.gameOver:

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)

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

def stopGame(self):
self.board.gameOver = True
sys.exit()
Loading

0 comments on commit 309d19c

Please sign in to comment.