-
Notifications
You must be signed in to change notification settings - Fork 0
/
Ui.py
1547 lines (1389 loc) · 75.8 KB
/
Ui.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
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
from copy import deepcopy
from Game import Game, GameError, GameRecord, MoveStack
from colorama import Fore, Style
from enum import Enum
from datetime import datetime
import Database
import Ai
from tkinter import *
from tkinter import ttk
from functools import partial
from PIL import Image, ImageDraw
from Client import Client
import threading
import getpass
# The Player Enum class defines different player types.
Player = Enum("Player", ["MAIN", "OPP", "GUEST", "COMP"])
# The Mode Enum class defines different playing modes.
Mode = Enum("Mode", ["PVP", "COMP", "LAN"])
# The Ui class contains attributes and methods shared by the two Uis: Terminal and Gui.
# Ui subclasses are run via the run method.
class Ui:
def __init__(self):
self._player = Player.GUEST
self._opponent = Player.GUEST
self._currPlayers = {Game.P1: Player.MAIN, Game.P2: Player.OPP}
self._currGameRecord = None
self._client = None
@property
def player(self):
return self._player
@player.setter
def player(self, player):
self._player = player
@property
def opponent(self):
return self._opponent
@opponent.setter
def opponent(self, opponent):
self._opponent = opponent
@property
def currPlayers(self):
return self._currPlayers
@currPlayers.setter
def currPlayers(self, currPlayers):
self._currPlayers = currPlayers
@property
def currGameRecord(self):
return self._currGameRecord
@currGameRecord.setter
def currGameRecord(self, currGameRecord):
self._currGameRecord = currGameRecord
@property
def client(self):
return self._client
@client.setter
def client(self, client):
self._client = client
# Given a player (one of Game.P1 and Game.P2), the function returns the username of that player number (or Player.GUEST if the player is not logged in, or Player.COMP if the player is a computer).
def _getUsernameOfPlayerNumber(self, player):
if self.currPlayers[player] == Player.MAIN:
return self.player
else:
return self.opponent
# Given a computer difficulty level (i.e. 1, 2 or 3) the function returns a string of one of Easy, Medium and Hard
@staticmethod
def _convCompDifficulty(compDifficulty):
return {1: "Easy", 2: "Medium", 3: "Hard"}[compDifficulty]
# Given a gameRecord (of the GameRecord datatype), returns a string summarising the information in the gameRecord.
@staticmethod
def _gameString(gameRecord):
players = [Database.getPlayerGameUsername(gameRecord.id, Game.P1), Database.getPlayerGameUsername(gameRecord.id, Game.P2)]
for i in range(2):
if players[i] == False:
if gameRecord.mode == Mode.COMP:
players[i] = f"Computer({Ui._convCompDifficulty(gameRecord.compDifficulty)})"
else:
players[i] = "Guest"
mode = f"{players[0]} v.s. {players[1]}"
whenSaved = datetime.strftime(gameRecord.whenSaved, "%d/%m/%Y, %H:%M:%S")
if gameRecord.game.winner == Game.P1:
status = "P1 won"
elif gameRecord.game.winner == Game.P2:
status = "P2 won"
elif gameRecord.game.winner == Game.DRAW:
status = "Draw"
else:
status = "ONGOING"
return f"{gameRecord.name} - players: {mode}, saved on: {whenSaved}, status: {status}"
# Given the username of a player, adds the player's result to their player profile by calling the database's addPlayerResult procedure.
def _addUserResult(self, player):
if self.currGameRecord.game.winner == Game.DRAW:
Database.addPlayerResult(player, -1)
elif player == self._getUsernameOfPlayerNumber(self.currGameRecord.game.winner):
Database.addPlayerResult(player, True)
else:
Database.addPlayerResult(player, False)
# Adds each player's result to their profile by calling the addUserResult procedure, and returns if any changes were made.
def _addResultsToProfile(self):
changesMade = False
for player in [self.player, self.opponent]:
if player in [Player.COMP, Player.GUEST]: continue
changesMade = True
self._addUserResult(player)
return changesMade
# When loading a game, finds from the database which player played as which player number and returns the main player (one of P1 and P2)
def _loadPlayers(self):
players = [Database.getPlayerGameUsername(self.currGameRecord.id, Game.P1), Database.getPlayerGameUsername(self.currGameRecord.id, Game.P2)]
mainPlayerPos = None
for i, player in enumerate(players):
pos = Game.P1 if i == 0 else Game.P2
if player == self.player:
mainPlayerPos = pos
elif player == False:
self.opponent = Player.COMP if self.currGameRecord.mode == Mode.COMP else Player.GUEST
else:
self.opponent = player
return mainPlayerPos
# Writes the moves of a given game to a text file (with a given gameRecord) using Pente notation.
@staticmethod
def _exportGameMoves(gameRecord):
boardsize = len(gameRecord.game.board)
reverseMoveStack = MoveStack()
moveStackCopy = deepcopy(gameRecord.game.moveStack)
while not moveStackCopy.isEmpty():
lastStack = moveStackCopy.pop()
reverseMoveStack.push(lastStack[0], lastStack[1], lastStack[2])
moveRecord = ""
isPlayer1Turn = True
lastCaptures = {Game.P1: [], Game.P2: []}
while not reverseMoveStack.isEmpty():
captures, row, col = reverseMoveStack.pop()
capturesMade = True if lastCaptures != captures else False
moveRecord += Game.getPenteMoveNotation(row, col, boardsize, capturesMade)
if isPlayer1Turn:
moveRecord += " "
else:
moveRecord += "\n"
isPlayer1Turn = not isPlayer1Turn
lastCaptures = captures
with open(gameRecord.name+"_moveRecord"+".txt", "w+") as f:
f.write(moveRecord)
# Returns a string containing the rules of Pente
@staticmethod
def _getRulesText():
return """
HOW TO PLAY PENTE
Pente is a board game where players take turns playing pieces on intersections of the board.
In this version of Pente, there will only be two players in total.
The first move for player 1 is forced, as they must move at the centre of the board.
Other than this, players can place their pieces on any empty intersection on the board.
A player can win in one of two ways:
1. They manage to make a row of 5 (or more) of their pieces on the board
2. They manage to capture 5 (or more) pairs of their opponent's pieces
Captures can be made by a player by placing their piece such that a pair of the opponent's pieces becomes trapped between two of the player's pieces.
This means that captures can only be made in pairs.
If, however, the opponent places their piece such that two of their pieces are trapped between two of the player's existing pieces, this does not count as a capture.
PENTE MOVE NOTATION
Pente moves are described in terms of positions relative to the centre of the board.
First, the x-position is described by writing R (right) or L (left), followed by the number of steps in that direction.
Then, the y-position is described by writing U (up) or D (down), again followed by the number of steps.
E.g. R3U2 refers to the position three to the right of and two above the centre of the board.
If the move is on one of the axes through the centre, one of the parts is ignored (e.g. U3 for directly up 3).
Moves made directly on the centre of the board is indicated by 0.
An asterisk (*) is made next to any move which causes a capture.
"""
# Raises a NotImplementedError if the subclasses don't have a run function
def run(self):
raise NotImplementedError
# The Gui class is a subclass of the Ui class, and contains all properties and methods required for the graphical user interface.
class Gui(Ui):
def __init__(self):
super().__init__()
self._MAX_CANVAS_SIZE = 730
self._currentBoard = None
self._playing = False
self._markLastPiece = False
self._root = Tk()
self._root.title("Pente")
self._menuFrame = Frame(self.root)
self._menuFrame.grid(row=0, column=0, sticky="EW")
self._gameFrame = Frame(self.root)
self._gameFrame.grid(row=0, column=1, sticky="EW")
self._buttons = []
self._optionFrame = Frame(self.root)
self._optionFrame.grid(row=0, column=2, sticky="EW")
self._headLabel = Label(self.gameFrame, bg="white", fg="black", font=("Helvetica", 18))
self._headLabel.grid(row=0, column=0, sticky="NESW")
self._playerNoPlayingLabel = None
self._c = Canvas()
self._p1CapLabel = Label(self.gameFrame, relief="ridge", font=("Helvetica", 18))
self._p1CapLabel.grid(row=1, column=0, sticky="NESW")
self._p2CapLabel = Label(self.gameFrame, relief="ridge", font=("Helvetica", 18))
self._p2CapLabel.grid(row=3, column=0, sticky="NESW")
self._updateOptionFrame()
self._updateMenuFrame()
self._updateGameFrame()
@property
def MAX_CANVAS_SIZE(self):
return self._MAX_CANVAS_SIZE
@property
def currentBoard(self):
return self._currentBoard
@currentBoard.setter
def currentBoard(self, currentBoard):
self._currentBoard = currentBoard
@property
def playing(self):
return self._playing
@playing.setter
def playing(self, playing):
self._playing = playing
@property
def markLastPiece(self):
return self._markLastPiece
@markLastPiece.setter
def markLastPiece(self, markLastPiece):
self._markLastPiece = markLastPiece
@property
def root(self):
return self._root
@root.setter
def root(self, root):
self._root = root
@property
def menuFrame(self):
return self._menuFrame
@menuFrame.setter
def menuFrame(self, menuFrame):
self._menuFrame = menuFrame
@property
def gameFrame(self):
return self._gameFrame
@gameFrame.setter
def gameFrame(self, gameFrame):
self._gameFrame = gameFrame
@property
def buttons(self):
return self._buttons
@buttons.setter
def buttons(self, buttons):
self._buttons = buttons
@property
def optionFrame(self):
return self._optionFrame
@optionFrame.setter
def optionFrame(self, optionFrame):
self._optionFrame = optionFrame
@property
def headLabel(self):
return self._headLabel
@headLabel.setter
def headLabel(self, headLabel):
self._headLabel = headLabel
@property
def c(self):
return self._c
@c.setter
def c(self, c):
self._c = c
@property
def p1CapLabel(self):
return self._p1CapLabel
@p1CapLabel.setter
def p1CapLabel(self, p1CapLabel):
self._p1CapLabel = p1CapLabel
@property
def p2CapLabel(self):
return self._p2CapLabel
@p2CapLabel.setter
def p2CapLabel(self, p2CapLabel):
self._p2CapLabel = p2CapLabel
@property
def playerNoPlayingLabel(self):
return self._playerNoPlayingLabel
@playerNoPlayingLabel.setter
def playerNoPlayingLabel(self, playerNoPlayingLabel):
self._playerNoPlayingLabel = playerNoPlayingLabel
# Starts running the GUI by calling Tkinter's mainloop subroutine.
def run(self):
self.root.mainloop()
# Creates a window which displays the rules of the game.
def _createDisplayRulesWin(self):
displayRulesWin = Toplevel(self.root)
displayRulesWin.title("Display rules")
rules = Ui._getRulesText().split("\n")
i = 0
for line in rules:
Label(displayRulesWin, text=line).grid(row=i, column=0, padx=10, pady=2)
i += 1
Button(displayRulesWin, text="Ok", command=displayRulesWin.destroy).grid(row=i, column=0, padx=10, pady=5)
# Creates a notification window with a specified title, display text, and top level window, with one Ok button which destroys the window on being pressed.
def _createNotificationWin(self, title, text, toplevel=-1):
if toplevel == -1: toplevel = self.root
notifWin = Toplevel(toplevel)
notifWin.title(title)
Label(notifWin, text=text).grid(row=0, column=0, padx=10, pady=5)
Button(notifWin, text="Ok", command=notifWin.destroy).grid(row=1, column=0, padx=10, pady=5)
# Creates a window that allows the user to choose which game mode to play.
def _chooseGameMode(self):
playGameWindow = Toplevel(self.root)
playGameWindow.title("Play")
Label(playGameWindow, text="Choose a game mode").grid(row=0, column=0, padx=10, pady=5)
Button(playGameWindow, text="Player v.s. Player", command=partial(self._confirmOppLogin, playGameWindow)).grid(row=1, column=0, padx=5)
Button(playGameWindow, text="Player v.s. Computer", command=partial(self._getComputerDifficulty, playGameWindow)).grid(row=2, column=0, padx=5)
if self.player != Player.GUEST:
Button(playGameWindow, text="Player v.s. Player (LAN)", command=partial(self._connectLan, playGameWindow)).grid(row=3, column=0, padx=5)
# If the Player v.s. Player mode is chosen, the window is changed to ask the user whether the opponent would like to login.
def _confirmOppLogin(self, playGameWindow):
for widget in playGameWindow.winfo_children(): widget.destroy()
self.opponent = Player.GUEST
Label(playGameWindow, text="Would the other player like to login?").grid(row=0, column=0, columnspan=2, padx=10, pady=5)
Button(playGameWindow, text="Yes", command=partial(self._createLoginWindow, Player.OPP, playGameWindow)).grid(row=1, column=0, padx=5)
Button(playGameWindow, text="No", command=partial(self._choosePlayer, playGameWindow, Mode.PVP)).grid(row=1, column=1, padx=5)
# Creates a window which allows the user to choose their computer difficulty.
def _getComputerDifficulty(self, playGameWindow):
for widget in playGameWindow.winfo_children(): widget.destroy()
Label(playGameWindow, text="Choose the computer difficulty").grid(row=0, column=0, padx=10, pady=5)
Button(playGameWindow, text="Easy", command=partial(self._choosePlayer, playGameWindow, Mode.COMP, 1)).grid(row=1, column=0, padx=5)
Button(playGameWindow, text="Medium", command=partial(self._choosePlayer, playGameWindow, Mode.COMP, 2)).grid(row=2, column=0, padx=5)
Button(playGameWindow, text="Hard", command=partial(self._choosePlayer, playGameWindow, Mode.COMP, 3)).grid(row=3, column=0, padx=5)
# Creates a window that allows the player to choose whether they play as player 1 or player 2.
def _choosePlayer(self, playGameWindow, mode, compDifficulty=-1):
if mode == Mode.COMP:
self.opponent = Player.COMP
if (self.player == Player.GUEST) and (self.opponent == Player.GUEST):
playGameWindow.destroy()
self._playGame(Game.P1, mode)
else:
for widget in playGameWindow.winfo_children(): widget.destroy()
if mode == Mode.COMP:
txt = "Would you like to be player 1 or player 2?"
else:
if self.player == Player.GUEST:
player = "the guest"
else:
player = self.player
txt = f"Would {player} like to be player 1 or player 2?"
Label(playGameWindow, text=txt).grid(row=0, column=0, columnspan=2, padx=10, pady=5)
Button(playGameWindow, text="Player 1", command=partial(self._playNewGame, playGameWindow, Game.P1, mode, compDifficulty)).grid(row=1, column=0, padx=5)
Button(playGameWindow, text="Player 2", command=partial(self._playNewGame, playGameWindow, Game.P2, mode, compDifficulty)).grid(row=1, column=1, padx=5)
# Destroys the choose player window and calls the playGame function to start the game.
def _playNewGame(self, playGameWindow, player, mode, compDifficulty=-1):
playGameWindow.destroy()
self._playGame(player, mode, compDifficulty)
# Creates a window providing the user the option to choose a saved game to load.
def _createLoadGameWindow(self, viewGamesWindow):
viewGamesWindow.destroy()
games = Database.loadGames(self.player, Game.ONGOING)
if not games:
self._createNotificationWin("Load game", "You have no ongoing games")
else:
loadGameWindow = Toplevel(self.root)
loadGameWindow.title("Load game")
Label(loadGameWindow, text="Select an ongoing game:").grid(row=0, column=0, padx=10, pady=5)
values = []
for gameRecord in games:
values.append(Ui._gameString(gameRecord))
comboBox = ttk.Combobox(loadGameWindow, values=values, width=100)
comboBox.current(0)
comboBox.grid(row=1, column=0, padx=10, pady=5)
Button(loadGameWindow, text="Load game", command=partial(self._loadGame, loadGameWindow, comboBox, games)).grid(row=2, column=0, padx=10, pady=5)
# Given the game information of the game being loaded, accesses the database for the game record of the game being loaded by calling the database's loadGames function, and calls the playGame procedure to start the game.
def _loadGame(self, loadGameWindow, comboBox, games):
gameInfo = comboBox.get()
for gameRecord in games:
if Ui._gameString(gameRecord) == gameInfo:
break
self.currGameRecord = gameRecord
mainPlayerPos = self._loadPlayers()
loadGameWindow.destroy()
self._playGame(mainPlayerPos, new=False)
# Returns a list of the player 1 and player 2 usernames respectively (or 'Guest' if the player is not logged in, or 'Computer' if the player is the computer).
# Used when displaying player information.
def _getCurrPlayerStrings(self):
players = []
for player in [Game.P1, Game.P2]:
p = self._getUsernameOfPlayerNumber(player)
if p == Player.GUEST:
players.append("Guest")
elif p == Player.COMP:
players.append(f"Computer({Ui._convCompDifficulty(self.currGameRecord.compDifficulty)})")
else:
players.append(p)
return players
# Creates a window allowing the user to save their currently being played game to the database and enter a game name to save it as.
def _createSaveGameWindow(self):
saveGameWindow = Toplevel(self.root)
saveGameWindow.title("Save game")
players = self._getCurrPlayerStrings()
winner = self.currGameRecord.game.winner
if winner == Game.P1:
gameStatus = "P1 won"
elif winner == Game.P2:
gameStatus = "P2 won"
elif winner == Game.DRAW:
gameStatus = "Draw"
else:
gameStatus = "ONGOING"
Label(saveGameWindow, text="Save game").grid(row=0, column=0, columnspan=2, pady=10)
Label(saveGameWindow, text="Player 1").grid(row=1, column=0, padx=5)
Label(saveGameWindow, text=players[0]).grid(row=1, column=1, padx=5)
Label(saveGameWindow, text="Player 2").grid(row=2, column=0, padx=5)
Label(saveGameWindow, text=players[1]).grid(row=2, column=1, padx=5)
Label(saveGameWindow, text="Game status").grid(row=3, column=0, padx=5)
Label(saveGameWindow, text=gameStatus).grid(row=3, column=1, padx=5)
Label(saveGameWindow, text="Save game as").grid(row=4, column=0, padx=5)
gameNameEntry = Entry(saveGameWindow)
gameNameEntry.grid(row=4, column=1, padx=5)
statusLabel = Label(saveGameWindow, text="")
statusLabel.grid(row=6, column=0, columnspan=2, pady=5)
Button(saveGameWindow, text="Confirm", command=partial(self._saveGame, saveGameWindow, gameNameEntry, statusLabel)).grid(row=5, column=0, columnspan=2, pady=10)
# Given a game name, calls the database's saveGame procedure to save the contents of the current game information with the game name to the database.
def _saveGame(self, saveGameWindow, gameNameEntry, statusLabel):
gameName = gameNameEntry.get()
if gameName == "":
statusLabel.config(text="Please enter a name to save the game as")
else:
self.currGameRecord.whenSaved, self.currGameRecord.name = datetime.now(), gameName
p1 = self._getUsernameOfPlayerNumber(Game.P1)
p2 = self._getUsernameOfPlayerNumber(Game.P2)
self.currGameRecord.id = Database.saveGame(p1, p2, self.currGameRecord)
saveGameWindow.destroy()
# Creates a window allowing the user to enter a username and password with which to create a new account.
def _createAccountWindow(self):
createAccountWindow = Toplevel(self.root)
createAccountWindow.title("Create Account")
Label(createAccountWindow, text="Create Account").grid(row=0, column=0, columnspan=2, pady=10)
Label(createAccountWindow, text="Username").grid(row=1, column=0, padx=5)
usernameEntry = Entry(createAccountWindow)
usernameEntry.grid(row=1, column=1, padx=5)
Label(createAccountWindow, text="Password").grid(row=2, column=0, padx=5)
passwordEntry1 = Entry(createAccountWindow, show="*")
passwordEntry1.grid(row=2, column=1, padx=5)
Label(createAccountWindow, text="Confirm password").grid(row=3, column=0, padx=5)
passwordEntry2 = Entry(createAccountWindow, show="*")
passwordEntry2.grid(row=3, column=1, padx=5)
statusLabel = Label(createAccountWindow, text="")
statusLabel.grid(row=5, column=0, columnspan=2, pady=5)
Button(createAccountWindow, text="Confirm", command=partial(self._createAccount, createAccountWindow, usernameEntry, passwordEntry1, passwordEntry2, statusLabel)).grid(row=4, column=0, columnspan=2, pady=10)
# Given a username and password, creates a new account by creating a new entry in the database's Player table by calling its savePlayer procedure.
def _createAccount(self, createAccountWindow, usernameEntry, passwordEntry1, passwordEntry2, statusLabel):
username, password1, password2 = usernameEntry.get(), passwordEntry1.get(), passwordEntry2.get()
if username == "" or password1 == "" or password2 == "":
statusLabel.config(text="Please make sure no entries are blank")
elif not Database.isUniqueUsername(username):
statusLabel.config(text="That username has been taken. Please try again.")
elif password1 != password2:
statusLabel.config(text="Error: passwords do not match")
else:
Database.savePlayer(username, password1, datetime.now())
self.player = username
self._updateMenuFrame()
self._updateHeadLabel()
self._updateOptionFrame()
createAccountWindow.destroy()
# Undoes the last move in the currently being played game, and displays an error if not possible.
def _undo(self):
if not self.playing:
self._createNotificationWin("Game ended", "The game has ended - you can no longer undo moves")
elif self.currGameRecord.mode == Mode.PVP:
try:
self.currGameRecord.game.undo()
except GameError as e:
self._createNotificationWin("Error", f"{e}.")
else:
self._updateState()
else:
try:
self.currGameRecord.game.undo()
self.currGameRecord.game.undo()
except GameError as e:
self._createNotificationWin("Error", f"{e}.")
else:
self._updateState()
# Updates how the option frame is displayed (the right-most frame in the GUI) depending on the current state of the game being played.
def _updateOptionFrame(self):
for widget in self.optionFrame.winfo_children(): widget.destroy()
if self.playing:
self.playerNoPlayingLabel = Label(self.optionFrame)
self.playerNoPlayingLabel.grid(row=0, column=0, padx=10, pady=5)
if self.currGameRecord.mode != Mode.LAN:
Button(self.optionFrame, text="Undo", command=self._undo).grid(row=1, column=0, padx=10, pady=5)
Button(self.optionFrame, text="Quit game", command=self._confirmQuit).grid(row=2, column=0, padx=10, pady=5)
txt = "Switch Mark Last Piece OFF" if self.markLastPiece else "Switch Mark Last Piece ON"
Button(self.optionFrame, text=txt, command=self._switchMarkPiece).grid(row=3, column=0, padx=10, pady=5)
if self.currGameRecord.mode != Mode.LAN:
Button(self.optionFrame, text="Get suggested move", command=self._createGetSuggestedMoveWin).grid(row=4, column=0, padx=10, pady=5)
if self.currGameRecord.mode != Mode.PVP:
if self.currPlayers[Game.P1] == Player.MAIN:
Label(self.optionFrame, text="YOU ARE PLAYER 1").grid(row=6, column=0, padx=10, pady=5)
else:
Label(self.optionFrame, text="YOU ARE PLAYER 2").grid(row=6, column=0, padx=10, pady=5)
if self.player != Player.GUEST or (self.opponent not in [Player.GUEST, Player.COMP]):
if self.currGameRecord.id == -1:
command = self._createSaveGameWindow
else:
command = self._createSavedGameConfirmationWindow
if self.currGameRecord.mode != Mode.LAN:
Button(self.optionFrame, text="Save game", command=command).grid(row=5, column=0, padx=10, pady=5)
else:
Label(self.optionFrame, text="Start playing?").grid(row=0, column=0, padx=10, pady=5)
# Creates a window which displays a suggested move for the player
def _createGetSuggestedMoveWin(self):
if not self.playing:
self._createNotificationWin("Suggested move", "You can't get a suggested move when the game has ended")
return
suggestedMoveWin = Toplevel(self.root)
suggestedMoveWin.title("Get suggested move")
moveLabel = Label(suggestedMoveWin, text="Getting suggested move... (please don't exit the window)")
moveLabel.grid(row=0, column=0, padx=10, pady=5)
x = threading.Thread(target=self._getSuggestedMove, args=(suggestedMoveWin, moveLabel))
x.start()
# Displays the suggested move on the suggested move window
def _getSuggestedMove(self, suggestedMoveWin, moveLabel):
row, col = Ai.play(self.currGameRecord.game.board, self.currGameRecord.game.captures, self.currGameRecord.game.player, 3)
moveLabel.config(text=f"Suggested move: {Game.getPenteMoveNotation(row, col, len(self.currGameRecord.game.board), False)}")
Button(suggestedMoveWin, text="Ok", command=suggestedMoveWin.destroy).grid(row=1, column=0, padx=10, pady=5)
# Switches the status of the "mark last piece" option and updates the GUI display
def _switchMarkPiece(self):
self.markLastPiece = not self.markLastPiece
self._updateOptionFrame()
self._updateState()
# Updates the game by calling the database's updateGame procedure, and creates a notification window to notify the user that the game was updated.
def _createSavedGameConfirmationWindow(self):
self.currGameRecord.whenSaved = datetime.now()
Database.updateGame(self.currGameRecord)
self._createNotificationWin("Game saved", "Your game has been saved")
# Updates how the menu frame is displayed (the left-most frame in the GUI) depending on whether the user is logged in or not.
def _updateMenuFrame(self):
for widget in self.menuFrame.winfo_children(): widget.destroy()
if self.player == Player.GUEST:
Label(self.menuFrame, text="Welcome to Pente!").grid(row=0, column=0, padx=10, pady=5)
Button(self.menuFrame, text="Display rules", command=self._createDisplayRulesWin).grid(row=1, column=0, padx=10, pady=5)
Button(self.menuFrame, text="Play new game", command=self._chooseGameMode).grid(row=2, column=0, padx=10, pady=5)
Button(self.menuFrame, text="Login", command=partial(self._createLoginWindow, Player.MAIN, self.root)).grid(row=3, column=0, padx=10, pady=5)
Button(self.menuFrame, text="Create Account", command=self._createAccountWindow).grid(row=4, column=0, padx=10, pady=5)
else:
Label(self.menuFrame, text=f"Welcome {self.player} to Pente!").grid(row=0, column=0, padx=10, pady=5)
Button(self.menuFrame, text="Display rules", command=self._createDisplayRulesWin).grid(row=1, column=0, padx=10, pady=5)
Button(self.menuFrame, text="Play new game", command=self._chooseGameMode).grid(row=2, column=0, padx=10, pady=5)
Button(self.menuFrame, text="View games", command=self._createViewGamesWindow).grid(row=3, column=0, padx=10, pady=5)
Button(self.menuFrame, text="View profile", command=self._createViewProfileWindow).grid(row=4, column=0, padx=10, pady=5)
Button(self.menuFrame, text="Logout", command=self._logout).grid(row=5, column=0, padx=10, pady=5)
# Creates a window which allows the user to view their player profile.
def _createViewProfileWindow(self):
viewProfileWindow = Toplevel(self.root)
viewProfileWindow.title("View profile")
whenSaved, numberOfWins, numberOfLosses, numberOfDraws, score = Database.getPlayer(self.player)
numberOfSavedGames = len(Database.loadAllGames(self.player))
numberOfOngoings = len(Database.loadGames(self.player, Game.ONGOING))
totalNumberOfGames = sum([numberOfWins, numberOfLosses, numberOfDraws])
rank = Database.getPlayerRank(self.player)
Label(viewProfileWindow, text=f"{self.player}'s profile:").grid(row=0, column=0, padx=10, pady=10)
Label(viewProfileWindow, text=f"Number of finished games: {totalNumberOfGames}").grid(row=1, column=0, padx=10, pady=5)
Label(viewProfileWindow, text=f"Number of won games: {numberOfWins}").grid(row=2, column=0, padx=10, pady=5)
Label(viewProfileWindow, text=f"Number of lost games: {numberOfLosses}").grid(row=3, column=0, padx=10, pady=5)
Label(viewProfileWindow, text=f"Number of drawn games: {numberOfDraws}").grid(row=4, column=0, padx=10, pady=5)
Label(viewProfileWindow, text=f"Number of saved games: {numberOfSavedGames}").grid(row=5, column=0, padx=10, pady=5)
Label(viewProfileWindow, text=f"Number of ongoing games: {numberOfOngoings}").grid(row=6, column=0, padx=10, pady=5)
Label(viewProfileWindow, text=f"Score: {score}").grid(row=7, column=0, padx=10, pady=10)
Label(viewProfileWindow, text=f"Rank: {rank}").grid(row=8, column=0, padx=10, pady=5)
Label(viewProfileWindow, text=f"Profile created on {datetime.strftime(whenSaved, '%d/%m/%Y, %H:%M:%S')}").grid(row=9, column=0, padx=10, pady=10)
Button(viewProfileWindow, text="Ok", command=viewProfileWindow.destroy).grid(row=10, column=0, padx=10, pady=5)
# Creates a window which allows the user to view their saved games, and to decide whether to load or delete any games.
def _createViewGamesWindow(self):
games = Database.loadAllGames(self.player)
if not games:
self._createNotificationWin("View games", "There are no games to view.")
else:
viewGamesWindow = Toplevel(self.root)
viewGamesWindow.title("View games")
Label(viewGamesWindow, text="Saved games:").grid(row=0, column=0, columnspan=3, padx=10, pady=5)
for i, gameRecord in enumerate(games):
Label(viewGamesWindow, text=f"{i+1}. {Ui._gameString(gameRecord)}").grid(row=i+1, column=0, columnspan=4, padx=10, pady=5)
Button(viewGamesWindow, text="Load game", command=partial(self._createLoadGameWindow, viewGamesWindow)).grid(row=i+2, column=0, padx=10, pady=5)
Button(viewGamesWindow, text="Delete game", command=partial(self._createDeleteGameWindow, viewGamesWindow, games)).grid(row=i+2, column=1, padx=10, pady=5)
Button(viewGamesWindow, text="Export game moves", command=partial(self._createExportGameMovesWindow, viewGamesWindow, games)).grid(row=i+2, column=2, padx=10, pady=5)
Button(viewGamesWindow, text="Go back", command=viewGamesWindow.destroy).grid(row=i+2, column=3, padx=10, pady=5)
# Creates a window which allows the user to select a game to export a file of its moves from
def _createExportGameMovesWindow(self, viewGamesWindow, games):
viewGamesWindow.destroy()
exportGameWindow = Toplevel(self.root)
exportGameWindow.title("Export game moves")
Label(exportGameWindow, text="Select a game:").grid(row=0, column=0, padx=10, pady=5)
values = []
for gameRecord in games:
values.append(Ui._gameString(gameRecord))
comboBox = ttk.Combobox(exportGameWindow, values=values, width=100)
comboBox.current(0)
comboBox.grid(row=1, column=0, padx=10, pady=5)
Button(exportGameWindow, text="Export game moves", command=partial(self._exportGameMoveFile, exportGameWindow, comboBox, games)).grid(row=2, column=0, padx=10, pady=5)
# Given the game information to export a move record for, it creates the file using the Ui exportGameMoves procedure and creates a notification window.
def _exportGameMoveFile(self, exportGameWindow, comboBox, games):
gameInfo = comboBox.get()
for gameRecord in games:
if Ui._gameString(gameRecord) == gameInfo:
break
Ui._exportGameMoves(gameRecord)
exportGameWindow.destroy()
self._createNotificationWin("Game move record created", f"Game move record for '{gameRecord.name}' successfully created.")
# Creates a window allowing the user to select a game to delete.
def _createDeleteGameWindow(self, viewGamesWindow, games):
viewGamesWindow.destroy()
deleteGameWindow = Toplevel(self.root)
deleteGameWindow.title("Delete game")
Label(deleteGameWindow, text="Select a game:").grid(row=0, column=0, padx=10, pady=5)
values = []
for gameRecord in games:
values.append(Ui._gameString(gameRecord))
comboBox = ttk.Combobox(deleteGameWindow, values=values, width=100)
comboBox.current(0)
comboBox.grid(row=1, column=0, padx=10, pady=5)
Button(deleteGameWindow, text="Delete game", command=partial(self._deleteGame, deleteGameWindow, comboBox, games)).grid(row=2, column=0, padx=10, pady=5)
# Give game information of the game to delete and a list of games, the function deletes the game from the database using the database's deleteGame procedure.
def _deleteGame(self, deleteGameWindow, comboBox, games):
gameInfo = comboBox.get()
for gameRecord in games:
if Ui._gameString(gameRecord) == gameInfo:
break
Database.deleteGame(gameRecord.id)
deleteGameWindow.destroy()
self._createNotificationWin("Game deleted", f"Game {gameRecord.name} successfully deleted.")
# Creates a window asking for the user's confirmation to quit the currently being played game.
def _confirmQuit(self):
confirmQuitWindow = Toplevel(self.root)
confirmQuitWindow.title("Quit?")
Label(confirmQuitWindow, text="Are you sure you want to quit?").grid(row=0, column=0, columnspan=2)
if self.currGameRecord.mode != Mode.LAN and self.playing:
txt = "(any unsaved progress will be lost)"
elif self.currGameRecord.mode == Mode.LAN and self.playing:
txt = "(quitting early will mean you automatically lose the game)"
else:
txt = ""
Label(confirmQuitWindow, text=txt).grid(row=1, column=0, columnspan=2)
Button(confirmQuitWindow, text="Yes", command=partial(self._quitGame, confirmQuitWindow)).grid(row=2, column=0)
Button(confirmQuitWindow, text="No", command=confirmQuitWindow.destroy).grid(row=2, column=1)
# Quits the currently being played game and updates the GUI display appropriately.
def _quitGame(self, confirmQuitWindow):
if self.currGameRecord.mode == Mode.LAN and self.playing:
if not self.client.requestingMove:
self.client.makeMove((-1, -1))
self.client.closeConnection()
if self.playing:
self.currGameRecord.game.winner = Game.P1 if self.currPlayers[Game.P1] == Player.OPP else Game.P2
self._addUserResult(self.player)
self._createNotificationWin("Profile updated", "Your profile has been updated with the game result.")
else:
self._createNotificationWin("Quit Error", "You can only quit on your turn", confirmQuitWindow)
return
self.playing = False
self._updateGameFrame()
self._updateOptionFrame()
confirmQuitWindow.destroy()
# Logs out the currently logged in user and updates the GUI display appropriately.
def _logout(self):
self.player = Player.GUEST
self._updateMenuFrame()
self._updateHeadLabel()
self._updateOptionFrame()
# Creates a window allowing the user to enter a username and password to log into an existing account.
def _createLoginWindow(self, player, toplevel):
loginWindow = Toplevel(toplevel)
loginWindow.title("Login")
Label(loginWindow, text="Login").grid(row=0, column=0, columnspan=2, pady=10)
Label(loginWindow, text="Username").grid(row=1, column=0, padx=5)
usernameEntry = Entry(loginWindow)
usernameEntry.grid(row=1, column=1, padx=5)
Label(loginWindow, text="Password").grid(row=2, column=0, padx=5)
passwordEntry = Entry(loginWindow, show="*")
passwordEntry.grid(row=2, column=1, padx=5)
statusLabel = Label(loginWindow, text="")
statusLabel.grid(row=4, column=0, columnspan=2, pady=5)
Button(loginWindow, text="Confirm", command=partial(self._login, loginWindow, player, usernameEntry, passwordEntry, statusLabel, toplevel)).grid(row=3, column=0, columnspan=2, pady=10)
# Given a username and password, logs the user in by calling the database's checkPassword function to check that the username and passwords match.
def _login(self, loginWindow, player, usernameEntry, passwordEntry, statusLabel, toplevel):
username, password = usernameEntry.get(), passwordEntry.get()
if Database.checkPassword(username, password):
if player == Player.MAIN:
self.player = username
self._updateMenuFrame()
else:
self.opponent = username
if self.playing:
self._updateHeadLabel()
self._updateOptionFrame()
loginWindow.destroy()
if player == Player.OPP: self._choosePlayer(toplevel, Mode.PVP)
else:
statusLabel.config(text="Incorrect username or password")
# Updates the head label (the central label at the top of the GUI) depending on the current game status.
def _updateHeadLabel(self):
if not self.playing:
self.headLabel.config(text="PENTE", fg="black")
else:
players = self._getCurrPlayerStrings()
self.headLabel.config(text=f"{players[0]} v.s. {players[1]}")
# Given the player number of the user (mainPlayer), the game mode, and whether the game is new or loaded, the playGame function creates the board images and starts the game.
def _playGame(self, mainPlayer, mode=Mode.PVP, compDifficulty=-1, new=True):
self.playing = True
self.currPlayers[mainPlayer] = Player.MAIN
otherPlayer = Game.P1 if mainPlayer == Game.P2 else Game.P2
self.currPlayers[otherPlayer] = Player.OPP
if new:
gridsize = 19
self.currGameRecord = GameRecord(game=Game(gridsize), mode=mode, compDifficulty=compDifficulty)
else:
gridsize = len(self.currGameRecord.game.board)
self.currentBoard = [[Game.EMPTY for _ in range(gridsize)] for _ in range(gridsize)]
canvasSize = self.MAX_CANVAS_SIZE - (self.MAX_CANVAS_SIZE%gridsize)
squareSize = canvasSize//(gridsize+1)
self._createImages(squareSize)
self._updateOptionFrame()
self._updateGameFrame(squareSize, canvasSize, gridsize)
if self.currGameRecord.mode == Mode.LAN:
if self.currPlayers[self.currGameRecord.game.player] == Player.MAIN:
self._play(gridsize//2, gridsize//2)
self._updateState()
self.client.makeMove((gridsize//2, gridsize//2))
x = threading.Thread(target=self._lanGetDisplayMove)
x.start()
else:
self._play(gridsize//2, gridsize//2)
self._updateState()
if self._getUsernameOfPlayerNumber(self.currGameRecord.game.player) == Player.COMP:
self._playComputer()
# Calls functions which create the images for the empty board cells and the player pieces.
def _createImages(self, squareSize):
self._createEmptyCellImage(squareSize)
self._createPlayerImage(squareSize, "red", "player1.png")
self._createPlayerImage(squareSize, "blue", "player2.png")
self._createMarkedPlayerImage(squareSize, "red", "player1Marked.png")
self._createMarkedPlayerImage(squareSize, "blue", "player2Marked.png")
# Draws an image of an empty cell.
def _createEmptyCellImage(self, squareSize):
img = Image.new("RGBA", (squareSize+6, squareSize+6), (255, 0, 0, 0))
draw = ImageDraw.Draw(img)
draw.line((img.size[0]/2, 0, img.size[0]/2, img.size[1]), fill="black")
draw.line((0, img.size[1]/2, img.size[0], img.size[1]/2), fill="black")
img.save("emptyCell.png", "PNG")
# Draws an image of a player piece of a specified colour.
def _createPlayerImage(self, squareSize, colour, name):
img = Image.new("RGBA", (squareSize+6, squareSize+6), (255, 0, 0, 0))
draw = ImageDraw.Draw(img)
draw.line((img.size[0]/2, 0, img.size[0]/2, img.size[1]), fill="black")
draw.line((0, img.size[1]/2, img.size[0], img.size[1]/2), fill="black")
draw.ellipse((img.size[0]/4, img.size[1]/4, img.size[0]*3/4, img.size[1]*3/4), fill=colour, outline="black")
img.save(name, "PNG")
# Draws an image of a player piece of a specified colour with black mark to indicate it was the last piece played.
def _createMarkedPlayerImage(self, squareSize, colour, name):
img = Image.new("RGBA", (squareSize+6, squareSize+6), (255, 0, 0, 0))
draw = ImageDraw.Draw(img)
draw.line((img.size[0]/2, 0, img.size[0]/2, img.size[1]), fill="black")
draw.line((0, img.size[1]/2, img.size[0], img.size[1]/2), fill="black")
draw.ellipse((img.size[0]/4, img.size[1]/4, img.size[0]*3/4, img.size[1]*3/4), fill=colour, outline="black")
draw.ellipse((img.size[0]*7/16, img.size[1]*7/16, img.size[0]*9/16, img.size[1]*9/16), fill="black", outline="black")
img.save(name, "PNG")
# Creates a 2D array of Button objects which are positioned on the board.
# Clicking a button indicates the user wants to place a piece at that position on the board.
# When a button is pressed, the place function is called with the button position on the board passed in as arguments.
def _getButtons(self, squareSize, gridsize):
photoImg = PhotoImage(file="emptyCell.png")
buttons = [[Button(self.gameFrame, width = squareSize, height = squareSize, image = photoImg, bg = "white", relief = FLAT, command = partial(self._place, y, x)) for x in range(gridsize)] for y in range(gridsize)]
for y, buttonRow in enumerate(buttons):
for x, button in enumerate(buttonRow):
button.image = photoImg
button_window = self.c.create_window(squareSize*(x+1), squareSize*(y+1), window=button)
return buttons
# Gets a move from the AI and plays it on the board.
def _playComputer(self):
row, col = Ai.play(self.currGameRecord.game.board, self.currGameRecord.game.captures, self.currGameRecord.game.player, self.currGameRecord.compDifficulty)
self._play(row, col)
self._updateState()
# Called when a button on the board is clicked, with the arguments indicating the position on the board.
# The place function calls the play function to play a piece at the specified position if the move is valid. The GUI display is updated to show this.
# If the user is playing against the computer or Player v.s. Player LAN, the function also gets the next move from the opponent.
def _place(self, row, col):
if (self.currGameRecord.game.winner != Game.ONGOING) or (self.currGameRecord.mode != Mode.PVP and self.currPlayers[self.currGameRecord.game.player] == Player.OPP): return
try:
Game.validateRowCol(row, col, self.currGameRecord.game.board)
except GameError as e:
self._createNotificationWin("Error", f"{e}. Try again.")
else:
if self.currGameRecord.mode == Mode.LAN:
self.client.makeMove((row, col))
self._play(row, col)
self._updateState()
x = threading.Thread(target=self._lanGetDisplayMove)
x.start()
else:
self._play(row, col)
self._updateState()
if self.currGameRecord.mode == Mode.COMP and self.currGameRecord.game.winner == Game.ONGOING:
self.root.after(1, self._playComputer)
# Called after a user playing Player v.s. Player LAN places a piece on the board.
# Gets the move from the opponent by calling the client's getMove function, and then plays and displays the new board state.
# If the opponent has quit, the game display is cleared and the user is notified.
def _lanGetDisplayMove(self):
if not self.playing:
return
row, col = self.client.getMove()
if (row, col) == (-1, -1):
self.playing = False
self._updateGameFrame()
self._updateOptionFrame()
self.currGameRecord.game.winner = Game.P1 if self.currPlayers[Game.P1] == Player.MAIN else Game.P2
self._addUserResult(self.player)
self._createNotificationWin("Opponent quit", "Your opponent has quit early - you have won the game (and your profile has been updated with the result)")
return
self._play(row, col)
self._updateState()
# Updates the display with the current game information such as the number of captured pieces and the board state.
def _updateState(self):
self.p1CapLabel.config(text=f"Player 1 captured pairs: {len(self.currGameRecord.game.captures[Game.P1])}")
self.p2CapLabel.config(text=f"Player 2 captured pairs: {len(self.currGameRecord.game.captures[Game.P2])}")
prevMoves = [None, None]
try:
lastStack = self.currGameRecord.game.moveStack.pop()
prevMoves[0] = lastStack[1:]
except GameError:
pass
else:
try:
prevMoves[1] = self.currGameRecord.game.moveStack.peek()[1:]
except GameError:
pass
self.currGameRecord.game.moveStack.push(lastStack[0], lastStack[1], lastStack[2])
for row in range(len(self.currGameRecord.game.board)):
for col in range(len(self.currGameRecord.game.board)):
if self.currGameRecord.game.board[row][col] == self.currentBoard[row][col] and (row, col) not in prevMoves:
continue
if self.markLastPiece and prevMoves[0] == (row, col):
self._updateCell(row, col, self.currGameRecord.game.board[row][col], mark=True)
elif self.markLastPiece and prevMoves[1] == (row, col):
self._updateCell(row, col, self.currGameRecord.game.board[row][col])
else:
self._updateCell(row, col, self.currGameRecord.game.board[row][col])
self.currentBoard = deepcopy(self.currGameRecord.game.board)
if self.currGameRecord.game.player == Game.P1:
self.playerNoPlayingLabel.config(text="Player 1 to play")
else:
self.playerNoPlayingLabel.config(text="Player 2 to play")
# Updates the image displayed for a single board cell given its position and the piece it should hold.
def _updateCell(self, row, col, piece, mark=False):
if piece == Game.EMPTY:
filename = "emptyCell.png"
elif not mark:
if piece == Game.P1:
filename = "player1.png"
else:
filename = "player2.png"
else:
if piece == Game.P1:
filename = "player1Marked.png"
else:
filename = "player2Marked.png"
photoImg = PhotoImage(file=filename)
button = self.buttons[row][col]
button.configure(image=photoImg)
button.image = photoImg
# Updates the headLabel (the central label at the top of the GUI) to show the game winner.
def _displayWin(self):
if self.currGameRecord.game.winner == Game.P1:
msg = "Player 1 WON!"
colour = "red"
elif self.currGameRecord.game.winner == Game.P2:
msg = "Player 2 WON!"
colour = "blue"
else:
msg = "It is a draw."
colour = "black"
self.headLabel.config(text=msg, fg=colour)
# Updates the game frame (the central frame) to either display the default screen (when no game is playing), or a new board (if a game is being played).
def _updateGameFrame(self, squareSize=None, canvasSize=None, gridsize=None):
self.c.delete("all")
self._updateHeadLabel()
if not self.playing:
bgColour = "#D3D3D3"
self.p1CapLabel.config(text="", bg=bgColour)
self.p2CapLabel.config(text="", bg=bgColour)
self.c = Canvas(self.gameFrame, height=self.MAX_CANVAS_SIZE, width=self.MAX_CANVAS_SIZE, bg=bgColour)
self.c.grid(row=2, column=0)