-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathchess.py
More file actions
3057 lines (2849 loc) · 162 KB
/
chess.py
File metadata and controls
3057 lines (2849 loc) · 162 KB
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
# -*- coding: utf-8 -*-
#-------------------------------------------------------------------------------
# Name: Chess
# Purpose:
#
# Author: David Muller
#
# Created: 03/29/2014
# Copyright: (c) David Muller 2014
# Licence: <your license>
#-------------------------------------------------------------------------------
try:
from tkinter import *
except:
print("The tkinter module is not installed. Please install it and try again.")
try:
from tkinter import filedialog
except:
print("The tkinter/filedialog module is not installed. Please install it and try again.")
try:
from tkinter import messagebox
except:
print("The tkinter/messagebox module is not installed. Please install it and try again.")
try:
from tkinter import colorchooser
except:
print("The tkinter/colorchooser module is not installed. Please install it and try again.")
try:
from pygame import mixer
except:
print("The pygame/mixer module is not installed. Please install it and try again.")
try:
from PIL import Image, ImageTk, ImageSequence
except:
print("The Pillow module is not installed. Please install it and try again.")
try:
import chess_notation as cn
except:
print("The Chess Notation module is not available. Please add it to the working folder and try again.")
try:
import timecontrol as tc
except:
print("The TimeControl module is not available.")
from random import *
import sys
import time
import os
class Player(object):
"""
An object of this class represents a player.
Attributes:
color (string): a string representing the player's color
(black or white)
mode (string): a string representing the player's choice of UI mode -
two clicks to move or click-and-drag to move
"""
def __init__(self, color):
self.color = color
self.mode = "click"
class Piece(object):
"""
An object of this class represents a chess piece.
Attributes:
all_squares (dict): a reference to the game's all_squares
color (string): the color of the piece
location(string): a two-character representation of the piece's location
"""
def __init__(self, all_squares, color, location):
self.all_squares = all_squares
self.color = color # the color of a piece - either "black" or "white"
self.location = location # the location of the piece, expressed
# as a 2-char string. "00" is the top left square, "07" is the bottom
# left, "70" is the top right, and "77" is the bottom right.
def add_if_okay(self, num1, num2):
"""
This method is used for pieces whose movesets extend until they hit
a piece: queens, rooks, bishops.
Parameters:
num1 (string): the x value of the move, from "0" to "7"
num2 (string): the y value of the move, from "0" to "7"
"""
if num1 not in range(8) or num2 not in range(8): # if not on board
return False # don't add to moves
loc = str(num1) + str(num2)
contents = self.all_squares.get(str(num1)+str(num2)).piece
if contents is None: # if the destination is empty
self.moveset.add(loc) # add to moves
return True # could be moves in that direction
else: # if there's a piece there
if contents.color is not self.color: # and it's an enemy
self.moveset.add(loc) # add it
return False # stop looking for moves in that direction
class Pawn(Piece):
"""
An object of this class represents a pawn piece.
Attributes:
direction (int): describes the direction the pawn can move - -1 for "up"
or 1 for "down"
moved (boolean): starts False, and then changes to True if it gets moved
vulnerable (boolean): a vulnerable state, caused by a first move
spanning two spaces, lasts for one turn and enables the pawn to be
captured en passant by enemy pawns
type (string): a string describing the piece with its color and type
moveset (set): a set of locations where this piece may move
"""
def __init__(self, all_squares, color, location):
super().__init__(all_squares, color, location)
self.direction = 1
if color == "white": # white player
self.direction = -1 # moves 'up' the board, negative on canvas
self.moved = False # hasn't moved yet
self.vulnerable = False # not vulnerable to en passant
self.type = self.color + "_pawn" # for dictionary key
def generate_moveset(self):
"""
Generates a moveset for this piece. Pawns can move ahead one space. If
it's their first move, they can move ahead two spaces. If there's an
enemy to their front-left or front-right, they can move and capture. If
an enemy pawn's first move brings them adjacent to the pawn in question,
the enemy pawn can be captured by moving to the front-left or
front-right.
"""
self.moveset = set() # start with an empty set
range8 = range(8) # generate a range(8) object and save it
# look one space ahead
loc = self.location[0] + str(int(self.location[1])+self.direction)
if len(loc) == 2: # if there's no '-' sign in the location
# and it's on the board
if int(loc[0]) in range8 and int(loc[1]) in range8:
if self.all_squares.get(loc).piece is None: # and empty
self.moveset.add(loc) # add it
# look another space ahead and do the same thing
loc = loc[0]+str(int(loc[1])+self.direction)
if len(loc) == 2:
if int(loc[0]) in range8 and int(loc[1]) in range8:
# make sure it's the first move, though
if not self.moved and self.all_squares.get(loc).piece is None \
and self.all_squares.get(loc[0]+ \
# and we can't jump a piece, so check the square behind target
str(int(loc[1])-self.direction)).piece is None:
self.moveset.add(loc) # add it
loc = str(int(self.location[0])-1) + \
str(int(self.location[1])+self.direction) # can it capture an enemy?
if len(loc) == 2: # make sure there's no '-' sign
# and it's on the board
if int(loc[0]) in range8 and int(loc[1]) in range8:
# if there's a piece
if self.all_squares.get(loc).piece is not None:
# and it's an enemy
if self.all_squares.get(loc).piece.color is not self.color:
self.moveset.add(loc) # it's a valid move
elif int(loc[0]) in range8 and \
int(loc[1])-self.direction in range8: # no piece there
# but there is a piece behind there
if self.all_squares.get \
(loc[0]+str(int(loc[1])-self.direction)).piece is not None:
# and it's a pawn
if "pawn" in self.all_squares.get \
(loc[0]+str(int(loc[1])-self.direction)).piece.type:
if self.all_squares.get \
(loc[0]+str(int(loc[1])-self.direction)) \
.piece.vulnerable: # and it's vulnerable
self.moveset.add(loc) # add due to en passant
loc = str(int(self.location[0])+1) + \
str(int(self.location[1])+self.direction) # now the other capture square
if len(loc) == 2: # if there's no '-' sign
# and it's on the board
if int(loc[0]) in range8 and int(loc[1]) in range8:
# and there's a piece there
if self.all_squares.get(loc).piece is not None:
# and the piece is an enemy
if self.all_squares.get(loc).piece.color is not self.color:
self.moveset.add(loc) # add the location
elif int(loc[0]) in range8 and \
int(loc[1])-self.direction in range8: # no piece there
# but there is a piece behind there
if self.all_squares.get \
(loc[0]+str(int(loc[1])-self.direction)).piece is not None:
# and it's a pawn
if "pawn" in self.all_squares.get \
(loc[0]+str(int(loc[1])-self.direction)).piece.type:
if self.all_squares.get\
(loc[0]+str(int(loc[1])-self.direction)) \
.piece.vulnerable: # and it's vulnerable
self.moveset.add(loc) # add due to en passant
class Rook(Piece):
"""
An object of this class represents a rook piece.
Attributes:
moved (boolean): False if the piece hasn't moved yet, True as soon as
it has
type (string): a string describing the piece with its color and type
moveset: a set of locations where this piece may move
"""
def __init__(self, all_squares, color, location):
super().__init__(all_squares, color, location)
self.moved = False # used to check castles
self.type = self.color + "_rook" # for dictionary key
def generate_moveset(self):
"""
Generates a moveset for this piece. Rooks can move in straight lines
horizontally or vertically until they reach a piece. If the piece
is an enemy, that space is a valid move for the rook.
"""
self.moveset = set() # start with an empty set
# here we're going to start looking for pieces to add in each of
# four directions. it'll first try to add a square, and then if it's
# empty it'll move to the next one. if there's a piece in that square,
# it'll add it if the piece is an enemy and then stop looking in that
# direction. getting to the end of the board will also stop that
# direction. this method relies heavily on add_if_okay, which checks
# a square and lets this method know whether there are more pieces to
# check in that direction.
for num in range((int(self.location[1])+1),8): # look down
if not self.add_if_okay(int(self.location[0]), num):
break # stop this direction
for num in range((int(self.location[0])+1),8): # look to the right
if not self.add_if_okay(num, int(self.location[1])):
break # stop this direction
for num in range((int(self.location[1])-1),-1,-1): # look up
if not self.add_if_okay(int(self.location[0]), num):
break # stop this direction
for num in range((int(self.location[0])-1),-1,-1): # look to the left
if not self.add_if_okay(num, int(self.location[1])):
break # stop this direction
class Knight(Piece):
"""
An object of this class represents a knight piece.
Attributes:
type (string): a string describing the piece with its color and type
moveset (set): a set of locations where this piece may move
"""
def __init__(self, all_squares, color, location):
super().__init__(all_squares, color, location)
self.type = self.color + "_knight" # for dictionary key
def generate_moveset(self):
"""
Generates a moveset for this piece. Knights can move to a predetermined
set of locations relative to their current position. If a location is
off the board or occupied by an allied piece, that location is not
included in the moveset.
"""
# the knight moveset is hard-coded. we start with the full possible
# set of moves.
self.moveset = {str(int(self.location[0])-2)+ \
str(int(self.location[1])-1),
str(int(self.location[0])-2)+str(int(self.location[1])+1),
str(int(self.location[0])-1)+str(int(self.location[1])-2),
str(int(self.location[0])-1)+str(int(self.location[1])+2),
str(int(self.location[0])+1)+str(int(self.location[1])-2),
str(int(self.location[0])+1)+str(int(self.location[1])+2),
str(int(self.location[0])+2)+str(int(self.location[1])-1),
str(int(self.location[0])+2)+str(int(self.location[1])+1)}
occupied = set() # we'll populate this set with invalid moves
for place in self.moveset:
if len(place) != 2: # if there's a '-' sign
occupied.add(place) # that square is no good
continue # immediately check the next square
# if the square is not on the board
if int(place[0]) not in range(8) or int(place[1]) not in range(8):
occupied.add(place) # it's no good
continue # immediately check the next square
occupier = self.all_squares.get(place).piece
if occupier is not None: # if there is a piece there
if occupier.color is self.color: # and it's allied
occupied.add(place) # that square is no good
self.moveset -= occupied # subtract the invalid squares from the total
class Bishop(Piece):
"""
An object of this class represents a bishop piece.
Attributes:
type (string): a string describing the piece with its color and type
moveset (set): a set of locations where this piece may move
"""
def __init__(self, all_squares, color, location):
super().__init__(all_squares, color, location)
self.type = self.color + "_bishop" # for dictionary key
def generate_moveset(self):
"""
Generates a moveset for this piece. Bishops can move in diagonal lines
until they reach a piece. If the piece is an enemy, that space is a
valid move for the bishop.
"""
self.moveset = set()# start with an empty set
# here we're going to start looking for pieces to add in each of
# four directions. it'll first try to add a square, and then if it's
# empty it'll move to the next one. if there's a piece in that square,
# it'll add it if the piece is an enemy and then stop looking in that
# direction. getting to the end of the board will also stop that
# direction. this method relies heavily on add_if_okay, which checks
# a square and lets this method know whether there are more pieces to
# check in that direction.
other = int(self.location[0]) + 1 # start going to the right
for num in range((int(self.location[1])+1),8): # and down
if not self.add_if_okay(other, num): # if you hit a piece or the end
break # stop this direction
other += 1 # this is what makes it diagonal
other = int(self.location[1]) - 1 # now go up
for num in range((int(self.location[0])+1),8): # and to the right
if not self.add_if_okay(num, other): # if you hit a piece or the end
break # stop this direction
other -= 1 # decrement the axis not handled in the for loop
other = int(self.location[0]) - 1 # now go to the left
for num in range((int(self.location[1])-1),-1,-1): # and up
if not self.add_if_okay(other, num): # if you hit a piece or the end
break # stop this direction
other -= 1 # decrement the axis not handled in the for loop
other = int(self.location[1]) + 1 # now go down
for num in range((int(self.location[0])-1),-1,-1): # and to the left
if not self.add_if_okay(num, other): # if you hit a piece or the end
break # stop this direction
other += 1 # increment the axis not handled in the for loop
class King(Piece):
"""
An object of this class represents a king piece.
Attributes:
moved (boolean): False if the piece hasn't moved yet, True as soon as
it has
type (string): a string describing the piece with its color and type
moveset (set): a set of locations where this piece may move
"""
def __init__(self, all_squares, color, location):
super().__init__(all_squares, color, location)
self.moved = False
self.type = self.color + "_king" # for dictionary key
def generate_moveset(self):
"""
Generates a moveset for this piece. Kings can move to a predetermined
set of locations relative to their current position. If a location is
off the board or occupied by an allied piece, that location is not
included in the moveset.
"""
# the king moveset is hard-coded. we start with the full possible
# set of moves.
self.moveset = {str(int(self.location[0])-1)+ \
str(int(self.location[1])-1),
str(int(self.location[0])-1)+str(int(self.location[1])),
str(int(self.location[0])-1)+str(int(self.location[1])+1),
str(int(self.location[0]))+str(int(self.location[1])-1),
str(int(self.location[0]))+str(int(self.location[1])+1),
str(int(self.location[0])+1)+str(int(self.location[1])-1),
str(int(self.location[0])+1)+str(int(self.location[1])),
str(int(self.location[0])+1)+str(int(self.location[1])+1)}
occupied = set() # we'll populate this set with invalid moves
for place in self.moveset:
if len(place) != 2: # if there's a '-' sign
occupied.add(place) # that square is no good
continue # immediately check the next square
# if the square is not on the board
if int(place[0]) not in range(8) or int(place[1]) not in range(8):
occupied.add(place) # it's no good
continue # immediately check the next square
occupier = self.all_squares.get(place).piece
if occupier is not None: # if there is a piece there
if occupier.color is self.color: # and it's allied
occupied.add(place) # that square is no good
self.moveset -= occupied # subtract the invalid squares from the total
class Queen(Piece):
"""
An object of this class represents a queen piece.
Attributes:
type (string): a string describing the piece with its color and type
moveset (set): a set of locations where this piece may move
"""
def __init__(self, all_squares, color, location):
super().__init__(all_squares, color, location)
self.type = self.color + "_queen" # for dictionary key
def generate_moveset(self):
"""
Generates a moveset for this piece. Queens can move in diagonal,
horizontal, or vertical lines until they reach a piece. If the piece is
an enemy, that space is a valid move for the queen.
"""
self.moveset = set() # we'll start with an empty set
# here we're going to start looking for pieces to add in each of
# eight directions. it'll first try to add a square, and then if it's
# empty it'll move to the next one. if there's a piece in that square,
# it'll add it if the piece is an enemy and then stop looking in that
# direction. getting to the end of the board will also stop that
# direction. the latter part of this method relies heavily on
# add_if_okay, which checks a square and lets this method know whether
# there are more pieces to check in that direction.
other = int(self.location[0]) + 1 # start going to the right
for num in range((int(self.location[1])+1),8): # and down
if not self.add_if_okay(other, num): # if you hit a piece or the end
break # stop this direction
other += 1 # this is what makes it diagonal
other = int(self.location[1]) - 1 # now go up
for num in range((int(self.location[0])+1),8): # and to the right
if not self.add_if_okay(num, other): # if you hit a piece or the end
break # stop this direction
other -= 1 # decrement the axis not handled in the for loop
other = int(self.location[0]) - 1 # now go to the left
for num in range((int(self.location[1])-1),-1,-1): # and up
if not self.add_if_okay(other, num): # if you hit a piece or the end
break # stop this direction
other -= 1 # decrement the axis not handled in the for loop
other = int(self.location[1]) + 1 # now go down
for num in range((int(self.location[0])-1),-1,-1): # and to the left
if not self.add_if_okay(num, other): # if you hit a piece or the end
break # stop this direction
other += 1 # increment the axis not handled in the for loop
for num in range((int(self.location[1])+1),8): # look down
if not self.add_if_okay(int(self.location[0]), num):
break # stop this direction
for num in range((int(self.location[0])+1),8): # look to the right
if not self.add_if_okay(num, int(self.location[1])):
break # stop this direction
for num in range((int(self.location[1])-1),-1,-1): # look up
if not self.add_if_okay(int(self.location[0]), num):
break # stop this direction
for num in range((int(self.location[0])-1),-1,-1): # look to the left
if not self.add_if_okay(num, int(self.location[1])):
break # stop this direction
class Square(object):
"""
An object of this class represents a square on the board.
Attributes:
location (string): a string representing the square's location
piece (Piece): the Piece object "resting" on that Square object
"""
def __init__(self, location):
self.location = location # the location of the square, expressed
# as a 2-char string. "00" is the top left square, "07" is the bottom
# left, "70" is the top right, and "77" is the bottom right.
self.piece = None # the piece on this square. None if empty.
class Chess(object):
"""
An object of this class represents a Chess game, with easy mode, hard mode,
and two-player mode.
Attributes:
parent (Tk): the root Tk object
argvs (dictionary): a dictionary of switch:argument from the command
line. Switches are of the form "*light" and arguments are of the
form "blue".
all_squares (dictionary): The keys are two-character strings describing
the location of an object on the board, and the values are Squares
on the board.
frame (Frame): a tkinter Frame that holds the visible game
menubar (Menu): the complete menu bar
filemenu (Menu): the "file" menu cascade
settingsmenu (Menu): the "settings" menu cascade
uimenu (Menu): the "ui" menu cascade under settings
opponentmenu (Menu): the "opponent" menu cascade under settings
audiomenu (Menu): the "audio" menu cascade under settings
boardmenu (Menu): the "board" menu cascade
bgmenu (Menu): the "background" menu cascade under board
navmenu (Menu): the "navigation" menu cascade
castlemenu (Menu): the "castling" menu cascade
helpmenu (Menu): the "help" menu cascade
mode (string): "easy" for easy AI play, "hard" for hard AI play, and
"human" for 2P matches
status_message (Label): a tkinter Label that displays the appropriate
message when a player wins
white_player (Player): a Player object, using the white pieces
black_player (Player): a Player object, using the black pieces
black_king_gif (PhotoImage): a PhotoImage object containing a GIF of
the black king
black_queen_gif (PhotoImage): a PhotoImage object containing a GIF of
the black queen
black_rook_gif (PhotoImage): a PhotoImage object containing a GIF of
a black rook
black_bishop_gif (PhotoImage): a PhotoImage object containing a GIF of
a black bishop
black_knight_gif (PhotoImage): a PhotoImage object containing a GIF of
a black knight
black_pawn_gif (PhotoImage): a PhotoImage object containing a GIF of
a black pawn
white_king_gif (PhotoImage): a PhotoImage object containing a GIF of
the white king
white_queen_gif (PhotoImage): a PhotoImage object containing a GIF of
the white queen
white_rook_gif (PhotoImage): a PhotoImage object containing a GIF of
a white rook
white_bishop_gif (PhotoImage): a PhotoImage object containing a GIF of
a white bishop
white_knight_gif (PhotoImage): a PhotoImage object containing a GIF of
a white knight
white_pawn_gif (PhotoImage): a PhotoImage object containing a GIF of
a white pawn
transparent_square_gif (PhotoImage): a PhotoImage object containing a
GIF of a transparent square - when there's no Piece on a Square,
this image is displayed
piece_pics (dictionary): a dictionary describing which images go with
which pieces. The keys are strings describing the piece (like
"black_bishop"), and the values are PhotoImage instance variables
(like self.black_bishop_gif).
board (Canvas): the visible board, containing rectangles and images
squares (list): the visible squares on the board, composed of black
and white rectangles
black_rook_1 (Rook): the queenside black rook
black_knight_1 (Knight): the queenside black knight
black_bishop_1 (Bishop): the queenside black bishop
black_queen (Queen): the black queen
black_king (King): the black king
black_bishop_2 (Bishop): the kingside black bishop
black_knight_2 (Knight): the kingside black knight
black_rook_2 (Rook): the kingside black rook
black_pawns (list): a list of black Pawns starting from the left
extra_black_queens (list): a list with cells that point to the extra
queens that black can unlock via pawn promotion
white_pawns (list): a list of white Pawns starting from the left
white_knight_1 (Knight): the queenside white knight
white_bishop_1 (Bishop): the queenside white bishop
white_queen (Queen): the white queen
white_king (King): the white king
white_bishop_2 (Bishop): the kingside white bishop
white_knight_2 (Knight): the kingside white knight
white_rook_2 (Rook): the kingside white rook
extra_white_queens (list): a list with cells that point to the extra
queens that white can unlock via pawn promotion
replaying (boolean): True if the game is in the middle of replaying a move
light_square_color (string): a string like "blue" or "#0000ff", for color
dark_square_color (string): a string like "blue" or "#0000ff", for color
square_outline_color (string): a string like "blue" or "#0000ff", for color
all_pieces (list): a list that points to each piece
square_overlay (list): a 2D list that refers to each image that rests
on a square. Squares with no piece have a transparent image, and
squares with a piece have an image of that piece. This list is
compatible with all_squares - a particular cell in one array matches
the proper cell in the other array so that each square can contain
the proper image.
player (Player): a reference to the currently-active Player
first_click (boolean): describes whether the player can expect to select
a piece with this click. If it's False, the player is currently
seeing move options for their selected piece.
last_ai_piece (Piece): a reference to the last Piece object the AI
player moved - allows the program to discourage the AI from moving
a piece and ignoring the rest, which also prevents the AI from
moving one piece back and forth for several turns
chosen_piece (Piece): the Piece that a human player has selected with
their first click
last_source (string): a string representation of the last-moved piece's
previous location
last_target (string): a string representation of the last-moved piece's
current location
dragged_piece (int): an int representation of the PhotoImage to be
dragged around in click-drag mode
sound_folder (string): Folder containing the sound files.
icon_folder (string): Folder containing the icon files.
audio (float): 0 if the user doesn't sound effects, float up to 1 otherwise
sound_filenames (dictionary): a 'filename.ogg':Sound() dictionary to get
Sound() objects by filename
sound_volumes (dictionary): a 'filename.ogg':float dictionary to get
volumes by filename
bg_init (bool): whether it's the first load
bg_present (str/bool): the bg filename, or False
movelist (list): a list of moves, in the form of "0077" for moving from
top left corner to bottom right corner, or "wl" for white
castling queenside
savename (string): the current filename for the active game. Is blank
after starting a new game.
replaycounter (int): an int that tells us what move we're on, to keep track
of where we are within the movelist
screen_size (int): the width and height of the canvas, in pixels
unsaved_changes (boolean): True if there are new moves in the movelist in
memory that have not yet been saved to disk
console_window (Toplevel): a toplevel window containing a console
console_text (Text): a text area for entering code in the console
console_run (Button): a button to compile and execute the console contents
console_close (Button); a button to close the console window
movelist_box (Listbox): a listbox to display the game's moves
movelist_scrollbar (Scrollbar): a scrollbar for movelist_box
movelist_box_top (int): the index of the top visible item in the movelist_box
white_promo (str): a character '1', '2', '3', or '4' that represents the next white promo
black_promo (str): a character '1', '2', '3', or '4' that represents the next black promo
"""
def __init__(self, parent):
parent.protocol("WM_DELETE_WINDOW", self.quit) # redirects the OS's 'x' close button to self.quit()
parent.title("Chess") # title for the window
try: # try to load an img for the window's icon (top left corner of title bar)
parent.tk.call('wm', 'iconphoto', parent._w, ImageTk.PhotoImage(Image.open("ico.png")))
except: # if it fails
pass # leave the user alone
self.parent = parent
self.animating = False
self.framerate = 2
# look for argv options and rejoin into a string, then lowercase,
# then split along ' *', then discard the first item
# for each string in that, the chars to the left of the FIRST space are a dictionary key,
# those to the right are the value - using partition() instead of split() allows
# arguments with spaces, which filenames sometimes include
self.argvs = {pair.partition(' ')[0]:pair.partition(' ')[2]
for pair in (' '.join(sys.argv)).lower().split(' *')[1:]}
# switches and commands:
# *iconfolder folder
# *audiofolder folder
# *audio on off
# *blackui click drag
# *whiteui click drag
# *opponent easy hard human
# *size int
# *light color
# *dark color
# *outline color
# *savefile file
# *bg file
# Here's the frame:
self.frame = Frame(parent)
self.frame.pack()
self.screen_size = 400
self.audio = 1
tempaudio = self.argvs.get('audio') # look for an audio argv
if tempaudio: # if it exists
if tempaudio == "off": # and it says off
self.audio = 0 # turn off audio
else:
try:
if tempaudio[-1] == '%':
self.audio = max(min(float(tempaudio[:-1]) / 100, 1), 0)
else:
self.audio = max(min(float(tempaudio), 1), 0)
except:
pass # we'll just stick with 1
self.last_source = self.chosen_piece = True # these can safely be checked and approved with an 'if'
# Menu bar!
self.menubar = Menu(parent)
self.filemenu = Menu(self.menubar, tearoff=0)
self.filemenu.add_command(label="New", command=self.new_game, underline=0, accelerator="Ctrl+N")
self.filemenu.add_command(label="New timer...", command=self.choose_timer, underline=0)
self.filemenu.add_command(label="Save", command=self.save_plain, underline=0, accelerator="Ctrl+S")
self.filemenu.add_command(label="Save As...", command=self.save_as, underline=0, accelerator="Ctrl+Shift+S")
self.filemenu.add_command(label="Export game in ICCF notation...", command=self.export_iccf)
self.filemenu.add_command(label="Export all notations...", command=self.export_notations)
self.filemenu.add_command(label="Open...", command=self.load, underline=0, accelerator="Ctrl+O")
self.filemenu.add_command(label="Import ICCF and open...", command=lambda: self.load(iccf=True))
self.filemenu.add_separator()
self.filemenu.add_command(label="Quit", command=self.quit, underline=0, accelerator="Ctrl+Q")
self.menubar.add_cascade(label="File", menu=self.filemenu)
self.settingsmenu = Menu(self.menubar, tearoff=0)
self.uimenu = Menu(self.settingsmenu, tearoff=0)
self.uimenu.add_command(label="Black", command=self.black_ui_toggle, underline=0)
self.uimenu.add_command(label="White", command=self.white_ui_toggle, underline=0)
self.opponentmenu = Menu(self.settingsmenu, tearoff=0)
# lambda functions! all these do is define an inline function that calls self.set_opponent with a parameter
self.opponentmenu.add_command(label="Easy AI", command=(lambda: self.set_opponent("easy")), state=DISABLED)
self.opponentmenu.add_command(label="Hard AI", command=(lambda: self.set_opponent("hard")))
self.opponentmenu.add_command(label="Human", command=(lambda: self.set_opponent("human")))
self.audiomenu = Menu(self.settingsmenu, tearoff=0)
self.audiomenu.add_command(label="Choose volumes...", command=self.choose_audio_levels, underline=0, accelerator="Ctrl+A")
self.audiomenu.add_command(label="SFX folder...", command=self.audio_from_folder, underline=0, accelerator="Ctrl+U")
self.boardmenu = Menu(self.settingsmenu, tearoff=0)
self.boardmenu.add_command(label="Icons...", command=self.icons_from_folder, underline=0, accelerator="Ctrl+I")
self.boardmenu.add_command(label="Size...", command=self.choose_board_size)
self.boardmenu.add_command(label="Light squares...", command=(lambda: self.set_square_color('light')))
self.boardmenu.add_command(label="Dark squares...", command=(lambda: self.set_square_color('dark')))
self.boardmenu.add_command(label="Grid lines...", command=self.set_square_outline_color)
self.bgmenu = Menu(self.settingsmenu, tearoff=0)
self.bgmenu.add_command(label="Background image...", command=self.choose_bg)
self.bgmenu.add_command(label="Choose sequence folder...", command=self.choose_sequence)
self.bgmenu.add_command(label="Choose GIF...", command=self.choose_animation)
self.bgmenu.add_command(label="Choose framerate...", command=self.choose_framerate)
self.bgmenu.add_command(label="Stop/restart animation", command=self.start_animation)
self.boardmenu.add_cascade(label="Background", menu=self.bgmenu)
self.boardmenu.add_command(label="Refresh", command=(lambda: self.set_board_size(self.screen_size)), underline=0, accelerator="F5")
self.settingsmenu.add_cascade(label="UI", menu=self.uimenu)
self.settingsmenu.add_cascade(label="Opponent", menu=self.opponentmenu)
self.settingsmenu.add_cascade(label="Audio", menu=self.audiomenu)
self.settingsmenu.add_cascade(label="Board", menu=self.boardmenu)
self.menubar.add_cascade(label="Settings", menu=self.settingsmenu)
self.navmenu = Menu(self.menubar, tearoff=0)
self.navmenu.add_command(label="Step back (undo)", command=self.step_back, underline=0, accelerator="Ctrl+←")
self.navmenu.add_command(label="Step forward (redo)", command=self.step_forward, underline=0, accelerator="Ctrl+→")
self.navmenu.add_command(label="Beginning of match", command=self.step_start, underline=0, accelerator="Ctrl+↑")
self.navmenu.add_command(label="End of match", command=self.step_end, underline=0, accelerator="Ctrl+↓")
self.menubar.add_cascade(label="Navigation", menu=self.navmenu)
self.castlemenu = Menu(self.menubar, tearoff=0)
self.castlemenu.add_command(label="Black queenside", command=self.castle_black_left, underline=0)
self.castlemenu.add_command(label="Black kingside", command=self.castle_black_right, underline=0)
self.castlemenu.add_separator()
self.castlemenu.add_command(label="White queenside", command=self.castle_white_left, underline=0)
self.castlemenu.add_command(label="White kingside", command=self.castle_white_right, underline=0)
self.menubar.add_cascade(label="Castle", menu=self.castlemenu)
self.promomenu = Menu(self.menubar, tearoff=0)
self.blackpromomenu = Menu(self.promomenu, tearoff=0)
self.blackpromomenu.add_command(label="Queen", command=lambda: self.set_promotion('black', '1'))
self.blackpromomenu.add_command(label="Rook", command=lambda: self.set_promotion('black', '2'))
self.blackpromomenu.add_command(label="Bishop", command=lambda: self.set_promotion('black', '3'))
self.blackpromomenu.add_command(label="Knight", command=lambda: self.set_promotion('black', '4'))
self.whitepromomenu = Menu(self.promomenu, tearoff=0)
self.whitepromomenu.add_command(label="Queen", command=lambda: self.set_promotion('white', '1'))
self.whitepromomenu.add_command(label="Rook", command=lambda: self.set_promotion('white', '2'))
self.whitepromomenu.add_command(label="Bishop", command=lambda: self.set_promotion('white', '3'))
self.whitepromomenu.add_command(label="Knight", command=lambda: self.set_promotion('white', '4'))
self.promomenu.add_cascade(label="Black", menu=self.blackpromomenu)
self.promomenu.add_cascade(label="White", menu=self.whitepromomenu)
self.menubar.add_cascade(label="Promotion", menu=self.promomenu)
self.helpmenu = Menu(self.menubar, tearoff=0)
self.helpmenu.add_command(label="View help", command=self.help, underline=0, accelerator="F1")
self.helpmenu.add_command(label="About", command=self.about, underline=0)
self.menubar.add_cascade(label="Help", menu=self.helpmenu)
parent.config(menu=self.menubar)
# The game starts on easy mode without making the player press a button.
self.mode = "easy"
tempopponent = self.argvs.get('opponent') # look for the opponent switch
if tempopponent: # if the value wasn't None,
self.set_opponent(tempopponent) # set the opponent
# Status message. When the game ends, the result is described here.
self.status_message = Label(self.frame, text="Welcome to Chess!")
self.status_message.grid(row=1, column = 0)
# Create the two players, white and black.
self.white_player = Player("white")
self.black_player = Player("black")
# Set a default pawn promotion to queen for each player.
self.white_promo = '1'
self.black_promo = '1'
# Disable the menu options for the current default promotion
self.blackpromomenu.entryconfig(int(self.black_promo)-1, state=DISABLED)
self.whitepromomenu.entryconfig(int(self.white_promo)-1, state=DISABLED)
"""
Default sounds from http://www.trekcore.com/audio:
game_start.ogg - computerbeep_50.mp3 'Computer Beep 50'
ui_toggle.ogg - hologram_off_2.mp3 'Hologram Off 2'
audio_on.ogg - computerbeep_73.mp3 'Computer Beep 73'
select_piece.ogg - computerbeep_55.mp3 'Computer Beep 55'
move_piece.ogg - computerbeep_74.mp3 'Computer Beep 74'
computer_move.ogg - computerbeep_75.mp3 'Computer Beep 75'
castle.ogg - energize.mp3 'Energize'
undo.ogg - input_failed_clean.mp3 'Input Failed 1'
torpedo.ogg - tng_torpedo_clean.mp3 'TNG Torpedo 1'
explosion.ogg - largeexplosion4.mp3 'Large Explosion 4'
"""
self.sound_filenames = {"ui_toggle.ogg":None, "audio_on.ogg":None, "select_piece.ogg":None,
"move_piece.ogg":None, "computer_move.ogg":None, "castle.ogg":None, "undo.ogg":None,
"torpedo.ogg":None, "explosion.ogg":None, "game_start.ogg":None}
# as it turns out, we do need to save the volumes, because they're unrecoverable
# when the master is set to zero (can't divide by zero).
self.sound_volumes = {"ui_toggle.ogg":1, "audio_on.ogg":1, "select_piece.ogg":1,
"move_piece.ogg":1, "computer_move.ogg":1, "castle.ogg":1, "undo.ogg":1,
"torpedo.ogg":1, "explosion.ogg":1, "game_start.ogg":1}
try: # here, we're going to try enabling audio with pygame
mixer.init(buffer=512) # initialize the mixer
try: # first we'll look into the command line
self.sound_folder = self.argvs.get('audiofolder') # try to get the folder from argvs
# so now we check to make sure every single audio file is
# present and can be loaded. it's either this or using
# try/except every time a sound is played.
for file in self.sound_filenames:
self.sound_filenames[file] = mixer.Sound(os.path.join(self.sound_folder, file))
except: # well, something failed in the command line attempt. either
# there was no command line argv given there, or the folder was
# missing, or one of the files was missing. so now we try the
# default sound folder, checking it for every needed file.
self.sound_folder = "sfx"
for file in self.sound_filenames:
self.sound_filenames[file] = mixer.Sound(os.path.join(self.sound_folder, file))
# if we've gotten this far, then an audio load was successful
self.parent.bind("<Control-a>", self.choose_audio_levels) # bind buttons
self.parent.bind("<Control-A>", self.choose_audio_levels)
self.audiomenu.entryconfig(0, state=NORMAL)
except: # couldn't load audio for some reason. either pygame isn't
# installed on the user's machine, or it couldn't find an audio folder.
self.audiomenu.entryconfig(0, state=DISABLED)
# and we'll give the user a message telling them what happened
self.parent.after(0, self.audio_failed)
self.audio = False # we'll disable audio
# now we'll load the piece icons
try:
try:
self.load_icons(self.argvs.get('iconfolder'))
except:
self.load_icons("piece_icons")
except:
self.castlemenu.entryconfig(0, state=DISABLED)
self.castlemenu.entryconfig(1, state=DISABLED)
self.castlemenu.entryconfig(3, state=DISABLED)
self.castlemenu.entryconfig(4, state=DISABLED)
if self.audio:
messagebox.showerror(title="Error", message=''.join(("Couldn't find images. Provide a command-line argument ",
"or default directory ('piece_icons' under the current directory) with valid images. Click OK ",
"to exit the program.")))
else:
self.status_message.config(text=''.join(("Couldn't find images. Provide a command-line argument ",
"or default directory ('piece_icons' under the current directory) with valid images. Click OK ",
"to exit the program.")))
self.parent.after(1, self.parent.destroy)
return
# this organizes the piece icons into an easily-accessed dictionary.
# the keys match the 'type' string variable of Piece objects.
self.piece_pics = {"black_king":self.black_king_gif,
"black_queen":self.black_queen_gif, "black_rook":self.black_rook_gif,
"black_bishop":self.black_bishop_gif, \
"black_knight":self.black_knight_gif,
"black_pawn":self.black_pawn_gif, "white_king":self.white_king_gif,
"white_queen":self.white_queen_gif, "white_rook":self.white_rook_gif,
"white_bishop":self.white_bishop_gif, \
"white_knight":self.white_knight_gif,
"white_pawn":self.white_pawn_gif,
"transparent_square":self.transparent_square_gif}
self.unsaved_changes = False # they just started the app, so there are no unsaved changes
self.replaying = False # indicate that we're not currently replaying - important for castle functions
self.light_square_color = 'white'
self.dark_square_color = 'gray35'
self.movelist_box_top = 0 # initialize the top index of the movelist_box to 0
tempout = self.argvs.get('outline')
if tempout == 'clear':
self.square_outline_color = ''
elif tempout:
try:
Label(background=tempout)
self.square_outline_color = tempout
except:
self.square_outline_color = 'black'
else:
self.square_outline_color = 'black'
self.bg_init = True # remember to load it the first time
self.bg_present = self.argvs.get('bg') if self.argvs.get('bg') else False
# Draws the board, which involves reinitialization of match-specific
# variables.
self.new_game()
# go through the blackui, whiteui, light, and dark switches
for argument in ['blackui', 'whiteui', 'light', 'dark']:
temp = self.argvs.get(argument) # assign that switch's value to a local var
if temp: # if it wasn't None,
if argument == 'blackui' and temp == 'drag': # if it was blackui and drag,
self.black_ui_toggle() # switch black ui
elif argument == 'whiteui' and temp == 'drag': # if it was whiteui and drag,
self.white_ui_toggle() # switch white ui
else: # the only other items in the list are 'light' and 'dark', so...
self.set_square_color(argument, temp) # set those squares to that color
tempboard = self.argvs.get('size') # look for the size switch
if tempboard: # if the value wasn't None,
try:
tempboard = int(tempboard) # turn it into an int
tempsuppressaudio = self.audio
if tempsuppressaudio:
self.audio = False
parent.after(1, lambda: self.set_board_size(tempboard)) # pass it in
if tempsuppressaudio:
self.audio = True
except:
pass # or not -_-
tempfile = self.argvs.get('savefile') # look for the savefile switch
if tempfile: # if the value wasn't None,
self.load(filename=tempfile) # tell load that it was an 'init' load and pass it the filename
self.step_end() # and go to the most recent step of the replay
def draw_board(self, *args):
"""
Creates the game, wiping any previous conditions.
Parameter:
*args: maybe it was called with a keyboard shortcut
"""
# This generates the board. Rectangles are saved to a 2D array.
self.board = Canvas(self.frame, width=self.screen_size, height = self.screen_size)
self.last_source = None
range8 = range(8) # make a range(8) object and save it
if self.bg_init:
self.bg_name = 'transparent_square.gif'
self.bg_img = ImageTk.PhotoImage(Image.open(self.bg_name).resize((self.screen_size, self.screen_size)))
self.bg_ref = self.board.create_image(self.screen_size//2, self.screen_size//2, image=self.bg_img)
self.bg_init = False
if self.bg_present:
self.set_bg(self.argvs.get('bg'))
self.light_square_color = ''
self.dark_square_color = ''
bg_present = False
else:
self.bg_ref = self.board.create_image(self.screen_size//2, self.screen_size//2, image=self.bg_img)
if not self.animating:
self.set_bg(self.bg_name)
self.squares = [] # this is a 2D list of black/white squares
for row in range8:
self.squares.append([])
for column in range8:
if (row+column)%2 == 0:
color = self.light_square_color
else:
color = self.dark_square_color
try: # this might not work if the user gave an invalid color in a command line arg
self.squares[row].append(self.board.create_rectangle(row*(self.screen_size//8),
column*(self.screen_size//8),row*(self.screen_size//8)+(self.screen_size//8),
column*(self.screen_size//8)+(self.screen_size//8), fill = color, outline=self.square_outline_color))
except: # if the color was invalid,
if color == self.light_square_color: # if it was light
color = self.light_square_color = 'white' # replace invalid light with white
else: # if it was dark
color = self.dark_square_color = 'gray35' # replace invalid dark with gray35
self.squares[row].append(self.board.create_rectangle(row*(self.screen_size//8),
column*(self.screen_size//8),row*(self.screen_size//8)+(self.screen_size//8),
column*(self.screen_size//8)+(self.screen_size//8), fill = color))
self.board.grid(row=0, column=0)
self.movelist_scrollbar = Scrollbar(self.frame, orient=VERTICAL)
self.movelist_box = Listbox(self.frame, yscrollcommand=self.movelist_scrollbar.set, width=5)
self.movelist_scrollbar.config(command=self.movelist_box.yview)
self.movelist_scrollbar.grid(row=0, column=2, sticky='NS')
self.movelist_box.grid(row=0, column=1, sticky='NS')
try: # fill the movelist box if there's a movelist
self.movelist_box.insert(END, *cn.chess11_to_iccf_full(self.movelist))
except AttributeError: # if not, do nothing
pass
# Make a dictionary of location:Square.
self.all_squares = {str(row)+str(column):Square(str(row)+str(column))
for row in range8 for column in range8}
# The pieces.
self.black_rook_1 = Rook(self.all_squares, "black", "00")
self.black_knight_1 = Knight(self.all_squares, "black", "10")
self.black_bishop_1 = Bishop(self.all_squares, "black", "20")
self.black_queen = Queen(self.all_squares, "black", "30")
self.black_king = King(self.all_squares, "black", "40")
self.black_bishop_2 = Bishop(self.all_squares, "black", "50")
self.black_knight_2 = Knight(self.all_squares, "black", "60")
self.black_rook_2 = Rook(self.all_squares, "black", "70")
self.black_pawns = [Pawn(self.all_squares, "black", str(i)+'1') for i in range8]
self.extra_black_queens = [Queen(self.all_squares, "black", "88") for i in range8]
self.extra_black_rooks = [Rook(self.all_squares, "black", "88") for i in range8]
self.extra_black_bishops = [Bishop(self.all_squares, "black", "88") for i in range8]
self.extra_black_knights = [Knight(self.all_squares, "black", "88") for i in range8]
self.white_pawns = [Pawn(self.all_squares, "white", str(i)+'6') for i in range8]
self.white_rook_1 = Rook(self.all_squares, "white", "07")
self.white_knight_1 = Knight(self.all_squares, "white", "17")
self.white_bishop_1 = Bishop(self.all_squares, "white", "27")
self.white_queen = Queen(self.all_squares, "white", "37")
self.white_king = King(self.all_squares, "white", "47")
self.white_bishop_2 = Bishop(self.all_squares, "white", "57")
self.white_knight_2 = Knight(self.all_squares, "white", "67")
self.white_rook_2 = Rook(self.all_squares, "white", "77")
self.extra_white_queens = [Queen(self.all_squares, "white", "88") for i in range8]
self.extra_white_rooks = [Rook(self.all_squares, "white", "88") for i in range8]
self.extra_white_bishops = [Bishop(self.all_squares, "white", "88") for i in range8]
self.extra_white_knights = [Knight(self.all_squares, "white", "88") for i in range8]
# This will put pieces in each square to set up the game.
self.all_squares.get("00").piece = self.black_rook_1
self.all_squares.get("10").piece = self.black_knight_1
self.all_squares.get("20").piece = self.black_bishop_1
self.all_squares.get("30").piece = self.black_queen
self.all_squares.get("40").piece = self.black_king
self.all_squares.get("50").piece = self.black_bishop_2
self.all_squares.get("60").piece = self.black_knight_2
self.all_squares.get("70").piece = self.black_rook_2
for p in range8:
self.all_squares.get(str(p)+'1').piece = self.black_pawns[p]