-
Notifications
You must be signed in to change notification settings - Fork 0
/
Game.py
213 lines (182 loc) · 7.98 KB
/
Game.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
from copy import deepcopy
from itertools import product
# Defines an exception that is raised when an error in the game occurs.
class GameError(Exception):
pass
# The Game class contains all properties, game constants, and methods required by a game.
class Game:
P1 = 1
P2 = 2
EMPTY = 3
DRAW = 4
ONGOING = 5
def __init__(self, boardsize):
self._board = [[Game.EMPTY for _ in range(boardsize)] for _ in range(boardsize)]
self._captures = {Game.P1: [], Game.P2: []}
self._winner = Game.ONGOING
self._player = Game.P1
self._moveStack = MoveStack()
@property
def board(self):
return self._board
@board.setter
def board(self, board):
self._board = board
@property
def captures(self):
return self._captures
@captures.setter
def captures(self, captures):
self._captures = captures
@property
def winner(self):
return self._winner
@winner.setter
def winner(self, winner):
self._winner = winner
@property
def player(self):
return self._player
@player.setter
def player(self, player):
self._player = player
@property
def moveStack(self):
return self._moveStack
# The function is given a board, starting coordinate, and pattern.
# The inRow function checks if the pattern is found in the E, SE, S and SW directions from the coordinate.
@staticmethod
def inRow(board, row, col, pattern):
validProducts = Game.getValidProducts([(0,1), (1,1), (1,0), (1,-1)], len(pattern), row, col, len(board))
for rc in validProducts:
pieces = [board[row+i*rc[0]][col+i*rc[1]] for i in range(1, len(pattern)+1)]
if pieces == pattern:
return True
return False
# Given a game state and a move to play, the newState function returns the new game state as a result of playing the move.
@staticmethod
def newState(board, captures, player, row, col):
board, captures = deepcopy(board), deepcopy(captures)
board[row][col] = player
opponent = Game.P2 if player == Game.P1 else Game.P1
pattern = [opponent, opponent, player]
products = list(product([0, 1, -1], repeat=2))
products.remove((0, 0))
validProducts = Game.getValidProducts(products, 3, row, col, len(board))
for rc in validProducts:
pieces = [board[row+i*rc[0]][col+i*rc[1]] for i in range(1, 4)]
if pieces == pattern:
captures[player].append([(row+i*rc[0], col+i*rc[1]) for i in range(1, 3)])
for i in range(1, 3):
board[row+i*rc[0]][col+i*rc[1]] = Game.EMPTY
return board, captures, opponent
# Given a board and captures, the getWinner function returns the player number who won if there's a winner, or Game.DRAW or Game.ONGOING otherwise.
@staticmethod
def getWinner(board, captures):
for player in [Game.P1, Game.P2]:
if len(captures[player]) >= 5:
return player
fullboard = True
boardsize = len(board)
for row in range(boardsize):
for col in range(boardsize):
player = board[row][col]
if player == Game.EMPTY:
fullboard = False
continue
if Game.inRow(board, row, col, [player]*4):
return player
if fullboard:
return Game.DRAW
return Game.ONGOING
# Given a list of tuples representing directions in which to search for a pattern, returns the valid directions which don't take the search off the board.
@staticmethod
def getValidProducts(products, size, row, col, boardsize):
validProducts = []
for rc in products:
if not Game.offBoard(row + rc[0]*size, col + rc[1]*size, boardsize):
validProducts.append(rc)
return validProducts
# Given a coordinate and a boardsize, returns True if the coordinate is on the board or False otherwise.
@staticmethod
def offBoard(row, col, boardsize):
return not ((0 <= row < boardsize) and (0 <= col < boardsize))
# Given a coordinate of a potential new move on a board, the validateRowCol function raises a gameError if the move is not valid.
@staticmethod
def validateRowCol(row, col, board):
if Game.offBoard(row, col, len(board)):
raise GameError("Move is off the board")
elif board[row][col] != Game.EMPTY:
raise GameError("Position is not empty")
# Given a move, the game goes onto its new state by calling the newState function, and updates the winner.
# The move and the state of the captures are pushed onto the moveStack.
def play(self, row, col):
self.board, self.captures, self.player = Game.newState(self.board, self.captures, self.player, row, col)
self.winner = Game.getWinner(self.board, self.captures)
self.moveStack.push(deepcopy(self.captures), row, col)
# Undoes the last move played.
def undo(self):
row, col = self.moveStack.pop()[1:]
if self.moveStack.isEmpty():
captures = {Game.P1: [], Game.P2: []}
else:
captures = self.moveStack.peek()[0]
otherPlayer = self.player
self.player = Game.P1 if self.player == Game.P2 else Game.P2
while len(captures[self.player]) != len(self.captures[self.player]):
lastPair = self.captures[self.player].pop()
for cap in lastPair:
self.board[cap[0]][cap[1]] = otherPlayer
self.board[row][col] = Game.EMPTY
# Given a Pente move, boardsize, and whether the move made any captures, the function will return the Pente notation of the move.
@staticmethod
def getPenteMoveNotation(row, col, boardsize, capturesMade):
centre = boardsize//2
changeInY, changeInX = row-centre, col-centre
if changeInX == 0 and changeInY == 0:
string = "0"
else:
if changeInX > 0:
xExp = f"R{changeInX}"
elif changeInX < 0:
xExp = f"L{-changeInX}"
else:
xExp = ""
if changeInY > 0:
yExp = f"D{changeInY}"
elif changeInY < 0:
yExp = f"U{-changeInY}"
else:
yExp = ""
string = xExp + yExp
if capturesMade: string += "*"
return string
# The MoveStack class is implemented as a stack used to store the captures and moves played in the game.
class MoveStack:
def __init__(self):
self._stack = []
# Returns True if the stack is empty and False otherwise.
def isEmpty(self):
return len(self._stack) == 0
# Given a dictionary of captures and a move, the push function pushes this together onto the top of the stack.
def push(self, captures, row, col):
self._stack.append((captures, row, col))
# Removes the top item in the stack and returns it, or raises a game error if the stack is empty.
def pop(self):
if self.isEmpty():
raise GameError("There have been no previous moves")
return self._stack.pop()
# Returns the top item in the stack without removing it, or raises a game error if the stack is empty.
def peek(self):
if self.isEmpty():
raise GameError("There have been no previous moves")
return self._stack[-1]
# The GameRecord class defines the datatype which all game information is stored as in the datatbase.
class GameRecord:
def __init__(self, id=-1, name=-1, whenSaved=-1, game=-1, mode=-1, compDifficulty=-1):
self.id = id
self.name = name
self.whenSaved = whenSaved
self.game = game
self.mode = mode
self.compDifficulty = compDifficulty