-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathOnitama_source.py
1691 lines (1421 loc) · 69.3 KB
/
Onitama_source.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
# Standard Imports
from std_imports import *
from MCTS import *
# The onitama class represents the board game state at any instant of gameplay
class Onitama:
""" Represents the board game state of the game Onitama """
def __init__(self, verbose = 1):
""" Class constructor, all game state variables are defined here"""
# Set to 1 to print out gameplay, 0 otherwise
self.verbose = verbose
# The board is a 2D list of the 5x5 grid representation (row --> i & column --> j)
# b1,b2,b3,b4 and r1,r2,r3,r4 refer to the blue and red team pawns
# B and R refer to the blue and red team masters
self.board = [['b1','b2','B','b3','b4'], # row 0
['•','•','•','•','•'], # row 1
['•','•','•','•','•'], # row 2
['•','•','•','•','•'], #row 3
['r1','r2','R','r3','r4']] # row 4
# save the position of each piece in a dictionary. This is used to translate piece names to their position and vice-versa
# in the event the player is out of the game, the value should be saved as -1. e.g. if b3 is out of the game, it should show 'b3': -1
self.piece_state = {'b1':[0,0], 'b2': [0,1],'b3':[0,3], 'b4': [0,4], 'B': [0,2], 'r1':[4,0], 'r2': [4,1],'r3':[4,3], 'r4': [4,4], 'R': [4,2]}
self.blue_pieces = ['b1', 'b2', 'b3', 'b4', 'B'] # list of blue pieces
self.red_pieces = ['r1', 'r2', 'r3', 'r4', 'R'] # list of red pieces
# The cards dictionary store the moves for each one of the 16 cards in onitama. The key of each card is another dictionary with the move name and move coordinate change
# The movement is stored in (i,-j) format for a BLUE player and (-i,j) for a RED player wrt the board.
# NOTE that for a red player, i must be multiplied by -1 when implementing into the board; while for a blue player, j must be multiplied by -1
# hence in the cards dictionary a positive first index means moving forward and a positive second index means moving to the right for ANY player
# The moves are given names (A, B, C & D) which we shall refer to as the variable 'choice' in the move method of this class
self.cards = {
'tiger' : {'A' : (2,0), 'B' : (-1,0)},
'dragon' : {'A' : (1,2), 'B' : (1,-2), 'C' : (-1,1), 'D' : (-1,-1)},
'frog' : {'A' : (0,-2), 'B' : (1,-1), 'C' : (-1,1)},
'rabbit' : {'A' : (1,1), 'B' : (0,2), 'C' : (-1,-1)},
'crab' : {'A' : (1,0), 'B' : (0,2), 'C' : (0,-2)},
'elephant' : {'A' : (1,1), 'B' : (1,-1), 'C' : (0,1), 'D' : (0,-1)},
'goose' : {'A' : (1,-1), 'B' : (0,-1), 'C' : (0,1), 'D' : (-1,1)},
'rooster' : {'A' : (-1,-1), 'B' : (0,-1), 'C' : (0,1), 'D' : (1,1)},
'monkey' : {'A' : (-1,-1), 'B' : (-1,1), 'C' : (1,-1), 'D' : (1,1)},
'mantis' : {'A' : (-1,0), 'B' : (1,1), 'C' : (1,-1)},
'horse' : {'A' : (1,0), 'B' : (-1,0), 'C' : (0,-1)},
'ox' : {'A' : (1,0), 'B' : (0,1), 'C' : (-1,0)},
'crane' : {'A' : (-1,1), 'B' : (-1,-1), 'C' : (1,0)},
'boar' : {'A' : (1,0), 'B' : (0,1), 'C' : (0,-1)},
'eel' : {'A' : (1,-1), 'B' : (-1,-1), 'C' : (0,1)},
'cobra' : {'A' : (0,-1), 'B' : (1,1), 'C' : (-1,1)}
}
# as the decision on who gets to go first is dependent on the colour of the card on the side, this dictionary saves that information
self.card_colour = {
'tiger' : 'blue',
'dragon' : 'red',
'frog' : 'red',
'rabbit' : 'blue',
'crab' : 'blue',
'elephant' : 'red',
'goose' : 'blue',
'rooster' : 'red',
'monkey' : 'blue',
'mantis' : 'red',
'horse' : 'red',
'ox' : 'blue',
'crane' : 'blue',
'boar' : 'red',
'eel' : 'blue',
'cobra' : 'red'
}
# Create empty representations of the position of the cards available to each player and the card on the side
self.blue_cards = []
self.red_cards = []
self.side_card = []
# Initialise a few more attributes that can change depending on the gameplay
self.whose_turn = None # either 'blue' or 'red' during gameplay
self.number_of_turns = 0 # increment by 1 after each player makes a move
self.blue_win = False
self.red_win = False
self.selected_pawn = None # stores selected pawn for a move in a turn
self.selected_card = None # stores selected card for a move in a turn
self.selected_move = None # stores selected move chosen for a turn
self.undo_turn = None # turn to undo to for pvp
# Initialise copies of the above for history keeping purposes
self.whose_turn_log = []
self.number_of_turns_log = []
self.blue_win_log = []
self.red_win_log = []
self.blue_cards_log = []
self.red_cards_log = []
self.side_card_log = []
self.board_log = []
self.piece_state_log = []
self.selected_pawn_log = []
self.selected_card_log = []
self.selected_move_log = []
# Initialise features need for minimax algo
self.depth = None # depth of tree for minimax
self.best_selected_pawn = None # stores best move from minimax
self.best_selected_card = None # stores best move from minimax
self.best_selected_move = None # stores best move from minimax
def reset(self):
""" resets the game state so a new game can be played in the same object """
self.board = [['b1','b2','B','b3','b4'],
['•','•','•','•','•'],
['•','•','•','•','•'],
['•','•','•','•','•'],
['r1','r2','R','r3','r4']]
self.piece_state = {'b1':[0,0], 'b2': [0,1],'b3':[0,3], 'b4': [0,4], 'B': [0,2], 'r1':[4,0], 'r2': [4,1],'r3':[4,3], 'r4': [4,4], 'R': [4,2]}
self.blue_pieces = ['b1', 'b2', 'b3', 'b4', 'B']
self.red_pieces = ['r1', 'r2', 'r3', 'r4', 'R']
self.blue_cards = []
self.red_cards = []
self.side_card = []
self.whose_turn = None
self.number_of_turns = 0
self.blue_win = False
self.red_win = False
self.selected_pawn = None
self.selected_card = None
self.selected_move = None
self.undo_turn = None
self.whose_turn_log = []
self.number_of_turns_log = []
self.blue_win_log = []
self.red_win_log = []
self.blue_cards_log = []
self.red_cards_log = []
self.side_card_log = []
self.board_log = []
self.piece_state_log = []
self.selected_pawn_log = []
self.selected_card_log = []
self.selected_move_log = []
self.depth = None
self.best_selected_pawn = None
self.best_selected_card = None
self.best_selected_move = None
def start_hardcoded_game(self, first_move = None):
""" Starts the game by 'shuffling' 5 HARDCODED cards into the game, which determines the starting player """
self.hardcoded_cards = {
'dragon' : {'A' : (1,2), 'B' : (1,-2), 'C' : (-1,1), 'D' : (-1,-1)},
'elephant' : {'A' : (1,1), 'B' : (1,-1), 'C' : (0,1), 'D' : (0,-1)},
'goose' : {'A' : (1,-1), 'B' : (0,-1), 'C' : (0,1), 'D' : (-1,1)},
'rooster' : {'A' : (-1,-1), 'B' : (0,-1), 'C' : (0,1), 'D' : (1,1)},
'monkey' : {'A' : (-1,-1), 'B' : (-1,1), 'C' : (1,-1), 'D' : (1,1)},
}
self.five_cards = []
# for loop randomly selects 5 distinct cards from hardcoded_cards and puts them into self.five_cards list
temp_cards = copy.deepcopy(self.hardcoded_cards)
for i in range(5):
# if a first move is specified, keep selecting a side_Card until it matches the turn of the player specified for first move
if i == 0 and first_move != None:
temp = None
while temp != first_move.lower():
potential_side_card = random.choice(list(temp_cards.keys()))
temp = self.card_colour[potential_side_card]
self.five_cards.append(potential_side_card)
else:
self.five_cards.append(random.choice(list(temp_cards.keys()))) # randomly select a card into our five cards
temp_cards.pop(self.five_cards[i]) # remove that card from temp_cards so no repetitions can occur
# assign the five cards to both players and the side
self.blue_cards.extend(self.five_cards[1:3])
self.red_cards.extend(self.five_cards[3:5])
self.side_card.append(self.five_cards[0])
# determine the starting player
if self.card_colour[self.side_card[0]] == 'blue':
self.whose_turn = 'blue'
else:
self.whose_turn = 'red'
# Save the history for TURN 0 (settings for initial starting position)
self.save_history()
# increment the number of turns to TURN 1
self.number_of_turns += 1
def start_game(self, first_move = None):
""" Starts the game by 'shuffling' 5 cards into the game, which determines the starting player """
self.five_cards = []
# for loop randomly selects 5 distinct cards from self.cards and puts them into self.five_cards list
temp_cards = copy.deepcopy(self.cards)
for i in range(5):
# if a first move is specified, keep selecting a side_Card until it matches the turn of the player specified for first move
if i == 0 and first_move != None:
temp = None
while temp != first_move.lower():
potential_side_card = random.choice(list(temp_cards.keys()))
temp = self.card_colour[potential_side_card]
self.five_cards.append(potential_side_card)
else:
self.five_cards.append(random.choice(list(temp_cards.keys()))) # randomly select a card into our five cards
temp_cards.pop(self.five_cards[i]) # remove that card from temp_cards so no repetitions can occur
# assign the five cards to both players and the side
self.blue_cards.extend(self.five_cards[1:3])
self.red_cards.extend(self.five_cards[3:5])
self.side_card.append(self.five_cards[0])
# determine the starting player
if self.card_colour[self.side_card[0]] == 'blue':
self.whose_turn = 'blue'
else:
self.whose_turn = 'red'
# Save the history for TURN 0 (settings for initial starting position)
self.save_history()
# increment the number of turns to TURN 1
self.number_of_turns += 1
@property
def show_board(self):
""" Shows board state neatly """
print("current board:")
print()
# Loop between rows
for row in self.board:
for elem in row:
if len(elem) == 1:
print(elem + " ", end = " ") # Print board state neatly for single lettered masters
else:
print(elem, end = " ") # Print board state neatly for pawns
print()
print()
@property
def show_player_moves(self):
""" Shows the available moves to for player """
# If turn is blue
if self.whose_turn == "blue":
print("player's (" + self.whose_turn + ") moves available:") # Print turn info
print()
left_card = self.blue_cards[0]
right_card = self.blue_cards[1]
print(left_card + ":" + " "*(16 - len(left_card)) + right_card + ":") # Print card name
moveset_left = [['◦' for x in range(5)] for y in range(5)] # Initialise empty array
moveset_left[2][2] = '☺' # Place reference pawn piece
for choice in self.cards[left_card]: # Loop between choices
i_change, j_change = self.cards[left_card][choice]
j_change *= -1 # Account for player's reference frame
moveset_left[2+i_change][2+j_change] = choice # Update array with move location
moveset_right = [['◦' for x in range(5)] for y in range(5)] # Initialise empty array
moveset_right[2][2] = '☺' # Place reference pawn piece
for choice in self.cards[right_card]: # Loop between choices
i_change, j_change = self.cards[right_card][choice]
j_change *= -1 # Account for player's reference frame
moveset_right[2+i_change][2+j_change] = choice # Update array with move location
for i in range(5):
for j in range(5):
print(moveset_left[i][j], end = " ")
print("\t", end = ' ')
for j in range(5):
print(moveset_right[i][j], end = " ")
print("\t")
print("\n")
else:
print("player's (" + self.whose_turn + ") moves available:") # Print turn info
print()
left_card = self.red_cards[0]
right_card = self.red_cards[1]
print(left_card + ":" + " "*(16 - len(left_card)) + right_card + ":") # Print card name
moveset_left = [['◦' for x in range(5)] for y in range(5)] # Initialise empty array
moveset_left[2][2] = '☺' # Place reference pawn piece
for choice in self.cards[left_card]: # Loop between choices
i_change, j_change = self.cards[left_card][choice]
i_change *= -1 # Account for player's reference frame
moveset_left[2+i_change][2+j_change] = choice # Update array with move location
moveset_right = [['◦' for x in range(5)] for y in range(5)] # Initialise empty array
moveset_right[2][2] = '☺' # Place reference pawn piece
for choice in self.cards[right_card]: # Loop between choices
i_change, j_change = self.cards[right_card][choice]
i_change *= -1 # Account for player's reference frame
moveset_right[2+i_change][2+j_change] = choice # Update array with move location
for i in range(5):
for j in range(5):
print(moveset_left[i][j], end = " ")
print("\t", end = " ")
for j in range(5):
print(moveset_right[i][j], end = " ")
print("\t")
print("\n")
@property
def show_opponent_moves(self):
""" Shows the available moves to for opponent """
# If turn is blue
if self.whose_turn == "blue":
print("opponent (red) moves available:") # Print opponent's info
print()
left_card = self.red_cards[0]
right_card = self.red_cards[1]
print(left_card + ":" + " "*(16 - len(left_card)) + right_card + ":") # Print card name
moveset_left = [['◦' for x in range(5)] for y in range(5)] # Initialise empty array
moveset_left[2][2] = '☺' # Place reference pawn piece
for choice in self.cards[left_card]: # Loop between choices
i_change, j_change = self.cards[left_card][choice]
i_change *= -1 # Account for player's reference frame
moveset_left[2+i_change][2+j_change] = choice # Update array with move location
moveset_right = [['◦' for x in range(5)] for y in range(5)] # Initialise empty array
moveset_right[2][2] = '☺' # Place reference pawn piece
for choice in self.cards[right_card]: # Loop between choices
i_change, j_change = self.cards[right_card][choice]
i_change *= -1 # Account for player's reference frame
moveset_right[2+i_change][2+j_change] = choice # Update array with move location
for i in range(5):
for j in range(5):
print(moveset_left[i][j], end = " ")
print("\t", end = " ")
for j in range(5):
print(moveset_right[i][j], end = " ")
print("\t")
print("\n")
else:
print("opponent (blue) moves available:") # Print opponent's info
print()
left_card = self.blue_cards[0]
right_card = self.blue_cards[1]
print(left_card + ":" + " "*(16 - len(left_card)) + right_card + ":") # Print card name
moveset_left = [['◦' for x in range(5)] for y in range(5)] # Initialise empty array
moveset_left[2][2] = '☺' # Place reference pawn piece
for choice in self.cards[left_card]: # Loop between choices
i_change, j_change = self.cards[left_card][choice]
j_change *= -1 # Account for player's reference frame
moveset_left[2+i_change][2+j_change] = choice # Update array with move location
moveset_right = [['◦' for x in range(5)] for y in range(5)] # Initialise empty array
moveset_right[2][2] = '☺' # Place reference pawn piece
for choice in self.cards[right_card]: # Loop between choices
i_change, j_change = self.cards[right_card][choice]
j_change *= -1 # Account for player's reference frame
moveset_right[2+i_change][2+j_change] = choice # Update array with move location
for i in range(5):
for j in range(5):
print(moveset_left[i][j], end = " ")
print("\t", end = ' ')
for j in range(5):
print(moveset_right[i][j], end = " ")
print("\t")
print("\n")
@property
def show_side_moves(self):
""" Shows the moves for side card """
print("side move:") # Print side move info
print()
for card in self.side_card: # Get side card
print(card + ":") # Print card name
moveset = [['◦' for x in range(5)] for y in range(5)] # Initialise empty array
moveset[2][2] = '☺' # Place reference pawn piece
for choice in self.cards[card]: # Loop between choices
i_change, j_change = self.cards[card][choice]
if self.whose_turn == 'blue':
j_change *= -1 # Account for current turn player's reference frame
else:
i_change *= -1
moveset[2+i_change][2+j_change] = choice # Update array with move location
for row in moveset:
for elem in row:
print(elem, end = " ") # Print board state neatly
print()
print()
@property
def show_game_state(self):
""" Shows current board, current player and opponent's moves and side cards moves """
print("Turn " + str(self.number_of_turns))
print()
self.show_board
self.show_player_moves
self.show_opponent_moves
self.show_side_moves
def prompt_player_move(self, stage, quick_prompt = True):
""" Prompt for decision from current turn player """
# Stages are utilised to see degree of information provided by player
# Stage 0: First time prompting
# Stage 1: Choosing pawn onwards (reselecting pawn)
# Stage 2: Choosing card onwards (reselecting card)
# Stage 3: Choosing move onwards (reselecting move)
# Stage 4: Completed selection
# quick_prompt allows the user to input the piece, card & move in one line
if quick_prompt:
quick_input = input("Select your piece, card & move: ").split(' ') # A list of 3 items
print(quick_input)
# Deploy undo check
if quick_input[0] == 'undo':
self.undo_turn = eval(input("What turn do you want to go back to: "))
if self.undo_turn > len(self.number_of_turns_log) - 1:
print("undo turn is is invalid")
return 0
else:
return "undo"
try:
# extract piece
self.selected_pawn = quick_input[0]
if '1' in self.selected_pawn or '2' in self.selected_pawn or '3' in self.selected_pawn or '4' in self.selected_pawn:
self.selected_pawn = self.selected_pawn.lower()
# extract card
self.selected_card = quick_input[1].lower()
# extract move
self.selected_move = quick_input[2].upper()
except:
print("entered invalid string")
return 0
# Deploy all 8 checks for the three inputs
if self.selected_pawn not in list(self.piece_state.keys()):
print("piece is invalid. please reselect piece.")
return 0
elif self.whose_turn == 'blue' and 'r' in self.selected_pawn.lower():
print("The blue player can only choose a blue piece. please reselect piece.")
return 0
elif self.whose_turn == 'red' and 'b' in self.selected_pawn.lower():
print("The red player can only choose a red piece. please reselect piece.")
return 0
elif self.piece_state[self.selected_pawn] == -1:
print("piece {} is dead, please reselect piece".format(self.selected_pawn))
return 0
elif (self.whose_turn == 'blue' and self.selected_card not in self.blue_cards) or (self.whose_turn == 'red' and self.selected_card not in self.red_cards):
print("card is invalid. please reselect card.")
return 0
elif self.selected_move not in list(self.cards[self.selected_card].keys()):
print("move is invalid. please reselect move.")
return 0
elif self.is_move_valid(mode = 'pvp') == False:
return 0
elif self.selected_pawn == None or self.selected_card == None or self.selected_move == None:
print("error in selection. please reselect again")
return 0
else:
return 4
# Slower line by line prompt
else:
# print current player's turn
if stage == 0:
print(self.whose_turn + "'s turn")
# prompt for pawn selection
if stage == 0 or stage == 1:
self.selected_pawn = input("Select your piece (Enter 0 to reselect, undo to Undo): ")
if '1' in self.selected_pawn or '2' in self.selected_pawn or '3' in self.selected_pawn or '4' in self.selected_pawn:
self.selected_pawn = self.selected_pawn.lower()
# undo function
if self.selected_pawn == 'undo':
self.undo_turn = eval(input("What turn do you want to go back to: "))
if self.undo_turn > len(self.number_of_turns_log) - 1:
print("undo turn is is invalid")
return 1
else:
return "undo"
# account for reselection and ensure that selected_pawn is valid
if self.selected_pawn == "0":
self.selected_pawn = None # standardise all selections back to None
self.selected_card = None
self.selected_move = None
return 0
elif self.selected_pawn not in list(self.piece_state.keys()):
print("piece is invalid. please reselect piece.")
return 1
elif self.whose_turn == 'blue' and 'r' in self.selected_pawn.lower():
print("The blue player can only choose a blue piece. please reselect piece.")
return 1
elif self.whose_turn == 'red' and 'b' in self.selected_pawn.lower():
print("The red player can only choose a red piece. please reselect piece.")
return 1
elif self.piece_state[self.selected_pawn] == -1:
print("piece {} is dead, please reselect piece".format(self.selected_pawn))
return 1
# prompt for card selection
if stage == 0 or stage == 1 or stage == 2:
self.selected_card = input("Select your card (Enter 0 to reselect, undo to Undo): ").lower()
# undo function
if self.selected_card == 'undo':
self.undo_turn = eval(input("What turn do you want to go back to: "))
if self.undo_turn > len(self.number_of_turns_log) - 1:
print("undo turn is is invalid")
return 1
else:
return "undo"
# ensure that card is valid
if self.selected_card == "0":
self.selected_pawn = None
self.selected_card = None
self.selected_move = None
return 0
elif (self.whose_turn == 'blue' and self.selected_card not in self.blue_cards) or (self.whose_turn == 'red' and self.selected_card not in self.red_cards):
print("card is invalid. please reselect card.")
return 2
# prompt for move selection
if stage == 0 or stage == 1 or stage == 2 or stage == 3:
self.selected_move = input("Select your move (Enter 0 to reselect, undo to Undo): ").upper()
# undo function
if self.selected_move == 'undo':
self.undo_turn = eval(input("What turn do you want to go back to: "))
if self.undo_turn > len(self.number_of_turns_log) - 1:
print("undo turn is is invalid")
return 1
else:
return "undo"
# ensure that move is valid
if self.selected_card == "0":
self.selected_pawn = None
self.selected_card = None
self.selected_move = None
return 0
elif self.selected_move not in list(self.cards[self.selected_card].keys()):
print("move is invalid. please reselect move.")
return 3
elif self.is_move_valid(mode = 'pvp') == False:
return 3
# raise error if there is empty selection and redirect to reselection
if self.selected_pawn == None or self.selected_card == None or self.selected_move == None:
print("error in selection. please reselect again")
return 0
else:
return 4
def is_move_valid(self, mode):
""" this function check if selected move is valid given board state """
# Note that the first move in the card is called A, followed by B, C & D for up to 4 moves
# self.selected_move (input into this function (not directly, but as a class attribute)) refers to this A, B, C or D
# determine the coordinate change of the piece and current coordinate of the piece
i_change, j_change = self.cards[self.selected_card][self.selected_move]
i_current = self.piece_state[self.selected_pawn][0]
j_current = self.piece_state[self.selected_pawn][1]
# Transpose coordinate change based on the player. This is hardcoded to the coordinate system of the board
if self.whose_turn == 'blue':
j_change *= -1
else:
i_change *= -1
# ensure that new coordinate (current + change) are valid on the board
# in other words it cannot be outside the board and it cannot be clashing with a piece from the SAME team
i_new = i_current + i_change
j_new = j_current + j_change
if i_new < 0 or i_new > 4 or j_new < 0 or j_new > 4: # outside the board
if mode == 'pvp':
print("move is invalid as the piece falls outside the board. please reselect move")
return False
if self.whose_turn == 'blue':
if 'b' in self.board[i_new][j_new].lower():
if mode == 'pvp':
print("a blue piece cannot take the place of another blue piece. please reselect move")
return False
else:
if 'r' in self.board[i_new][j_new].lower():
if mode == 'pvp':
print("a red piece cannot take the place of another red piece. please reselect move")
return False
def are_ai_selections_valid(self): # a copy of the prompt_player_move method for AI
""" check the validity of the moves selected by the ai """
# check for validity of pawn
if self.selected_pawn not in list(self.piece_state.keys()):
return False
elif self.piece_state[self.selected_pawn] == -1:
return False
elif self.whose_turn == 'blue' and 'r' in self.selected_pawn.lower():
return False
elif self.whose_turn == 'red' and 'b' in self.selected_pawn.lower():
return False
# check for validity of card
if (self.whose_turn == 'blue' and self.selected_card not in self.blue_cards) or (self.whose_turn == 'red' and self.selected_card not in self.red_cards):
return False
# check for validity of move
if self.selected_move not in list(self.cards[self.selected_card].keys()):
return False
elif self.is_move_valid(mode = 'ai') == False:
return False
# raise error if there is empty selection and redirect to reselection
if self.selected_pawn == None or self.selected_card == None or self.selected_move == None:
return False
else:
return True
def move(self):
""" This method implements a move and updates the game state accordingly """
# Note that the first move in the card is called A, followed by B, C & D for up to 4 moves
# self.selected_move (input into this function) refers to this A, B, C or D
# determine the coordinate change of the piece and current coordinate of the piece
i_change, j_change = self.cards[self.selected_card][self.selected_move]
i_current = self.piece_state[self.selected_pawn][0]
j_current = self.piece_state[self.selected_pawn][1]
# Transpose coordinate change based on the player. This is hardcoded to the coordinate system of the board
if self.whose_turn == 'blue':
j_change *= -1
else:
i_change *= -1
# ensure that new coordinate (current + change) are valid on the board
# in other words it cannot be outside the board and it cannot be clashing with a piece from the SAME team
i_new = i_current + i_change
j_new = j_current + j_change
# first we identify if the move kills a piece from the opposing team
if self.whose_turn == 'blue':
if 'r' in self.board[i_new][j_new].lower():
dead_piece = self.board[i_new][j_new]
self.piece_state[dead_piece] = -1 # mark red piece as dead
else:
if 'b' in self.board[i_new][j_new].lower():
dead_piece = self.board[i_new][j_new]
self.piece_state[dead_piece] = -1 # mark blue piece as dead
# now we update the piece state of the piece into its new position
self.piece_state[self.selected_pawn] = [i_new, j_new]
# afterwards, we update the board
self.board[i_current][j_current] = '•'
self.board[i_new][j_new] = self.selected_pawn
return True
def turn_pvp(self):
""" This method implements the process of one turn of gameplay for pvp"""
# show game_state
self.show_game_state
# prompt player to make decision
stage = 0
while stage != 4:
stage = self.prompt_player_move(stage)
if stage == "undo":
self.load_history(self.undo_turn) # load to desired turn
return
# move the pieces and update the board
self.move()
# update the blue_win or red_win booleans if win condition is met
self.check_win()
# now update the position of the cards, the player of this turn will get the side card and discard used card to the side
# update based on the order specified in card_colours
if self.whose_turn == 'blue':
self.blue_cards.remove(self.selected_card)
if (list(self.card_colour.keys()).index(self.blue_cards[0]) < list(self.card_colour.keys()).index(self.side_card[0])):
self.blue_cards.append(self.side_card[0])
else:
self.blue_cards.insert(0, self.side_card[0])
self.side_card = [self.selected_card]
else:
self.red_cards.remove(self.selected_card)
if (list(self.card_colour.keys()).index(self.red_cards[0]) < list(self.card_colour.keys()).index(self.side_card[0])):
self.red_cards.append(self.side_card[0])
else:
self.red_cards.insert(0, self.side_card[0])
self.side_card = [self.selected_card]
# finally, change turns
if self.whose_turn == 'blue':
self.whose_turn = 'red'
else:
self.whose_turn = 'blue'
# save current game state
self.save_history()
# increment the number of turns
self.number_of_turns += 1
def turn_ai(self):
""" This method implements the process of one turn of gameplay for ai (presumes moves have already been chosen prior and are valid)"""
# move the pieces and update the board
self.move()
# update the blue_win or red_win booleans if win condition is met
self.check_win()
# now update the position of the cards, the player of this turn will get the side card and discard used card to the side
# update based on the order specified in card_colours
if self.whose_turn == 'blue':
self.blue_cards.remove(self.selected_card)
if (list(self.card_colour.keys()).index(self.blue_cards[0]) < list(self.card_colour.keys()).index(self.side_card[0])):
self.blue_cards.append(self.side_card[0])
else:
self.blue_cards.insert(0, self.side_card[0])
self.side_card = [self.selected_card]
else:
self.red_cards.remove(self.selected_card)
if (list(self.card_colour.keys()).index(self.red_cards[0]) < list(self.card_colour.keys()).index(self.side_card[0])):
self.red_cards.append(self.side_card[0])
else:
self.red_cards.insert(0, self.side_card[0])
self.side_card = [self.selected_card]
# finally, change turns
if self.whose_turn == 'blue':
self.whose_turn = 'red'
else:
self.whose_turn = 'blue'
# save current game state
self.save_history()
# increment the number of turns
self.number_of_turns += 1
return True
def turn_minimax(self, minimax_depth = None, parallel = None, prune = True, return_move_index = True):
""" This method implements the process of one turn of gameplay for minimax algo"""
self.prune = prune
# manual setting of depth in minimax tree
if minimax_depth != None:
self.depth = minimax_depth
# show game_state
if self.verbose:
self.show_game_state
# call minimax algo and choose selected move given current state
if parallel == "Process":
value = self.multi_minimax_Process(depth = 0, alpha = -math.inf, beta = math.inf)
if parallel == "Pool":
value = self.multi_minimax_Pool(depth = 0, alpha = -math.inf, beta = math.inf)
else:
value = self.minimax(depth = 0, alpha = -math.inf, beta = math.inf, return_move_index = return_move_index)
if self.verbose:
print("Turn {}: ".format(self.number_of_turns),self.best_selected_pawn, self.best_selected_card, self.best_selected_move)
self.selected_pawn = self.best_selected_pawn
self.selected_card = self.best_selected_card
self.selected_move = self.best_selected_move
# move the pieces and update the board
self.move()
# update the blue_win or red_win booleans if win condition is met
self.check_win()
# now update the position of the cards, the player of this turn will get the side card and discard used card to the side
# update based on the order specified in card_colours
if self.whose_turn == 'blue':
self.blue_cards.remove(self.selected_card)
if (list(self.card_colour.keys()).index(self.blue_cards[0]) < list(self.card_colour.keys()).index(self.side_card[0])):
self.blue_cards.append(self.side_card[0])
else:
self.blue_cards.insert(0, self.side_card[0])
self.side_card = [self.selected_card]
else:
self.red_cards.remove(self.selected_card)
if (list(self.card_colour.keys()).index(self.red_cards[0]) < list(self.card_colour.keys()).index(self.side_card[0])):
self.red_cards.append(self.side_card[0])
else:
self.red_cards.insert(0, self.side_card[0])
self.side_card = [self.selected_card]
# finally, change turns
if self.whose_turn == 'blue':
self.whose_turn = 'red'
else:
self.whose_turn = 'blue'
# save history
self.save_history()
# increment the number of turns
self.number_of_turns += 1
def turn_mcts(self, mcts_object):
""" This method implements the process of one turn of gameplay for monte-carlo search tree algo"""
# show game_state
if self.verbose:
self.show_game_state
# execute MCTS and obtain tuple of actions (piece, card, move)
action_tuple = mcts_object.search(initialState = self)
if self.verbose:
print("Turn {}: ".format(self.number_of_turns),action_tuple[0], action_tuple[1], action_tuple[2])
self.selected_pawn = action_tuple[0]
self.selected_card = action_tuple[1]
self.selected_move = action_tuple[2]
# move the pieces and update the board
self.move()
# update the blue_win or red_win booleans if win condition is met
self.check_win()
# now update the position of the cards, the player of this turn will get the side card and discard used card to the side
# update based on the order specified in card_colours
if self.whose_turn == 'blue':
self.blue_cards.remove(self.selected_card)
if (list(self.card_colour.keys()).index(self.blue_cards[0]) < list(self.card_colour.keys()).index(self.side_card[0])):
self.blue_cards.append(self.side_card[0])
else:
self.blue_cards.insert(0, self.side_card[0])
self.side_card = [self.selected_card]
else:
self.red_cards.remove(self.selected_card)
if (list(self.card_colour.keys()).index(self.red_cards[0]) < list(self.card_colour.keys()).index(self.side_card[0])):
self.red_cards.append(self.side_card[0])
else:
self.red_cards.insert(0, self.side_card[0])
self.side_card = [self.selected_card]
# finally, change turns
if self.whose_turn == 'blue':
self.whose_turn = 'red'
else:
self.whose_turn = 'blue'
# need to save history first
self.save_history()
# increment the number of turns
self.number_of_turns += 1
def check_win(self):
""" This method checks if any player has won and updates self.blue_win or self.red_win if necessary """
# check if blue wins (either R is no longer on the board or there is a blue master at (4,2))
if self.piece_state['R'] == -1 or 'B' in self.board[4][2]:
self.blue_win = True
# check if red wins (either B is no longer on the board or there is a red master at (0,2))
if self.piece_state['B'] == -1 or 'R' in self.board[0][2]:
self.red_win = True
# ensure both teams cannot win at the same time (if so, there is an error in the onitama class source code)
if self.blue_win and self.red_win:
raise ValueError("both blue and red have won, check the source code for error(s)")
def save_history(self):
""" this function saves every game variable for every turn """
# if past entry for turn exists in same index, replace it
if self.number_of_turns == len(self.number_of_turns_log) - 1:
self.whose_turn_log[self.number_of_turns] = self.whose_turn
self.number_of_turns_log[self.number_of_turns] = self.number_of_turns
self.blue_win_log[self.number_of_turns] = self.blue_win
self.red_win_log[self.number_of_turns] = self.red_win
self.blue_cards_log[self.number_of_turns] = self.blue_cards[:]
self.red_cards_log[self.number_of_turns] = self.red_cards[:]
self.side_card_log[self.number_of_turns] = self.side_card[:]
self.board_log[self.number_of_turns] = [x[:] for x in self.board]
self.piece_state_log[self.number_of_turns] = self.piece_state.copy()
self.selected_pawn_log[self.number_of_turns] = self.selected_pawn
self.selected_card_log[self.number_of_turns] = self.selected_card
self.selected_move_log[self.number_of_turns] = self.selected_move
# else append new entry for new turn
else:
self.whose_turn_log.append(self.whose_turn)
self.number_of_turns_log.append(self.number_of_turns)
self.blue_win_log.append(self.blue_win)
self.red_win_log.append(self.red_win)
self.blue_cards_log.append(self.blue_cards[:])
self.red_cards_log.append(self.red_cards[:])
self.side_card_log.append(self.side_card[:])
self.board_log.append([x[:] for x in self.board])
self.piece_state_log.append(self.piece_state.copy())
self.selected_pawn_log.append(self.selected_pawn)
self.selected_card_log.append(self.selected_card)
self.selected_move_log.append(self.selected_move)
def load_history(self, turn_num, remove_obsolete_history = True):
""" this functions reinstates all the game variables for a specified turn BEFORE MOVE ON TURN IS MADE """
self.whose_turn = self.whose_turn_log[turn_num - 1]
self.number_of_turns = self.number_of_turns_log[turn_num - 1]
self.blue_win = self.blue_win_log[turn_num - 1]
self.red_win = self.red_win_log[turn_num - 1]
self.blue_cards = self.blue_cards_log[turn_num - 1][:]
self.red_cards = self.red_cards_log[turn_num - 1][:]
self.side_card = self.side_card_log[turn_num - 1][:]
self.board = [x[:] for x in self.board_log[turn_num - 1]]
self.piece_state = self.piece_state_log[turn_num - 1].copy()
self.selected_pawn = self.selected_pawn_log[turn_num - 1]
self.selected_card = self.selected_card_log[turn_num - 1]
self.selected_move = self.selected_move_log[turn_num - 1]
# set number_of_turns accordingly
self.number_of_turns = turn_num
if remove_obsolete_history:
# remove obsolete history
self.remove_obsolete_history()
def remove_obsolete_history(self):
""" this function removes redundant entries in log """
self.whose_turn_log = self.whose_turn_log[:self.number_of_turns]
self.number_of_turns_log = self.number_of_turns_log[:self.number_of_turns]
self.blue_win_log = self.blue_win_log[:self.number_of_turns]
self.red_win_log = self.red_win_log[:self.number_of_turns]
self.blue_cards_log= self.blue_cards_log[:self.number_of_turns]
self.red_cards_log = self.red_cards_log[:self.number_of_turns]
self.side_card_log = self.side_card_log[:self.number_of_turns]
self.board_log = self.board_log[:self.number_of_turns]
self.piece_state_log = self.piece_state_log[:self.number_of_turns]
self.selected_pawn_log = self.selected_pawn_log[:self.number_of_turns]
self.selected_card_log = self.selected_card_log[:self.number_of_turns]
self.selected_move_log = self.selected_move_log[:self.number_of_turns]
def eval_board_state(self, mode = None):
""" This function evaluates the board state based on OUR preconceived notion of value """
# simple reward for winning or losing
if mode == "simple_reward":
if self.red_win == True:
return 1
elif self.blue_win == True:
return -1
else:
return 0
# initialise score