-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathcurling2.py
726 lines (637 loc) · 25.7 KB
/
curling2.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
import pickle
import random
# import copy
import time
PRINT = True
class Card:
def __init__(self, name, suit, player=None):
self.name = str(name)
if self.name == '10':
self.name = '0' # For spacing
if name in ['*', 'Jkr', ' ']:
self.value = 0
elif name in ['J', 'Q', 'K']:
self.value = 10
elif name == 'A':
self.value = 1
else:
self.value = int(name)
self.suit = suit
self.played = False
self.discarded = False
self.player = player
def __repr__(self):
return '{} {}'.format(self.name, self.suit)
class Joker(Card):
def __init__(self):
super().__init__('Jkr', '')
self.played = True
self.discarded = False
def __repr__(self):
return self.name
class BlankCard(Card):
def __init__(self, pos):
super().__init__(' ', ' ')
self.played = True
self.discarded = False
self.pos = pos
def __bool__(self):
return False
class Board:
def __init__(self, size=5, empty='Default'):
self._final = 0
self.size = size
self.joker = Joker()
self._cards = []
for _ in range(size):
self._cards.append([Card('*', '*')] * size)
self.joker_pos = size // 2
self._cards[self.joker_pos][self.joker_pos] = self.joker
if empty == 'Default':
empty = [(0, 0), (0, 1), (0, 3), (0, 4), (1, 0), (1, 4), (3, 0), (3, 4), (4, 0), (4, 1), (4, 3), (4, 4)]
self.blanks = []
for x, y in empty:
blank = BlankCard((x + 1, y + 1))
self._cards[x][y] = blank
self.blanks.append(blank)
self.scoring_pos = [(1, [(self.joker_pos - 1, self.joker_pos - 1), (self.joker_pos - 1, self.joker_pos + 1),
(self.joker_pos + 1, self.joker_pos - 1), (self.joker_pos + 1, self.joker_pos + 1)]),
(2, [(self.joker_pos - 1, self.joker_pos), (self.joker_pos, self.joker_pos - 1),
(self.joker_pos + 1, self.joker_pos), (self.joker_pos, self.joker_pos + 1)])]
@property
def cards(self):
return [r[:] for r in self._cards]
@cards.setter
def cards(self, cards):
self._cards = [r[:] for r in cards]
def finalise(self):
self._final = 1
def unfinalise(self):
self._final = 0
def get_empty(self):
if any(not blank.discarded for blank in self.blanks):
return [blank.pos for blank in self.blanks if not blank.discarded]
else:
return []
def score(self, player):
out = 0
if not self._final:
for s, l in self.scoring_pos:
for x, y in l:
if self._cards[x][y].player == player:
out += s * self._cards[x][y].value
else:
raise Exception('Trying to score points on a finalised board for {}'.format(player))
return out
def update(self, ply, test=False):
"""Starting from the outside left as column 0, top as row 0, place your card outside the space you want to
insert it, e.g. 0, 2 to insert from the left into the second row"""
card = ply.card
error = ''
empty = self.get_empty()
if empty:
if (ply.row, ply.column) not in empty:
if PRINT:
print('Please choose from empty cells', empty)
discarded = ''
error = 'Please choose from empty cells {}'.format(empty)
return discarded, error
else:
discarded, self._cards[ply.row - 1][ply.column - 1] = self._cards[ply.row - 1][ply.column - 1], card
else:
# Check insertion condition, determine if a row or column is being inserted to
if 0 < ply.row <= self.size and ply.column in (0, self.size + 1):
tempcards = self._cards
ins_row = True # row or column?
ins_left = (ply.column == 0) # left(top) or right(bottom)? (reverse if undoing)
ins_pos = ply.row # row(col) index
elif 0 < ply.column <= self.size and ply.row in (0, self.size + 1):
# treat row and column insertion the same by transposing the card list for one of them
tempcards = list(map(list, zip(*self._cards)))
# column insertion
ins_row = False
ins_left = (ply.row == 0)
ins_pos = ply.column
else:
discarded = ''
if PRINT:
print('Invalid row/column')
error = 'Invalid row/column'
return discarded, error
# treat left and right insertion the same by reversing in one case
if ins_left:
card_row = tempcards[ins_pos - 1]
else:
card_row = tempcards[ins_pos - 1][::-1]
discarded = card_row[-1]
new_row = [card] + card_row[:-1]
# put the joker back in the right place if we're in that row/col
if ins_pos == self.joker_pos + 1:
new_row.insert(self.joker_pos, new_row.pop(self.joker_pos + 1))
# undo the row reverse and transpose
if not ins_left:
new_row.reverse()
if ins_row:
self._cards[ins_pos - 1] = new_row
else:
tempcards[ins_pos - 1] = new_row
self._cards = [x for x in map(list, zip(*tempcards))]
discarded.discarded = 1 # Set card attribute
if not test and isinstance(discarded, BlankCard):
self.blanks.remove(discarded)
return discarded, error
# def is_setup_phase(self):
# return (len(self.get_empty()) > 0)
def __repr__(self):
out = []
for row in self._cards:
out.append(' | '.join(str(card) for card in row))
return '\n'.join(out)
# return '\n'.join(' '.join(str(card) for card in rows) for rows in self._cards) + '\n'
# The information a player gives to the game to make a ply (move)
class Ply:
def __init__(self, card, row, column):
self.card = card
if self.card == '10':
self.card = '0'
self.row = row
self.column = column
def __repr__(self):
return str(self.card) + " at " + str(self.row) + ", " + str(self.column)
class Player:
def __init__(self, name, suit, card_options=1):
self.score = 0
self.name = name
self.suit = suit
# noinspection PyTypeChecker
l = ['K', 'Q', 'J', 'A'] + list(range(2, 11))
self.hand = sorted((Card(i, suit, self) for i in l), key=lambda x: -x.value)
self.AI = False
self.card_options = card_options
def in_hand(self, card):
"""Tests if a card (by instance or name) is in player's hand and returns instance or False"""
for c in self.hand:
if c.name == card or c == card:
return c
return False
# return any(c.name == card for c in self.hand) or card in self.hand
def play(self, card):
"""Tells a player to remove a card from their hand"""
if not self.in_hand(card):
raise Exception('Card {} not in hand'.format(card))
for i, c in enumerate(self.hand):
if c.name == card or c == card: # Either by name or card instance
c.played = True
del self.hand[i]
return True
raise Exception('Card not removed')
# def unplay(self, card):
# if self.in_hand(card):
# raise Exception('Card {} already in hand during tree backtrack'.format(card))
# if card != '':
# card.played = False
# self.hand.append(card)
# return True
def alter_score(self, delta):
if delta > 500:
raise Exception('Trying to add score above 500')
self.score += delta
return self.score
@staticmethod
def enum_plies(game, p_turn):
"""Given a game generates all potential moves for player indexed by p_turn"""
# assuming we have cards left to play
player = game.players[p_turn]
# card_options = sorted(player.hand, key=lambda x: -x.value) Player's hand is now sorted
empty = game.board.get_empty()
if empty:
rowcol_options = empty
else:
bsize = game.board.size
rowcol_options = [(0, i) for i in range(1, bsize + 1)] + \
[(bsize + 1, i) for i in range(1, bsize + 1)] + \
[(i, 0) for i in range(1, bsize + 1)] + \
[(i, bsize + 1) for i in range(1, bsize + 1)]
# choose either the highest or lowest value card
if len(player.hand) > 1 and player.card_options > 1:
if player.card_options == 2:
card_choices = [player.hand[0], player.hand[-1]]
else:
raise NotImplementedError
else:
# Pick maximum
card_choices = [player.hand[0]]
for card in card_choices:
for rowcol in rowcol_options:
yield Ply(card, rowcol[0], rowcol[1])
# To be implemented by inheriting classes
def make_move(self, game_state):
raise NotImplementedError
def __repr__(self):
if self.AI:
return 'AI - {} ({})'.format(self.name, self.suit)
else:
return '{} ({})'.format(self.name, self.suit)
class HumanPlayer(Player):
def make_move(self, game_state):
while 1:
while 1:
card = input('Pick a card:')
if card == '10':
card = '0'
card = self.in_hand(card)
if self.in_hand(card):
break
while 1:
row = input('Pick row:')
column = input('Pick column:')
try:
row = int(row)
column = int(column)
except ValueError:
# print('Please use integers')
continue
else:
break
# noinspection PyUnboundLocalVariable
return Ply(card, row, column)
class AIPlayer(Player):
def __init__(self, name, suit):
super().__init__(name, suit)
self.AI = True
def make_move(self, game_state):
"""Selects max card and random valid row/column"""
if self.hand:
card = max(self.hand, key=lambda x: x.value)
else:
raise Exception("Empty Hand")
# print(card)
empty = game_state.board.get_empty()
# print(empty)
if empty:
row, column = random.choice(empty)
else:
insert = random.choice(('R', 'C'))
if insert == "R":
row = random.choice((0, game_state.board.size + 1))
column = random.randint(1, 5)
else:
column = random.choice((0, game_state.board.size + 1))
row = random.randint(1, 5)
return Ply(card, row, column)
class AITreeSearch(Player):
def __init__(self, name, suit, depth=2, card_options=1):
super().__init__(name, suit, card_options)
self.AI = True
self.depth = depth # tree search depth (plies)
self.t_game = [] # to hold the local version of the game
def make_move(self, game_state):
"""Runs a tree search to find out best move"""
# entry point
global PRINT
PRINT = False
print("Enter AITree make_move")
# create local version of the game without letting it enter its game loop
self.t_game = Game(game_state, autostart=False) # copy.deepcopy(Game(game_state, autostart = False))
# do a tree search recursively to find the best ply and its expected scores
alter_scores = {player: 0 for player in self.t_game.players}
bestscores, bestply = self.tree_search(self.t_game, self.depth, self.t_game.p_turn, alter_scores)
# point the resulting card object to the actual card in the real game
for card in self.hand:
if card.name == bestply.card.name:
bestply.card = card
print("Exit AITree make_move, best scores: ", bestscores)
PRINT = True
return bestply
# recursive search of future moves to the given depth
def tree_search(self, game, depth, p_turn, alter_scores):
# p_turn = game.p_turn
player = game.players[p_turn]
plies = player.enum_plies(game, p_turn)
best = ''
# Store a copy of board cards to put back later
stored_cards = game.board.cards
for ply in plies:
new_alter_scores, new_p_turn, gameover, discard = game.test_move(ply, p_turn, alter_scores.copy())
if gameover or depth == 0:
node_values = self.heuristic_eval(game, new_alter_scores, new_p_turn, gameover)
else:
node_values = self.tree_search(game, depth - 1, new_p_turn, new_alter_scores)[0]
# Undo move by
ply.card.played = False
discard.discarded = False
# Restore the board's cards
game.board.cards = stored_cards
if best == '' or node_values[player] > best[player]:
best = node_values
bestplies = [ply]
elif node_values[player] == best[player]:
# noinspection PyUnboundLocalVariable
bestplies.append(ply)
bestply = random.choice(bestplies)
return best, bestply
# returns the value of the current game for each player in a three-item list
# trying to take into account immediate future moves without doing a tree search
# (so that this evaluation doesn't favour the player who just played)
# noinspection PyProtectedMember
@staticmethod
def heuristic_eval(game, alter_scores, p_turn, gameover):
if not gameover:
num_players = len(game.players)
values = {p: 0 for p in game.players}
# Catch non player cards
values[None] = 0
# add some value for cards not currently in scoring positions
central = (game.board.size // 2 - 1, game.board.size // 2, game.board.size // 2 + 1)
# TODO: Work out scoring for other sized boards - where are scoring pos etc.
# Only worry about outer rows here, inner is counted by score
rows = [0, game.board.size - 1]
columns = [0, game.board.size - 1]
for r in rows:
for c in columns:
card = game.board._cards[r][c]
values[card.player] += 0.2 * card.value
for c in central:
card = game.board._cards[r][c]
values[card.player] += 0.5 * card.value
for c in columns:
for r in central:
card = game.board._cards[r][c]
values[card.player] += 0.5 * card.value
del values[None]
# for row, x in enumerate(game.board._cards):
# for column, card in enumerate(x):
# if row in central or column in central:
# values[card.player] += 0.5 * card.value
# else:
# values[card.player] += 0.2 * card.value
for i, player in enumerate(game.players):
waittime = (i - p_turn) % num_players # plies until your next ply
score = player.score + alter_scores[player]
boardscore = game.board.score(player) * (1 + num_players - waittime) # how good the board is
#hand_potential = sum(c.value for c in player.hand if not c.played)
values[player] += score + boardscore #+ 0.4 * hand_potential
s = sum(values.values())
for k, v in values.items():
values[k] = 2 * v - s # I.e. subtract others
else:
# TODO: Should we differentiate between winning states? E.g probability of winning, margin of winning?
# losers have a large negative score
values = {p: -500 for p in game.players}
maxscore = 0
winners = []
for player in game.players:
if player.score + alter_scores[player] > maxscore:
maxscore = player.score + alter_scores[player]
winners = [player]
elif player.score + alter_scores[player] == maxscore:
winners.append(player)
# winner has a large positive score
winning_bonus = 10000 / len(winners)
for player in winners:
values[player] += winning_bonus
return values
# the information which players are sent to make their move
class GameState:
def __init__(self, board, players, plyhistory, p_turn, gameover):
self.board = board
self.players = players
self.plyhistory = plyhistory
self.p_turn = p_turn
self.next_player = self.players[self.p_turn]
self.gameover = gameover
def statement(self):
if not self.gameover:
return "{}'s turn\nThey scored {} points\nThey have in their hand:\n{}".format(self.next_player,
self.board.score(
self.next_player),
str([c.name for c in
self.next_player.hand]))
else:
winners = []
maxscore = 0
for player in self.players:
if player.score > maxscore:
maxscore = player.score
winners = [player]
elif player.score == maxscore:
winners.append(player)
return "Final score:\n" + '\n'.join('{}: {}'.format(player, player.score) for player in self.players) + \
"\n\n{} Wins!".format(' '.join(str(p) for p in winners))
def __repr__(self):
return str(self.board) + "\n\n" + self.statement()
class StartGameState(GameState):
def __init__(self, board=None, players=None):
if board is None:
board = Board(empty="Default")
if players is None:
players = [HumanPlayer('Matt', chr(9829)),
HumanPlayer('F. Rob', chr(9830)),
HumanPlayer('Rob H.', chr(9827))]
super().__init__(board, players, [], 0, False)
class Game:
def __init__(self, game_state, fname='', save=1, load=1, autostart=True):
if fname != '':
self.fname = fname
self.save = save
if load:
try:
game_state = self.load()
except FileNotFoundError:
print('No file {} found. Using input GameState'.format(fname))
else:
self.save = 0
self.fname = 'err.pi'
self.board = game_state.board
self.players = game_state.players
self.plyhistory = game_state.plyhistory
self.p_turn = game_state.p_turn
self.gameover = game_state.gameover
self.plyhistory = []
if self.save:
self.dump()
if autostart:
self.gameloop()
def gameloop(self):
while not self.gameover:
if PRINT:
print()
print('\n'.join('{}: {}'.format(player, player.score) for player in self.players))
print(self.get_game_state())
self.turn()
def turn(self):
self.gameover = False
player = self.players[self.p_turn]
while True:
ply = player.make_move(self.get_game_state())
card = player.in_hand(ply.card)
if not card:
return 'Please pick card again'
else:
break
if PRINT:
# noinspection PyUnboundLocalVariable
print(ply)
error = self.make_move(ply)
return error
def online_turn(self, card, row, column):
t = time.time()
self.gameover = False
player = self.players[self.p_turn]
if not player.AI:
card = player.in_hand(card)
if not card:
return 'Please pick card again'
ply = Ply(card, row, column)
error = self.make_move(ply)
else:
# Catch and proceed when starting from AI player.
error = "Done"
# Loop through AI turns
while error == "Done" and self.players[self.p_turn].AI and not self.gameover:
player = self.players[self.p_turn]
print(player, 'turn')
ply = player.make_move(self.get_game_state())
error = self.make_move(ply)
# Timeout
if time.time() - t > 5:
break
return error
def make_move(self, ply):
player = self.players[self.p_turn]
discard, error = self.board.update(ply)
self.plyhistory.append((ply, discard))
if error:
if player.AI:
raise Exception("AI error: {} trying {} in\n{}".format(error, ply, self.board))
else:
return error
else:
player.play(ply.card)
self.p_turn = (self.p_turn + 1) % len(self.players)
next_player = self.players[self.p_turn]
if not next_player.hand:
self.final()
else:
next_player.alter_score(self.board.score(next_player))
if self.save:
self.dump()
return 'Done'
def test_move(self, ply, p_turn, alter_scores):
player = self.players[p_turn]
discard, error = self.board.update(ply, test=1)
# self.plyhistory.append((ply, discard))
if error:
if player.AI:
raise Exception("AI error: {} trying {} in\n{}".format(error, ply, self.board))
else:
return error
else:
ply.card.played = True
p_turn = (p_turn + 1) % len(self.players)
next_player = self.players[p_turn]
if not next_player.hand or not any(not c.played for c in next_player.hand):
for player in self.players:
alter_scores[player] += self.board.score(player)
gameover = 1
else:
alter_scores[next_player] += self.board.score(next_player)
gameover = 0
return alter_scores, p_turn, gameover, discard
# def unmake_move(self):
# player = self.players[self.p_turn]
# (unply, undiscard) = self.plyhistory.pop()
# if self.gameover:
# self.unfinal()
# else:
# player.alter_score(-self.board.score(player))
#
# self.p_turn = (self.p_turn - 1) % len(self.players)
# lastplayer = self.players[self.p_turn]
#
# lastplayer.unplay(unply.card)
# undiscard, error = self.board.update(unply, True, undiscard)
# if error:
# raise Exception("Unmake move error")
# if self.save:
# self.dump()
# return unply
def final(self):
self.gameover = True
if PRINT:
print('\n'.join('{}: {}'.format(player, player.score) for player in self.players))
for player in self.players:
score = self.board.score(player)
player.alter_score(score)
self.board.finalise()
game_state = self.get_game_state()
if PRINT:
print("\nFinal board:")
print(game_state)
if self.save:
self.dump()
return game_state.statement()
def unfinal(self):
self.gameover = False
self.board.unfinalise()
for player in self.players:
score = self.board.score(player)
player.alter_score(-score)
if self.save:
self.dump()
return True
def get_game_state(self):
return GameState(self.board, self.players, self.plyhistory, self.p_turn, self.gameover)
def dump(self):
with open(self.fname, 'wb') as f:
pickle.dump(self.get_game_state(), f)
def load(self):
with open(self.fname, 'rb') as f:
return pickle.load(f)
#
# def AI_on_off(player_n, ai_on, self.fname='curling.pi'):
# board, players, discarded, p_turn, statement = load(self.fname)
# players[player_n].AI = ai_on
# dump(board, players, discarded, p_turn, statement, self.fname)
#
#
def main(fname='curling.pi'):
t = time.time()
board = Board(empty="Default")
players = [AITreeSearch('Matt', chr(9829), 2),
AITreeSearch('F. Rob', chr(9830), 2),
AITreeSearch('Rob H.', chr(9827), 2)]
game_state = StartGameState(board, players)
Game(game_state, fname=fname, save=0, load=0)
print("\nTime is:", time.time() - t)
def averages(runs):
global PRINT
PRINT = False
results = []
for _ in range(runs):
results.append(main())
end = [0, 0, 0]
diff = [0, 0, 0]
av_m = []
for a, b, c in results:
m = max(a, b, c)
if a == m:
end[0] += 1
if b == m:
end[1] += 1
if c == m:
end[2] += 1
diff[0] += a - b
diff[1] += b - c
diff[2] += c - a
av_m.append(m)
print(end)
for i in range(3):
diff[i] /= runs
for i in range(len(results[0])):
print(sum(x[i] for x in results) / runs)
print(diff)
print(sum(av_m) / runs)
if __name__ == '__main__':
PRINT = True
main()