|
import statistics |
|
import random |
|
|
|
class InvalidDropException(Exception): |
|
def __init__(self, message): |
|
self.message = message |
|
super().__init__(self.message) |
|
|
|
class GameState: |
|
def __init__(self): |
|
self.board: list[list[bool]] = [ |
|
[False, False, False, False], |
|
[False, False, False, False], |
|
[False, False, False, False], |
|
[False, False, False, False], |
|
] |
|
|
|
def __str__(self): |
|
ToReturn: str = "" |
|
ToReturn = " ββββββ" + "\n" |
|
onRow: int = 0 |
|
for row in self.board: |
|
|
|
ToReturn = ToReturn + str(onRow) + "β" |
|
|
|
|
|
for column in row: |
|
if column: |
|
ToReturn = ToReturn + "β" |
|
else: |
|
ToReturn = ToReturn + " " |
|
ToReturn = ToReturn + "β\n" |
|
onRow = onRow + 1 |
|
ToReturn = ToReturn + " ββββββ" |
|
ToReturn = ToReturn + "\n" + " 0123" |
|
return ToReturn |
|
|
|
def column_depths(self) -> list[int]: |
|
"""Calculates how 'deep' the available space on each column goes, from the top down.""" |
|
|
|
|
|
column_depths: list[int] = [0, 0, 0, 0] |
|
column_collisions: list[bool] = [ |
|
False, |
|
False, |
|
False, |
|
False, |
|
] |
|
|
|
|
|
for ri in range(0, len(self.board)): |
|
for ci in range( |
|
0, len(self.board[0]) |
|
): |
|
if ( |
|
column_collisions[ci] == False and self.board[ri][ci] == False |
|
): |
|
column_depths[ci] = column_depths[ci] + 1 |
|
else: |
|
column_collisions[ci] = True |
|
|
|
return column_depths |
|
|
|
def over(self) -> bool: |
|
"""Determines the game is over (if all cols in top row are occupied).""" |
|
return self.board[0] == [1, 1, 1, 1] |
|
|
|
def drop(self, column: int) -> float: |
|
"""Drops a single block into the column, returns the reward of doing so.""" |
|
if column < 0 or column > 3: |
|
raise InvalidDropException( |
|
"Invalid move! Column to drop in must be 0, 1, 2, or 3." |
|
) |
|
|
|
reward_before: float = self.score_plus() |
|
cds: list[int] = self.column_depths() |
|
if cds[column] == 0: |
|
raise InvalidDropException( |
|
"Unable to drop on column " + str(column) + ", it is already full!" |
|
) |
|
self.board[cds[column] - 1][column] = True |
|
reward_after: float = self.score_plus() |
|
return reward_after - reward_before |
|
|
|
def score(self) -> int: |
|
ToReturn: int = 0 |
|
for row in self.board: |
|
for col in row: |
|
if col: |
|
ToReturn = ToReturn + 1 |
|
return ToReturn |
|
|
|
def score_plus(self) -> float: |
|
|
|
ToReturn: float = float(self.score()) |
|
|
|
|
|
stdev: float = statistics.pstdev(self.column_depths()) |
|
ToReturn = ToReturn - (stdev * 2) |
|
|
|
return ToReturn |
|
|
|
def randomize(self) -> float: |
|
"""Sets the board to a random setup.""" |
|
|
|
|
|
for ri in range(0, len(self.board)): |
|
for ci in range(0, len(self.board[0])): |
|
self.board[ri][ci] = False |
|
|
|
|
|
for ci in range(0, 4): |
|
random_drops: int = random.randint(0, 4) |
|
for _ in range(0, random_drops): |
|
self.drop(ci) |
|
|
|
|
|
if self.score() == 16: |
|
self.board[0][random.randint(0, 3)] = ( |
|
False |
|
) |
|
|