School Commit Init

This commit is contained in:
2024-08-31 12:07:21 +03:00
commit 0b130ee18c
2801 changed files with 4720552 additions and 0 deletions
@@ -0,0 +1,129 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
pip-wheel-metadata/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
.python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
@@ -0,0 +1,43 @@
# :video_game: Assignment 09
## Requirements
- You will be given one of the problems below to solve
- Use object oriented programming and layered architecture
- All modules with the exception of the UI will have specifications and PyUnit test cases
- The program must protect itself against the users invalid input
**NB!** We do not expect you to implement optimal play for the computer player. However, it should still employ a strategy when making its moves in order to attempt to win the game and provide an entertaining opponent for the human player. Minimally, the computer player should move to win the game whenever possible and should block the human players attempts at 1-move victory, whenever possible
**deadline is week 14**
## GUI Bonus (0.2P)
- In addition to the console-based user interface required, also implement a graphical user interface (GUI) for the program
- To receive the bonus, both user interfaces (menu-based and graphical) must use the same program layers. You have to be able to start the application with either user interface
## AI Bonus (0.2P)
- Implement computer AI using a [minimax algorithm](https://en.wikipedia.org/wiki/Minimax). Computer play should be competitive against the human player
- In the case where minimax cannot be applied (e.g. Battleship, which is not a [complete information](https://en.wikipedia.org/wiki/Complete_information) game), find a suitable alternative; talk to your lab professor about the bonus possibility in this case
## Best-of-FP Bonus (0.2P)
- This bonus will be awarded to the very best implementations. To receive it, you need to implement both the **GUI** and **AI** bonuses, follow all implementation requirements, and have your work be selected by the laboratory professor
- These implementations will be part of a separate GitHub repository that we aim to make publicly accessible in order to feature some of our students' best work during this semester
## Problem Statements
### Connect Four
The game is described [here](https://en.wikipedia.org/wiki/Connect_Four)
### Gomoku
The game is described [here](https://en.wikipedia.org/wiki/Gomoku)
### Obstruction
The game is described [here](http://www.papg.com/show?2XMX)
### Battleship
The game is described [here](https://en.wikipedia.org/wiki/Battleship_(game))
### Planes
The game is described [here](https://ro.wikipedia.org/wiki/Avioane_(joc))
### Nine men's morris
The game is described [here](https://en.wikipedia.org/wiki/Nine_men%27s_morris)
### Other games!
You are free to implement a different board game, as long as its complexity is similar to those above. Talk to your laboratory professor to validate your idea before starting work!
@@ -0,0 +1,14 @@
from src.Game.game import Game
from src.Board.board import Board
from src.AI.ai import AI
from src.UI.ui import UI
def main() -> None:
board = Board()
ai = AI()
game = Game(board, ai)
ui = UI(game)
ui.start()
if __name__ == "__main__":
main()
@@ -0,0 +1,79 @@
from src.Board.board import Board
import random
class AI:
def __init__(self) -> None:
"""
:params:
None
:returns:
None
This is the constructor for the AI class. It is called when the game class is initialized. It does not take any parameters.
"""
def get_move(self, board_state: Board) -> int:
"""
:params:
board_state: Board
:returns:
int
This function is called by the game class to get the AI's move. It should return an integer between 0 and 6. This integer represents the column the AI wants to play in.
"""
move=self.__check_imeadiate_win_or_loss(board_state,2)
if move is not None:
return move
move=self.__check_imeadiate_win_or_loss(board_state,1)
if move is not None:
return move
moves=self.__get_all_possible_moves(board_state)
return random.choice(moves)
def __get_all_possible_moves(self, board_state: Board) -> list[int]:
"""
:params:
board_state: Board
:returns:
list[int]
This function returns a list of all possible moves the AI can make. This is used to randomly select a move if the AI can't win or lose in the next move.
"""
moves=[]
for i in range(7):
if board_state.check_cell(5,i)==None:
moves.append(i)
return moves
def __check_imeadiate_win_or_loss(self, board_state: Board,player: int) -> int:
"""
:params:
board_state: Board
player: int
:returns:
int
This function checks if the AI can win or lose in the next move. If it can, it returns the column it should play in. If it can't, it returns None.
"""
for column in range(7):
for row in range(6):
if column + 3 < 7:
to_check = [board_state.get_board()[column+i][row] for i in range(4)]
if to_check.count(player) == 3 and to_check.count(None) == 1 and (row==0 or board_state.check_cell(row-1,column+to_check.index(None)) is not None):
return column + to_check.index(None)
if row + 3 < 6:
to_check = [board_state.get_board()[column][row+i] for i in range(4)]
if to_check.count(player) == 3 and to_check.count(None) == 1:
return column
if column + 3 < 7 and row + 3 < 6:
to_check = [board_state.get_board()[column+i][row+i] for i in range(4)]
if to_check.count(player) == 3 and to_check.count(None) == 1 and (row-to_check.index(None) == 0 or board_state.check_cell(row+to_check.index(None)-1,column+to_check.index(None)) is not None):
return column + to_check.index(None)
if column + 3 < 7 and row - 3 >= 0:
to_check = [board_state.get_board()[column+i][row-i] for i in range(4)]
if to_check.count(player) == 3 and to_check.count(None) == 1 and (row-to_check.index(None) == 0 or board_state.check_cell(row-to_check.index(None)-1,column+to_check.index(None)) is not None):
return column + to_check.index(None)
return None
@@ -0,0 +1,100 @@
class Board:
def __init__(self) -> None:
"""
:params:
None
:return:
None
This is the constructor for the Board class. It sets up the board and the empty spaces. It also sets up the unicode characters for the circles that will be used to represent the pieces.
"""
self.__green_circle= "\033[32m\u2b24\033[0m"
self.__red_circle= "\033[31m\u2b24\033[0m"
self.__board = [[None for _ in range(6)] for _ in range(7)]
self.__empty_spaces = 42
def __str__(self) -> str:
return """
| {5} | {11} | {17} | {23} | {29} | {35} | {41} |
+---+---+---+---+---+---+---+
| {4} | {10} | {16} | {22} | {28} | {34} | {40} |
+---+---+---+---+---+---+---+
| {3} | {9} | {15} | {21} | {27} | {33} | {39} |
+---+---+---+---+---+---+---+
| {2} | {8} | {14} | {20} | {26} | {32} | {38} |
+---+---+---+---+---+---+---+
| {1} | {7} | {13} | {19} | {25} | {31} | {37} |
+---+---+---+---+---+---+---+
| {0} | {6} | {12} | {18} | {24} | {30} | {36} |
+---+---+---+---+---+---+---+
| 1 | 2 | 3 | 4 | 5 | 6 | 7 |
""".format(*[self.__green_circle if i==1 else self.__red_circle if i==2 else " " for row in self.__board for i in row])
def __repr__(self) -> str:
return str(self)
def place(self, column: int, player: int) -> int:
"""
:params:
column: int
player: int
:return:
int
This method places a piece in the board. It will raise an exception if the column is full. It returns the row where the piece was placed.
"""
for row in range(6):
if self.__board[column][row] == None:
self.__board[column][row] = player
self.__empty_spaces -= 1
return row
raise Exception("Column is full already!")
def is_full(self) -> bool:
"""
:params:
None
:return:
bool
This method returns True if the board is full, False otherwise.
"""
return self.__empty_spaces == 0
def reset(self) -> None:
"""
:params:
None
:return:
None
This method resets the board.
"""
self.__board = [[None for _ in range(6)] for _ in range(7)]
self.__empty_spaces = 42
def get_board(self) -> list[list[int]]:
"""
:params:
None
:return:
list[list[int]]
This method returns the board as a list of lists of ints. 1 represents a green piece, 2 represents a red piece and None represents an empty space.
"""
return self.__board
def check_cell(self, row: int, column: int) -> int:
"""
:params:
row: int
column: int
:return:
int
This method returns the value of the cell in the given row and column. 1 represents a green piece, 2 represents a red piece and None represents an empty space.
"""
return self.__board[column][row]
@@ -0,0 +1,203 @@
from src.Board.board import Board
from src.AI.ai import AI
import random
class Game:
def __init__(self, board: Board, ai: AI) -> None:
"""
:params:
board: Board
ai: AI
:return:
None
This is the constructor for the Game class. It takes a Board object and an AI object as parameters. It also sets the score to 0-0 and randomly chooses who goes first.
"""
self.__board = board
self.__ai = ai
self.__score = [0, 0]
self.__first_player = random.choice(["Player", "AI"])
if self.__first_player == "AI":
self.__play_ai()
def __str__(self) -> str:
return f" Connect 4 - Score : Player: {self.__score[0]} | AI: {self.__score[1]}"+str(self.__board)
def __repr__(self) -> str:
return str(self)
def play(self, column: int) -> None:
"""
:params:
column: int
:return:
None
This function is called by the main class to play a move. It takes an integer between 0 and 6 as a parameter. This integer represents the column the player wants to play in.
"""
lpo = self.__play_human(column)
if self.__check_win(lpo):
self.__score[0] += 1
return "Player"
if self.__board.is_full():
return "Draw"
lpo = self.__play_ai()
if self.__check_win(lpo):
self.__score[1] += 1
return "AI"
if self.__board.is_full():
return "Draw"
return None
def get_score(self) -> list[int]:
"""
:params:
None
:return:
list[int]
This method returns the score as a list of ints. The first int represents the player's score and the second int represents the AI's score.
"""
return self.__score
def __check_win(self, lpo: tuple[int,int]) -> bool:
"""
:params:
lpo: tuple[int,int]
:return:
bool
This method checks if the last piece played has won the game. It takes a tuple of ints as a parameter. This tuple represents the last piece played.
"""
return self.__check_column_win(lpo) or self.__check_row_win(lpo) or self.__check_diagonal1_win(lpo) or self.__check_diagonal2_win(lpo)
def __check_column_win(self, lpo: tuple[int,int]) -> bool:
"""
:params:
lpo: tuple[int,int]
:return:
bool
This method checks if the last piece played has won the game by checking the column. It takes a tuple of ints as a parameter. This tuple represents the last piece played.
"""
check = 0
for row in range(5):
if self.__board.check_cell(row, lpo[1]) == self.__board.check_cell(row + 1, lpo[1]) and self.__board.check_cell(row, lpo[1]) != None:
check += 1
else:
check = 0
if check == 3:
return True
return False
def __check_row_win(self, lpo: tuple[int,int]) -> bool:
"""
:params:
lpo: tuple[int,int]
:return:
bool
This method checks if the last piece played has won the game by checking the row. It takes a tuple of ints as a parameter. This tuple represents the last piece played.
"""
check = 0
for column in range(6):
if self.__board.check_cell(lpo[0], column) == self.__board.check_cell(lpo[0], column + 1) and self.__board.check_cell(lpo[0], column) != None:
check += 1
else:
check = 0
if check == 3:
return True
return False
def __check_diagonal1_win(self, lpo: tuple[int,int]) -> bool:
"""
:params:
lpo: tuple[int,int]
:return:
bool
This method checks if the last piece played has won the game by checking the first diagonal. It takes a tuple of ints as a parameter. This tuple represents the last piece played.
"""
check = 0
offset = -lpo[0] if lpo[0] < lpo[1] else -lpo[1]
while lpo[0]+offset < 5 and lpo[1]+offset < 6:
if self.__board.check_cell(lpo[0]+offset, lpo[1]+offset) == self.__board.check_cell(lpo[0]+offset+1, lpo[1]+offset+1) and self.__board.check_cell(lpo[0]+offset, lpo[1]+offset) != None:
check += 1
else:
check = 0
if check == 3:
return True
offset += 1
def __check_diagonal2_win(self, lpo: tuple[int,int]) -> bool:
"""
:params:
lpo: tuple[int,int]
:return:
bool
This method checks if the last piece played has won the game by checking the second diagonal. It takes a tuple of ints as a parameter. This tuple represents the last piece played.
"""
check = 0
offset = -lpo[0] if lpo[0] < 6-lpo[1] else lpo[1]-5
while lpo[0]+offset < 5 and lpo[1]-offset > 0:
if self.__board.check_cell(lpo[0]+offset, lpo[1]-offset) == self.__board.check_cell(lpo[0]+offset+1, lpo[1]-offset-1) and self.__board.check_cell(lpo[0]+offset, lpo[1]-offset) != None:
check += 1
else:
check = 0
if check == 3:
return True
offset += 1
def reset_board(self) -> None:
"""
:params:
None
:return:
None
This method resets the board and chooses a random player to start the game.
"""
self.__board.reset()
self.__first_player = random.choice(["Player", "AI"])
if self.__first_player == "AI":
self.__play_ai()
def reset_game(self) -> None:
"""
:params:
None
:return:
None
This method resets the game and the board.
"""
self.__score = [0, 0]
self.reset_board()
def __play_ai(self) -> tuple[int,int]:
"""
:params:
None
:return:
tuple[int,int]
This method plays the AI and returns the row and column of the last piece played.
"""
column = self.__ai.get_move(self.__board)
row = self.__board.place(column, 2)
return (row, column)
def __play_human(self, column: int) -> tuple[int,int]:
"""
:params:
column: int
:return:
tuple[int,int]
This method plays the human and returns the row and column of the last piece played.
"""
row = self.__board.place(column-1, 1)
return (row, column-1)
@@ -0,0 +1,38 @@
from src.Game.game import Game
class UI:
def __init__(self, game: Game) -> None:
self.game = game
def start(self) -> None:
print("\nTo play, enter a column number. To quit, enter 'quit'. To reset the board, enter 'reset'. To reset the game, enter 'reset game'.\n")
while True:
print(self.game)
command = input("Enter a column: ")
if command == "quit":
break
elif command == "reset":
self.game.reset_board()
elif command == "reset game":
self.game.reset_game()
elif command.isnumeric() and 0 < int(command) and int(command) < 8:
try:
winner = self.game.play(int(command))
if winner == "Draw":
print(self.game)
print("Draw!")
input("Press enter to continue")
self.game.reset_board()
continue
if winner is not None:
print(self.game)
print(f"{winner} won!")
input("Press enter to continue")
self.game.reset_board()
except Exception as e:
print(e)
else:
print("Invalid command")
@@ -0,0 +1,35 @@
import unittest
from src.Board.board import Board
from src.AI.ai import AI
class AI_Tester(unittest.TestCase):
def setUp(self) -> None:
self.ai=AI()
self.board=Board()
return super().setUp()
def tearDown(self) -> None:
self.ai=None
return super().tearDown()
def test_get_move_random(self):
self.board.reset()
for i in range(6):
self.board.place(0,i%2+1)
self.board.place(1,i%2+1)
self.board.place(2,(i+1)%2+1)
self.board.place(3,(i+1)%2+1)
self.board.place(5,i%2+1)
self.board.place(6,i%2+1)
self.assertEqual(self.ai.get_move(self.board),4)
def test_get_move_win(self):
self.board.reset()
self.board.place(0,2)
self.board.place(1,2)
self.board.place(2,2)
self.assertEqual(self.ai.get_move(self.board),3)
def test_get_move_block(self):
self.board.reset()
self.board.place(0,1)
self.board.place(1,1)
self.board.place(2,1)
self.assertEqual(self.ai.get_move(self.board),3)
@@ -0,0 +1,49 @@
import unittest
from src.Board.board import Board
class Board_Tester(unittest.TestCase):
def setUp(self) -> None:
self.board=Board()
return super().setUp()
def tearDown(self) -> None:
self.board=None
return super().tearDown()
def test_place(self):
self.board.reset()
for i in range(6):
self.board.place(0,1)
self.assertEqual(self.board.check_cell(i,0),1)
self.assertRaises(Exception,self.board.place,0,1)
def test_is_full(self):
self.board.reset()
for i in range(7):
for j in range(6):
self.assertFalse(self.board.is_full())
self.board.place(i,1)
self.assertTrue(self.board.is_full())
def test_reset(self):
self.board.reset()
self.board.place(0,1)
self.board.reset()
self.assertEqual(self.board.check_cell(0,0),None)
def test_get_board(self):
self.board.reset()
for i in range(7):
for j in range(6):
self.board.place(i,1)
board_state=self.board.get_board()
for i in range(7):
for j in range(6):
self.assertEqual(board_state[i][j],1)
def test_check_cell(self):
self.board.reset()
for i in range(7):
for j in range(6):
self.board.place(i,1)
for i in range(7):
for j in range(6):
self.assertEqual(self.board.check_cell(j,i),1)
@@ -0,0 +1,52 @@
from src.Game.game import Game
from src.Board.board import Board
from src.AI.ai import AI
import unittest
class Game_Tester(unittest.TestCase):
def setUp(self) -> None:
self.board=Board()
self.ai=AI()
self.game=Game(self.board,self.ai)
return super().setUp()
def tearDown(self) -> None:
self.game=None
return super().tearDown()
def test_play(self):
self.game.reset_game()
self.board.reset()
self.board.place(0,1)
self.board.place(1,1)
self.game.play(3)
self.assertEqual(self.board.check_cell(0,0),1)
self.assertEqual(self.board.check_cell(0,1),1)
self.assertEqual(self.board.check_cell(0,2),1)
self.assertEqual(self.board.check_cell(0,3),2)
def test_get_score(self):
self.game.reset_game()
self.assertListEqual(self.game.get_score(),[0,0])
self.board.reset()
self.board.place(0,1)
self.board.place(1,1)
self.board.place(2,1)
self.game.play(4)
self.assertListEqual(self.game.get_score(),[1,0])
def test_reset_board(self):
self.game.reset_board()
count=0
for i in range(7):
if self.board.check_cell(0,i)==None:
count+=1
self.assertGreaterEqual(count,6)
def test_reset_game(self):
self.game.reset_game()
count=0
for i in range(7):
if self.board.check_cell(0,i)==None:
count+=1
self.assertGreaterEqual(count,6)
self.assertListEqual(self.game.get_score(),[0,0])