-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathgame.py
332 lines (279 loc) · 18.7 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
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
from board import * # Import everything from board
from board_actions import * # Import everything from board_actions
from generator import BoardGenerator # Import board generator class
from solver import BoardSolver # Import board solver class
from data_structures import Stack # Import stack class
from datetime import datetime, timedelta # Import datetime and timedelta functions from datetime
import json # Import json module
import os # Import os module
from copy import deepcopy # Import deepcopy function from copy
from math import floor # Import floor function from math
from rating_calc import * # Import rating calculation details
from application import Application # Import application class to get default directory to save games
class GameError(Exception): # GameError exception class
pass
'''
Class to represent the game that is currently being played,
contains the board and processes any in-game operations such as
placing and removing numbers from the board with validation.
'''
class Game: # Game class
DISABLED_GAMEMODES = {("Normal", "16x16", "Expert"), ("Killer", "16x16", "Expert"),
("Normal", "16x16", "Hard"), ("Killer", "16x16", "Hard"),
("Killer", "16x16", "Medium"), ("Killer", "12x12", "Expert"),
("Killer", "12x12", "Hard")}
DIFFICULTY_NUMS = {1: "Easy", 2: "Medium", 3: "Hard", 4: "Expert"} # Difficulty-number pair for TERMINAL only
NUM_AUTO_NOTES = {"Easy": 80, "Medium": 65, "Hard": 50, "Expert": 35} # Number of auto notes for each difficulty
NUM_HINTS = {"Easy": 5, "Medium": 6, "Hard": 8, "Expert": 11} # Number of hints for each difficulty
def __init__(self): # Constructor
self.__action_stack = Stack() # Stack to store user actions
self.__file = None # File for which game has been loaded from
self.__creation_date = str(datetime.now().date()) # Creation date
self.__creation_time = str(datetime.now().time()) # Creation time
def generate(self, mode, difficulty, board_size, timed, hardcore,
bonus_hints): # Generate new board method (takes mode : str, difficulty: str, board_size : int, timed: boolean)
self.__mode = mode # Set mode
self.__difficulty = difficulty # Set difficulty
self.__board_size = board_size # Set board size
self.__VALID_NUMS = [i for i in range(1, self.__board_size + 1)] # Set valid numbers depending on board size
self.__board = BoardGenerator.new_board(self.__mode, self.__difficulty,
self.__board_size) # Create the board object
self.__orig_board = deepcopy(self.__board) # Create orig board object using deepcopy
self.__timed = timed # Set timed
self.__hardcore = hardcore # Set hardcore
self.__bonus_hints = bonus_hints
if self.__hardcore: # No auto notes or hints allowed in hardcore mode
self.__num_of_auto_notes = 0
self.__orig_num_of_auto_notes = 0
self.__num_of_hints = 0
self.__orig_num_of_hints = 0
else:
self.__num_of_auto_notes = int(self.NUM_AUTO_NOTES[self.__difficulty] / 81 * (
board_size ** 2)) # Calculate number of auto notes based on number of squares
self.__orig_num_of_auto_notes = self.__num_of_auto_notes # Set original number of auto notes
self.__num_of_hints = int(self.NUM_HINTS[self.__difficulty] / 81 * (
board_size ** 2)) + self.__bonus_hints # Calculate number of hints based on number of squares
self.__orig_num_of_hints = self.__num_of_hints # Set original number of hints
self.__time_elapsed = 0 if self.__timed else None # Set time elapsed
self.__solved_board = BoardSolver.solver(deepcopy(self.__orig_board))
@staticmethod
def get_stats_from(account, file): # Method to get stats of a given file (returns dictionary)
with open(f"{Application.DEFAULT_DIRECTORY}/{account}/{file}") as f:
return json.load(f)
def load_game(self, account, file): # Method to load game from file (takes file : str)
data = self.get_stats_from(account, file) # Stores as dictionary
self.__file = file # Set file attribute to file name
self.__mode = data["mode"] # Set mode
self.__difficulty = data["difficulty"] # Set difficulty
self.__num_of_auto_notes = data["num of auto notes"]
self.__orig_num_of_auto_notes = data["orig num of auto notes"]
self.__num_of_hints = data["num of hints"] # Set number of hints
self.__orig_num_of_hints = data["orig num of hints"] # Set original number of hints
self.__board_size = data["board size"] # Set board size
self.__VALID_NUMS = [i for i in range(1, self.__board_size + 1)] # Set valid numbers depending on board size
# Create null board object
self.__board = NormalModeBoard(self.__board_size) if self.__mode == "Normal" else KillerModeBoard(
self.__board_size)
self.__orig_board = deepcopy(self.__board) # Deepcopy to get orig board
self.__solved_board = NormalModeBoard(self.__board_size) if self.__mode == "Normal" else KillerModeBoard(
self.__board_size) # Create null solved board object
self.__board.load(data["board"]) # Load null board with squares
self.__orig_board.load(data["orig board"]) # Load null orig board with squares
self.__solved_board.load(data["solved board"]) # Load null solved board with squares
self.__creation_date = data["creation date"] # Set creation date
self.__creation_time = data["creation time"] # Set creation time
self.__timed = data["timed"] # Set timed
self.__hardcore = data["hardcore"] # Set hardcore
self.__time_elapsed = data["time elapsed"] # Set time elapsed
'''
#####################################################################################################
# GROUP B Skill: Writing and reading from files - used to store game files as .json in directory #
# #
# The Game class reads and writes JSON files using the json library in python to store information #
# about games that have been paused by the user. Whenever a user pauses a game, the state of the #
# board along with other variables are written to a JSON file in the account’s directory. Whenever #
# a user browses through their stored games in the OpenGameScreen, the JSON file is read in and the #
# settings of the game are displayed on the screen. #
#####################################################################################################
'''
def save_game(self, account): # Save game to file method
# Create file name based on local time if board is not loaded from a file
file_name = f"sudoku_{datetime.now().strftime('%d-%m-%y_%H-%M-%S')}.json" if self.__file is None else self.__file
with open(dir := f"{Application.DEFAULT_DIRECTORY}/{account}/{file_name}", "w") as f: # Open json file
f.write(json.dumps({"creation date": self.__creation_date, "creation time": self.__creation_time,
"mode": self.__mode, "difficulty": self.__difficulty,
"num of hints": self.__num_of_hints,
"orig num of hints": self.__orig_num_of_hints,
"num of auto notes": self.__num_of_auto_notes,
"orig num of auto notes": self.__orig_num_of_auto_notes,
"board size": self.__board_size, "board": self.__board.hash(),
"orig board": self.__orig_board.hash(),
"solved board": self.__solved_board.hash(), "timed": self.__timed,
"hardcore": self.__hardcore, "time elapsed": self.__time_elapsed},
indent=4)) # Write data to json file
print(f"Game saved to {dir}")
def remove_game_file(self, account): # Remove game file when game is resigned or won
if self.__file is not None:
if os.path.exists(path := f"{Application.DEFAULT_DIRECTORY}/{account}/{self.__file}"):
os.remove(path) # Remove file if the file exists
def get_stats(self, completed): # Method to get game stats when user completes or resigns the game
return [self.__mode, self.__difficulty, self.__board_size, self.__orig_num_of_auto_notes,
self.__num_of_auto_notes,
self.__orig_num_of_hints, self.__num_of_hints, self.__timed, completed, self.__hardcore,
0 if self.__time_elapsed is None else self.__time_elapsed, self.__creation_date, self.__creation_time]
'''Getters'''
@property
def difficulty(self): # Gets difficulty (returns str)
return self.__difficulty
@property
def orig_num_of_auto_notes(self): # Gets original number of auto notes given (returns int)
return self.__orig_num_of_auto_notes
@property
def num_auto_notes_left(self): # Gets number of auto notes left (returns int)
return self.__num_of_auto_notes
@property
def orig_num_hints(self): # Gets original number of hints given (returns int)
return self.__orig_num_of_hints
@property
def num_hints_left(self): # Gets number of hints left (returns int)
return self.__num_of_hints
@property
def mode(self): # Gets mode (returns str)
return self.__mode
@property
def timed(self): # Gets timed (returns bool)
return self.__timed
@property
def hardcore(self): # Gets hardcore (returns bool)
return self.__hardcore
@property
def time_elapsed(self): # Getstime elapsed (returns str)
return str(timedelta(seconds=floor(self.__time_elapsed)))
@property
def board_size(self): # Gets board size (returns int)
return self.__board_size
@property
def matrix_size(self): # Gets matrix size (returns tuple)
return self.__board.matrix_size
@property
def curr_board(self): # Gets current board 2D array (returns 2D array of square objects)
return self.__board.board
@property
def orig_board(self): # Gets original board 2D array (returns 2D array of square objects)
return self.__orig_board.board
@property
def solved_board(self): # Gets solved board (returns 2D array of square objects)
return self.__solved_board.board
@property
def groups(self): # Gets groups (returns dictionary if board is a killer mode board, returns None otherwise)
if isinstance(self.__board, KillerModeBoard):
return self.__board.groups
@property
def group_colours(
self): # Gets group colours (returns dictionary if board is a killer mode board, returns None otherwise)
if isinstance(self.__board, KillerModeBoard):
return self.__board.group_colours()
def note_at(self, row, col): # Gets note at square (returns str) : only used for GUI
return self.__board.note_str(row, col)
def pieced_note_at(self, row, col, piece): # Gets note at square (returns str) : only usd for TERMINAL
return self.__board.pieced_note_str(row, col, piece)
'''Other various methods'''
def inc_time_elapsed(self): # Incerement time elapsed every 0.01 seconds (10 milliseconds)
self.__time_elapsed += 0.01
def is_complete(self): # Check if board is complete (returns bool)
return self.__board.num_empty_squares == 0
def percent_complete(self): # Gets percentage completion for progress bar
return round(((
num_orig_empty := self.__orig_board.num_empty_squares) - self.__board.num_empty_squares) / num_orig_empty * 100,
2)
def push_action(self, action): # Push action to stack
self.__action_stack.push(action)
def pop_action(self): # Pop action from stack
return self.__action_stack.pop()
def __validate(self, n): # Validate method (for user interaction with game)
try:
if (n := int(n)) not in self.__VALID_NUMS:
raise GameError("Number inputted is not between 1 and 9")
return n
except TypeError:
raise GameError("Number inputted is not an integer")
except ValueError:
raise GameError("Number inputted is not an integer")
'''UI Specific Methods to interact with the game/board (with validation)'''
def get_num_at(self, row, col): # Method to get number at certain square
row, col = self.__validate(row) - 1, self.__validate(
col) - 1 # '-1' is used to convert 1-based system to a 0-based system
return self.__board.get_num_at(row, col)
def put_down_number(self, row, col,
num): # Put down number method (takes row, col, num where row, col are in a 1-based system)
row, col, num = self.__validate(row) - 1, self.__validate(col) - 1, self.__validate(to_num(num))
if (orig_num := self.__board.get_num_at(row, col)) == 0: # Check if square is empty
if self.__board.is_safe(row, col, num): # Check if number is safe to be placed
self.__board.set_num_at(row, col, num) # Place the number
else:
raise GameError(f"Please enter a number that doesn't exist in the row / column / box you specified")
else:
raise GameError(f"A number already exists at this square")
self.push_action(SetNumAction(row, col, orig_num, num)) # Push set num action to stack (for undo button)
def remove_number(self, row, col): # Remove number method (takes row, col, both are in a 1-based system)
row, col = self.__validate(row) - 1, self.__validate(
col) - 1 # '-1' is used to convert 1-based system to a 0-based system
if (orig_num := self.__board.get_num_at(row, col)) == 0: # Check if square is already empty
raise GameError(f"There is no number at this square that you can delete")
else:
if self.__orig_board.get_num_at(row, col) != 0: # Check if square is part of original board
raise GameError(f"This square is part of the original board and cannot be deleted")
else:
self.__board.set_num_at(row, col, 0)
self.push_action(SetNumAction(row, col, orig_num, 0)) # Push set num action to stack (for undo button)
def edit_note(self, row, col,
num): # Edit (toggle) note method (takes row, col, num, where row, col are in a 1-based system)
row, col, num = self.__validate(row) - 1, self.__validate(col) - 1, self.__validate(
to_num(num)) # '-1' is used to convert 1-based system to a 0-based system
self.__board.toggle_num_at_note(row, col, num) # Toggle number at note (on -> off, off -> on)
self.push_action(EditNoteAction(row, col, num)) # Push edit note action to stack (for undo button)
def __get_auto_note_at(self, row,
col): # INTERNAL PRIVATE Get auto note at square method (takes row, col where both are in a 0-based system)
if self.__board.get_num_at(row, col) != 0: # Check if square is already filled
raise GameError(f"ERROR: Auto-Note is unavailable for this square as it is not empty")
if self.__num_of_auto_notes == 0: # Check if still have auto notes to use
raise GameError(f"Not enough auto-notes")
self.__num_of_auto_notes -= 1 # Decrement auto note counter
return [self.__board.is_safe(row, col, num) for num in self.__VALID_NUMS] # Return valid note array
def use_auto_note(self, row,
col): # EXTERNAL Use auto note method (takes row, col where both are in a 1-based system)
row, col = self.__validate(row) - 1, self.__validate(
col) - 1 # '-1' is used to convert 1-based system to a 0-based system
orig_note = self.__board.get_note_at(row, col)
self.__board.set_note_at(row, col, new_note := self.__get_auto_note_at(row, col))
self.push_action(SetNoteAction(row, col, orig_note, new_note))
def use_hint(self, row, col): # EXTERNAL Use hint method (takes row, col where both are in a 1-based system)
row, col = self.__validate(row) - 1, self.__validate(
col) - 1 # '-1' is used to convert 1-based system to a 0-based system
if (orig_num := self.__board.get_num_at(row, col)) != 0: # Check if square is already filled
raise GameError(f"ERROR: Hint is unavailable for this square as it is not empty")
if self.__num_of_hints == 0: # Check if still have hints to use
raise GameError(f"Not enough hints")
self.__board.set_num_at(row, col,
new_num := self.__solved_board.get_num_at(row, col)) # Fill in the correct number
self.push_action(SetNumAction(row, col, orig_num, new_num)) # Push set num action to stack (for undo button)
self.__num_of_hints -= 1 # Decrement hint counter
def undo_last_move(self): # Undo method, uses BoardActions imported from board_actions.py
if (action := self.pop_action()) != -1: # Check if action stack isn't empty
reverse_action = action.reverse() # Get reverse of that action
if isinstance(reverse_action, SetNumAction): # Apply the reverse action (for SetNumAction)
self.__board.set_num_at(reverse_action.row, reverse_action.col, reverse_action.new_num)
elif isinstance(reverse_action, EditNoteAction): # (for EditNoteAction)
self.__board.toggle_num_at_note(reverse_action.row, reverse_action.col, reverse_action.num)
elif isinstance(reverse_action, SetNoteAction): # (for SetNoteAction)
self.__board.set_note_at(reverse_action.row, reverse_action.col, reverse_action.new_note)
def rating_change(self, rating,
won): # Method to return change in rating after game is completed or resigned, takes current rating (int) and whether the game was completed or not (bool)
if won:
return rating_gain(self.__mode, self.__board_size, self.__difficulty, rating, self.__time_elapsed,
self.__orig_num_of_auto_notes - self.__num_of_auto_notes, self.__orig_num_of_auto_notes,
self.__orig_num_of_hints - self.__num_of_hints,
self.__orig_num_of_hints) # Return rating gain if won
else:
return -rating_loss(self.__mode, self.__board_size, self.__difficulty,
rating) # Return rating loss, loss signified by the minus sign