-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathGameFramework.py
293 lines (278 loc) · 12.7 KB
/
GameFramework.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
import tkinter as tk
import SaveSystem
import Settings
import Case
class GameFramework:
def __init__(self, settings: Settings, canvas: tk.Canvas, label: tk.Label, score: tk.Label):
"""
The class which handles the whole game logic.
:param settings: The settings class, in order to retrieve some useful data.
:param canvas: The main canvas, to draw on it, kinda useful.
:param label: The main label used to tell the player's turn & victory.
"""
self.settings = settings
self.canvas = canvas
self.label = label
self.score = score
self.cases = []
self.previous = None
self.player = 0
self.scores = [0] * len(self.settings.player_array)
self.offset = ()
self.data = {}
self.start()
self.switch_player(first=True)
def start(self):
"""
This function starts and set-ups the game.
"""
self.canvas.configure(width=(self.settings.gridSizeX + 3) * Case.Case.size(),
height=(self.settings.gridSizeY + 3) * Case.Case.size())
self.canvas.bind('<Button-1>', self.select)
self.canvas.bind('s', lambda event: self.key_press(True))
self.canvas.bind('m', lambda event: self.key_press(False))
offset_x = (int(self.canvas.cget("width")) - self.settings.gridSizeX * Case.Case.size()) / 2
offset_y = (int(self.canvas.cget("height")) - self.settings.gridSizeY * Case.Case.size()) / 2
self.offset = (int(offset_x), int(offset_y))
self.cases = [[]] * self.settings.gridSizeY
for y in range(self.settings.gridSizeY):
self.cases[y] = [0] * self.settings.gridSizeX
for x in range(self.settings.gridSizeX):
rect = self.canvas.create_rectangle(
offset_x + x * Case.Case.size(), offset_y + y * Case.Case.size(),
offset_x + (x+1) * Case.Case.size(), offset_y + (y+1) * Case.Case.size(),
fill="white",
outline="black",
tags="case")
self.cases[y][x] = Case.Case(x, y, rect)
def select(self, event):
"""
The selection method, event fired on mouse click.
:param event: The mouse event data
"""
real_x, real_y = event.x - self.offset[0], event.y - self.offset[1]
if self.previous: self.canvas.itemconfigure(self.previous.id, outline="black", width=1)
# Here we check if the mouse click is inside the range of the game
if (real_x < 0 or real_y < 0
or real_x > self.settings.gridSizeX * Case.Case.size()
or real_y > self.settings.gridSizeY * Case.Case.size()):
return
# The selection logic
x, y = real_x // Case.Case.size(), real_y // Case.Case.size()
# print(f"({x}, {y})")
self.previous = self.cases[y][x]
color = self.settings.player_array[self.player][1]
self.canvas.itemconfigure(self.previous.id, outline=color, width=2)
def key_press(self, is_key_s: bool):
"""
Function called after a key is pressed.
:param is_key_s: Boolean representing which key out of the two is pressed.
"""
# If no key is selected -> return
if not self.previous: return
# We check if the case is empty
if self.previous.text == "":
if is_key_s:
self.previous.text = "S"
else:
self.previous.text = "M"
x, y = self.previous.x, self.previous.y
self.previous.text_id = self.canvas.create_text(
self.offset[0] + (x+0.5) * Case.Case.size(),
self.offset[1] + (y+0.5) * Case.Case.size(),
text=self.previous.text,
fill=self.settings.player_array[self.player][1],
font=('Helvetica','15','bold'),
tags="case_text")
self.canvas.itemconfigure(self.previous.id, outline="black", width=1)
# Checking for end of the game
has_won = self.check_sms()
# Save system
self.data[f"Letter_{x}_{y}"] = self.previous.text
# TicTacToe game mode
if self.settings.game_mode == 1 and has_won:
self.end()
return
# For both modes, we check if the grid isn't full
if self.is_grid_full():
self.end()
return
# The game continues
self.switch_player()
def switch_player(self, first: bool = False):
"""
This function performs player-switching logic.
"""
self.player += 1
if self.player >= len(self.settings.player_array):
self.player = 0
(name, color) = self.settings.player_array[self.player]
self.label.configure(text=name, fg=color)
self.score.configure(text=f"Score: {self.scores[self.player]}", fg=color)
if first:
self.data["FirstPlayer"] = str(self.player)
def check_sms(self) -> bool:
"""
This function is vital, it checks for 'SMS' alignments after a new letter is entered.
:return: True if an alignment was detected, False otherwise.
"""
interesting_couples = []
for i in range(-1, 2):
for j in range(-1, 2):
# Here we check all the cases surrounding The One, illustration:
# X X X |
# X O X | O is the interesting case, aka. The One
# X X X | X is a checked case
if i != 0 or j != 0:
# print(self.previous, "(", i, ", ", j, ")")
case = self.propagate(self.previous, (i, j))
# If the case is None, we check the next
if not case: continue
# If the latest placed text was M
if self.previous.text == "M" and case.text == "S":
interesting_couples.append((i, j))
# If the latest placed text was S
if self.previous.text == "S" and case.text == "M":
interesting_couples.append((i, j))
for (i, j) in interesting_couples.copy():
# If the latest placed text was M
if self.previous.text == "M":
if (-i, -j) in interesting_couples:
# Player won!
interesting_couples.remove((i, j))
interesting_couples.remove((-i, -j))
self.won(self.propagate(self.previous, (i, j)), self.propagate(self.previous, (-i, -j)))
# TicTacToe game mode
if self.settings.game_mode == 1:
return True
# If the latest placed text was S
elif self.previous.text == "S":
# We continue propagating on the same direction as it looks promising, hehe
# TODO: improve this chain of functions (Although, I don't care since it will always work)
last_case = self.propagate(self.propagate(self.previous, (i, j)), (i, j))
# print("last case: ", last_case, "| (i,j): ", (i,j))
if last_case and last_case.text == "S":
# Player won!
interesting_couples.remove((i, j))
self.won(self.previous, last_case)
# TicTacToe game mode
if self.settings.game_mode == 1:
return True
# TODO: Rework the returns to work properly as designed. (Although, I don't care since it will always work)
return False
def propagate(self, from_dir: Case, to_dir: (int, int)) -> Case:
"""
This function checks if a propagation following a vector is possible.
:param from_dir: The case were the vector starts.
:param to_dir: The vector.
:return: Returns the corresponding case if reachable, returns 'None' otherwise.
"""
(x, y) = to_dir
if 0 <= from_dir.x + x <= self.settings.gridSizeX - 1:
if 0 <= from_dir.y + y <= self.settings.gridSizeY - 1:
return self.cases[from_dir.y + y][from_dir.x + x]
return None
def won(self, start_case: Case, end_case: Case):
"""
This function draws line when an alignment is made.
:param start_case: One 'S' case of the alignment.
:param end_case: The second 'S' case of the alignment.
"""
(name, color) = self.settings.player_array[self.player]
x0 = self.offset[0] + (start_case.x+0.5) * Case.Case.size()
y0 = self.offset[1] + (start_case.y+0.5) * Case.Case.size()
x1 = self.offset[0] + (end_case.x+0.5) * Case.Case.size()
y1 = self.offset[1] + (end_case.y+0.5) * Case.Case.size()
self.canvas.create_line(x0, y0, x1, y1, width=2, fill=color, tags="line")
self.scores[self.player] += 1
self.score.configure(text=f"Score: {self.scores[self.player]}", fg=color)
def is_grid_full(self) -> bool:
"""
Checks if the grid is full or not.
:return: True is the grid is full, False otherwise.
"""
for y in range(self.settings.gridSizeY):
for x in range(self.settings.gridSizeX):
if self.cases[y][x].text == "":
return False
return True
def end(self):
"""
This function ends the game, it can detect if several equally ranked players won.
"""
# TODO: Test this function (seems to work)
max_score = max(self.scores)
# Here we check if two players have the same score or not
nbr = 0
for player_score in self.scores:
if player_score == max_score:
nbr += 1
(name, color) = self.settings.player_array[self.scores.index(max_score)]
if nbr >= 2:
self.label.configure(text=str(nbr) + " equally ranked players won!", fg=color)
else:
self.label.configure(text=name + " won!", fg=color)
# Show all the players' scores
sum_scores = ""
for i in range(len(self.settings.player_array)):
sum_scores += f"{self.settings.player_array[i][0]}'s score: {self.scores[i]} - "
sum_scores = sum_scores.rstrip(" - ")
self.score.configure(text=sum_scores, fg=color)
self.canvas.unbind('<Button-1>')
self.canvas.unbind('s')
self.canvas.unbind('m')
def reset(self):
"""
This function resets the canvas completely by deleting everything.
It also resets player scores.
"""
self.canvas.delete("line")
self.canvas.delete("case_text")
self.canvas.delete("case")
self.scores = [0] * len(self.settings.player_array)
def restart(self):
"""
This function restarts the game, whatever state the game was in before.
"""
self.end()
self.reset()
self.start()
self.switch_player(first=True)
def save(self):
"""
This method saves the game at its current state.
Known bug: if the settings change before saving the game, and if the game is not restarted,
there will be some issue (player number not matching between the settings and the game save)
"""
SaveSystem.save(self.data, "game")
def load(self):
"""
This method restores the settings previously saved by playing the game step by step.
"""
# Grab the data back, if any
self.data = SaveSystem.load("game")
if len(self.data) > 0:
# Reset the game
self.end()
self.reset()
self.start()
# Set the first player to play
self.player = int(self.data["FirstPlayer"])
(name, color) = self.settings.player_array[self.player]
self.label.configure(text=name, fg=color)
self.score.configure(text=f"Score: {self.scores[self.player]}", fg=color)
# self.data[f"Letter_{x}_{y}"] = self.previous.text
keys = list(self.data.keys())
# We use the property of dicts -> they are ordered, which means that the data is in the same order as saved
for i in range(1, len(self.data)):
# Here we remove the first 7 characters to get the x_y values
temp = keys[i][7:].split("_")
# So we can get the coordinates stored inside the dict keys
x, y = int(temp[0]), int(temp[1])
# We play the game, we feed values to our functions
self.previous = self.cases[y][x]
case = self.data[keys[i]]
is_key_s = False
if case == "S":
is_key_s = True
self.key_press(is_key_s)