-
Notifications
You must be signed in to change notification settings - Fork 0
/
game.py
403 lines (356 loc) · 15.9 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
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
"""
Riccardo Prosdocimi
CS 5001, Fall 2021
Final Project -- The Game: Puzzle Slider
This is a Game class.
This resembles the Puzzle Slider Game.
"""
from constants import *
import turtle
import os
import random
from screen import Screen
from button import Button
from text import Text
from puzzle import Puzzle
from tiles import Tiles
from error import Error
class Game:
def __init__(self):
"""
This is the constructor of the Game class.
"""
self.screen = Screen()
self.screen.set()
self.screen.set_pop_up("Resources/splash_screen.gif")
self.user = ""
self.clicks = 0
self.moves = 0
self.get_user()
self.get_moves()
self.screen.game_area.draw()
self.screen.leaderboard_area.draw()
self.screen.status_area.draw()
self.leaderboard = []
self.read_leaderboard()
self.screen.get_screen().onclick(self.get_click)
self.leaders = Text(LEADERS_X, LEADERS_Y, self.leaderboard,
"blue")
self.clicks_count = Text(CLICKS_COUNTER_X, CLICKS_COUNTER_Y,
self.clicks, "black")
self.reset_button = Button(RESET_BUTTON_X, RESET_BUTTON_Y,
"Resources/resetbutton.gif", 80)
self.load_button = Button(LOAD_BUTTON_X, LOAD_BUTTON_Y,
"Resources/loadbutton.gif", 76)
self.quit_button = Button(QUIT_BUTTON_X, QUIT_BUTTON_Y,
"Resources/quitbutton.gif", 53)
self.show_graphics()
self.puzzle = Puzzle()
self.master_dict = {}
self.loaded_puzzle = {}
self.tiles = []
self.winning_list = []
self.load_default_puzzle()
turtle.done()
def get_user(self):
"""
This method gets user input for their name.
:return: user name capitalized (str)
"""
while self.user == '': # keeps asking until name is entered
self.user = self.screen.get_input("Enter Name",
"Your Name (11 characters max):")
if self.user is None: # user clicks cancel
os._exit(0) # shuts down the program
else:
self.user = self.user.capitalize()
def get_moves(self):
"""
This method gets user input for the number of moves or chances.
:return: number of moves/chances (int)
"""
self.moves = self.screen.get_num_input("Enter Moves",
"Number of moves (chances) you "
"want (5-200):", 50, 5, 200)
if self.moves is None: # user clicks cancel
os._exit(0)
else:
return self.moves
def print_moves(self):
"""
This method casts the number of moves to an int and returns it.
:return: number of moves/chances (int)
"""
return int(self.moves)
def show_graphics(self):
"""
This method shows/draws the game graphics and controls.
"""
self.screen.set_text("Leaders:", "blue", "normal", LEADERS_TEXT_X,
LEADERS_TEXT_Y)
self.leaders.show_leaders(self.leaderboard)
self.screen.set_text("Player Moves:", "black", "bold",
PLAYER_MOVES_TEXT_X, PLAYER_MOVES_TEXT_Y)
self.clicks_count.show_text()
self.screen.set_text("/", "black", "bold", CLICKS_SLASH_X,
CLICKS_SLASH_Y)
self.screen.set_text(self.print_moves(), "black", "bold",
USER_CLICKS_X, USER_CLICKS_Y)
self.reset_button.show_button()
self.load_button.show_button()
self.quit_button.show_button()
def load_default_puzzle(self):
"""
This method shows/draws the mario puzzle, which is the default puzzle.
"""
self.master_dict = self.puzzle.get_puz()
default_puzzle = "mario"
if default_puzzle in self.master_dict:
self.loaded_puzzle = self.master_dict[default_puzzle]
self.screen.set_thumbnail(self.loaded_puzzle["thumbnail"],
THUMBNAIL_X, THUMBNAIL_Y)
self.set_tiles()
self.set_scrambled_tiles()
else:
self.screen.set_pop_up("Resources/file_error.gif")
error = Error("mario.puz", "Game.load_default_puzzle()")
error.write_puz_error()
self.puzzle.warn_user()
self.validate_puzzle()
def load_puzzle(self):
"""
This method lets the user choose a puzzle from a list.
:return: the selected puzzle (str)
"""
puzzles = list(self.master_dict.keys())
if "malformed_mario" in puzzles: # known faulty puzzle among options?
puzzles.remove("malformed_mario") # get rid of it
puzzles = "\n".join(puzzles)
selected_puzzle = self.screen.get_input("Load Puzzle",
f"Enter the name of the "
f"puzzle you wish to load. "
f"Choices are:\n{puzzles}")
return selected_puzzle
def validate_puzzle(self):
"""
This method homogenizes user input for loading a new puzzle and checks
if the selected puzzle is both loadable and among the available
options.
"""
selected_puzzle = self.load_puzzle()
if selected_puzzle is not None: # user doesn't click cancel
selected_puzzle = selected_puzzle.lower()
# puzzle name among available options?
if selected_puzzle in self.master_dict.keys():
self.loaded_puzzle = self.master_dict[selected_puzzle]
tiles_number = self.loaded_puzzle["number"]
# is the puzzle loadable?
if tiles_number not in ["4", "9", "16"]:
self.screen.set_pop_up("Resources/file_error.gif")
else: # if it's loadable
for i in range(len(self.tiles)):
for j in range(len(self.tiles)):
self.tiles[i][j].clear_tiles() # clears game area
self.tiles = [] # resets nested list containing the tiles
self.winning_list = [] # resets winning configuration
self.clicks = 0 # resets user clicks count
self.clicks_count.update(self.clicks) # updates graphics
# shows/draws the correct thumbnail
self.screen.set_thumbnail(self.loaded_puzzle["thumbnail"],
THUMBNAIL_X, THUMBNAIL_Y)
self.set_tiles()
self.set_scrambled_tiles()
else: # puzzle not in available options
self.screen.set_pop_up("Resources/file_error.gif")
def set_tiles(self):
"""
This method calculates the tiles position on the screen and creates an
object of each tile, saving the tiles to one nested list and their
images to another nested list.
"""
tiles_size = int(self.loaded_puzzle["size"])
tiles_number = int(int(self.loaded_puzzle["number"]) ** 0.5)
if tiles_number == 2:
r = 200 - (tiles_size * tiles_number) # puzzle's total space left
elif tiles_number == 3:
r = 300 - (tiles_size * tiles_number)
else: # tiles_number == 4
r = 400 - (tiles_size * tiles_number)
space = r / (tiles_number + 1) # available space in between borders
counter = 1
for i in range(tiles_number):
row = [] # initializes inner lists
winning_row = [] # initializes winning configuration's inner lists
for j in range(tiles_number):
x_coord = (GAME_AREA_X + 10 + (tiles_size / 2) + (
tiles_size + space) * j)
y_coord = (GAME_AREA_Y - 10 - (tiles_size / 2) - (
tiles_size + space) * i)
image = self.loaded_puzzle[str(counter)]
counter += 1
# creates an object of each tile on the screen
tiles = Tiles(x_coord, y_coord, tiles_size, image)
row.append(tiles) # adds tiles to the inner lists
winning_row.append(image) # populates winning configuration
# data structure that reflects the puzzle grid on the screen
self.tiles.append(row) # adds lists to the outer list
# completes winning configuration
self.winning_list.append(winning_row)
def set_scrambled_tiles(self):
"""
This method shuffles the tiles on the screen letting the user solve the
puzzle.
"""
tiles_number = int(int(self.loaded_puzzle["number"]) ** 0.5)
# creates a random list of numbers from 1 to the number of puzzle tiles
num_list = random.sample(
range(1, int(self.loaded_puzzle["number"]) + 1),
int(self.loaded_puzzle["number"]))
counter = 0
for i in range(tiles_number):
for j in range(tiles_number):
# scrambles the puzzle's tiles
image = self.loaded_puzzle[str(num_list[counter])]
self.tiles[i][j].set_image(image) # sets tile object's image
self.tiles[i][j].draw_tiles() # draws/shows the tiles
counter += 1
def reset_tiles(self):
"""
This method draws/shows the puzzle's winning configuration.
"""
tiles_number = int(int(self.loaded_puzzle["number"]) ** 0.5)
counter = 1
for i in range(tiles_number):
for j in range(tiles_number):
image = self.loaded_puzzle[str(counter)]
self.tiles[i][j].set_image(image)
self.tiles[i][j].t.shape(image)
counter += 1
def find_blank(self):
"""
This method searches for the blank tile in the puzzle.
"""
for row in self.tiles:
for tiles in row:
tiles.is_blank()
def is_adjacent(self, row, column):
"""
This method checks if a tile is adjacent to the blank tile and swaps
the two tiles if it is while checking if the player has won after each
swap. It also updates user clicks.
:param row: index traversing the outer list (int)
:param column: index traversing the inner lists (int)
"""
# sifts through the tiles nested list checking if a tile on the screen
# is next to the blank tile either horizontally or vertically
directions = [[1, 0], [0, 1], [-1, 0], [0, -1]]
for i in range(len(directions)):
new_row = directions[i][0] + row
new_column = directions[i][1] + column
# blank tile next to the clicked tile?
if 0 <= new_row < len(self.tiles) and 0 <= new_column < len(
self.tiles) and self.tiles[new_row][new_column].is_blank():
# swap tiles -> clicked tile shifts to blank tile's place
# and vice versa
blank_image = self.tiles[new_row][new_column].get_image()
image = self.tiles[row][column].get_image()
self.tiles[new_row][new_column].swap_tiles(image)
self.tiles[row][column].swap_tiles(blank_image)
# each time a tile shifts -> +1 click
self.count_clicks()
self.clicks_count.update(self.clicks)
self.check_end_game() # nested list == winning configuration
def quit_game(self):
"""
This method shows a pop-up message and shuts the game down.
"""
self.screen.set_pop_up("Resources/quitmsg.gif")
os._exit(0)
def get_click(self, x, y):
"""
This method gets user clicks and calls other methods based on the
click's location on the screen.
:param x: x coordinate of the click on the screen (int)
:param y: y coordinate of the click on the screen (int)
"""
for i in range(len(self.tiles)):
for j in range(len(self.tiles[0])):
if self.tiles[i][j].is_clicked(x, y):
# clicked tile next to the blank tile?
self.is_adjacent(i, j)
if self.reset_button.is_clicked(x, y): # click on reset button
self.reset_tiles()
if self.load_button.is_clicked(x, y): # click on load button
self.puzzle.warn_user()
self.validate_puzzle()
if self.quit_button.is_clicked(x, y): # click on quit button
self.quit_game()
def count_clicks(self):
"""
This method updates the number of clicks every time a tile is shifted.
"""
self.clicks = self.clicks + 1
def check_end_game(self):
"""
This method checks if the player has won (unscrambles the puzzle) by
comparing the nested list containing the winning configuration with the
nested list shown/drawn on the screen. It also keeps track of the total
clicks to check if the user has gone above the chances they chose,
hence losing the game.
"""
is_win = True # flag variable
for i in range(len(self.winning_list)):
for j in range(len(self.winning_list)):
# nested list != winning configuration?
if self.winning_list[i][j] != self.tiles[i][j].get_image():
is_win = False # continue playing
break
if is_win: # player wins
self.screen.set_pop_up("Resources/winner.gif")
self.screen.set_pop_up("Resources/credits.gif")
self.write_leaderboard() # saves score and player's name
os._exit(0)
# player hasn't won but has reached the number of clicks chosen
elif is_win is False and self.clicks == self.moves + 1:
self.screen.set_pop_up("Resources/Lose.gif")
self.screen.set_pop_up("Resources/credits.gif")
os._exit(0)
def write_leaderboard(self):
"""
This method saves a player's name and number of clicks to a file.
"""
file = "leaderboard.txt"
try:
# name's length longer than 11 characters -> keep first 11
player = lambda user: str(user) if len(user) <= 11 else \
str(user[:11] + "...")
with open(file, "a") as leaders_file:
leaders_file.write(str(self.clicks) + ": " +
str(player(self.user)) + "\n")
except FileNotFoundError:
self.screen.set_pop_up("Resources/leaderboard_error.gif")
error = Error(file, "Game.write_leaderboard()")
error.write_file_error() # logs error if file not found
except OSError:
print("Some unknown error occurred")
def read_leaderboard(self):
"""
This method reads players' names and number of moves from a file and
populates a list with them.
"""
file = "leaderboard.txt"
try:
with open(file, "r") as leaders_file:
for line in leaders_file:
leaders = line.strip("\n")
self.leaderboard.append(leaders)
# sort the list in ascending order based off of the number of moves
self.leaderboard.sort(key=lambda entry: int(entry.split(": ")[0]))
# keeps the top 14 players
self.leaderboard = self.leaderboard[:14]
except FileNotFoundError:
self.screen.set_pop_up("Resources/leaderboard_error.gif")
error = Error(file, "Game.read_leaderboard()")
error.write_file_error()
except OSError:
print("Some unknown error occurred")