Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
aimacode
GitHub Repository: aimacode/aima-python
Path: blob/master/games.py
615 views
1
"""Games or Adversarial Search (Chapter 5)"""
2
3
import copy
4
import itertools
5
import random
6
from collections import namedtuple
7
8
import numpy as np
9
10
from utils import vector_add
11
12
GameState = namedtuple('GameState', 'to_move, utility, board, moves')
13
StochasticGameState = namedtuple('StochasticGameState', 'to_move, utility, board, moves, chance')
14
15
16
# ______________________________________________________________________________
17
# MinMax Search
18
19
20
def minmax_decision(state, game):
21
"""Given a state in a game, calculate the best move by searching
22
forward all the way to the terminal states. [Figure 5.3]"""
23
24
player = game.to_move(state)
25
26
def max_value(state):
27
if game.terminal_test(state):
28
return game.utility(state, player)
29
v = -np.inf
30
for a in game.actions(state):
31
v = max(v, min_value(game.result(state, a)))
32
return v
33
34
def min_value(state):
35
if game.terminal_test(state):
36
return game.utility(state, player)
37
v = np.inf
38
for a in game.actions(state):
39
v = min(v, max_value(game.result(state, a)))
40
return v
41
42
# Body of minmax_decision:
43
return max(game.actions(state), key=lambda a: min_value(game.result(state, a)))
44
45
46
# ______________________________________________________________________________
47
48
49
def expect_minmax(state, game):
50
"""
51
[Figure 5.11]
52
Return the best move for a player after dice are thrown. The game tree
53
includes chance nodes along with min and max nodes.
54
"""
55
player = game.to_move(state)
56
57
def max_value(state):
58
v = -np.inf
59
for a in game.actions(state):
60
v = max(v, chance_node(state, a))
61
return v
62
63
def min_value(state):
64
v = np.inf
65
for a in game.actions(state):
66
v = min(v, chance_node(state, a))
67
return v
68
69
def chance_node(state, action):
70
res_state = game.result(state, action)
71
if game.terminal_test(res_state):
72
return game.utility(res_state, player)
73
sum_chances = 0
74
num_chances = len(game.chances(res_state))
75
for chance in game.chances(res_state):
76
res_state = game.outcome(res_state, chance)
77
util = 0
78
if res_state.to_move == player:
79
util = max_value(res_state)
80
else:
81
util = min_value(res_state)
82
sum_chances += util * game.probability(chance)
83
return sum_chances / num_chances
84
85
# Body of expect_minmax:
86
return max(game.actions(state), key=lambda a: chance_node(state, a), default=None)
87
88
89
def alpha_beta_search(state, game):
90
"""Search game to determine best action; use alpha-beta pruning.
91
As in [Figure 5.7], this version searches all the way to the leaves."""
92
93
player = game.to_move(state)
94
95
# Functions used by alpha_beta
96
def max_value(state, alpha, beta):
97
if game.terminal_test(state):
98
return game.utility(state, player)
99
v = -np.inf
100
for a in game.actions(state):
101
v = max(v, min_value(game.result(state, a), alpha, beta))
102
if v >= beta:
103
return v
104
alpha = max(alpha, v)
105
return v
106
107
def min_value(state, alpha, beta):
108
if game.terminal_test(state):
109
return game.utility(state, player)
110
v = np.inf
111
for a in game.actions(state):
112
v = min(v, max_value(game.result(state, a), alpha, beta))
113
if v <= alpha:
114
return v
115
beta = min(beta, v)
116
return v
117
118
# Body of alpha_beta_search:
119
best_score = -np.inf
120
beta = np.inf
121
best_action = None
122
for a in game.actions(state):
123
v = min_value(game.result(state, a), best_score, beta)
124
if v > best_score:
125
best_score = v
126
best_action = a
127
return best_action
128
129
130
def alpha_beta_cutoff_search(state, game, d=4, cutoff_test=None, eval_fn=None):
131
"""Search game to determine best action; use alpha-beta pruning.
132
This version cuts off search and uses an evaluation function."""
133
134
player = game.to_move(state)
135
136
# Functions used by alpha_beta
137
def max_value(state, alpha, beta, depth):
138
if cutoff_test(state, depth):
139
return eval_fn(state)
140
v = -np.inf
141
for a in game.actions(state):
142
v = max(v, min_value(game.result(state, a), alpha, beta, depth + 1))
143
if v >= beta:
144
return v
145
alpha = max(alpha, v)
146
return v
147
148
def min_value(state, alpha, beta, depth):
149
if cutoff_test(state, depth):
150
return eval_fn(state)
151
v = np.inf
152
for a in game.actions(state):
153
v = min(v, max_value(game.result(state, a), alpha, beta, depth + 1))
154
if v <= alpha:
155
return v
156
beta = min(beta, v)
157
return v
158
159
# Body of alpha_beta_cutoff_search starts here:
160
# The default test cuts off at depth d or at a terminal state
161
cutoff_test = (cutoff_test or (lambda state, depth: depth > d or game.terminal_test(state)))
162
eval_fn = eval_fn or (lambda state: game.utility(state, player))
163
best_score = -np.inf
164
beta = np.inf
165
best_action = None
166
for a in game.actions(state):
167
v = min_value(game.result(state, a), best_score, beta, 1)
168
if v > best_score:
169
best_score = v
170
best_action = a
171
return best_action
172
173
174
# ______________________________________________________________________________
175
# Players for Games
176
177
178
def query_player(game, state):
179
"""Make a move by querying standard input."""
180
print("current state:")
181
game.display(state)
182
print("available moves: {}".format(game.actions(state)))
183
print("")
184
move = None
185
if game.actions(state):
186
move_string = input('Your move? ')
187
try:
188
move = eval(move_string)
189
except NameError:
190
move = move_string
191
else:
192
print('no legal moves: passing turn to next player')
193
return move
194
195
196
def random_player(game, state):
197
"""A player that chooses a legal move at random."""
198
return random.choice(game.actions(state)) if game.actions(state) else None
199
200
201
def alpha_beta_player(game, state):
202
return alpha_beta_search(state, game)
203
204
205
def minmax_player(game,state):
206
return minmax_decision(state,game)
207
208
209
def expect_minmax_player(game, state):
210
return expect_minmax(state, game)
211
212
213
# ______________________________________________________________________________
214
# Some Sample Games
215
216
217
class Game:
218
"""A game is similar to a problem, but it has a utility for each
219
state and a terminal test instead of a path cost and a goal
220
test. To create a game, subclass this class and implement actions,
221
result, utility, and terminal_test. You may override display and
222
successors or you can inherit their default methods. You will also
223
need to set the .initial attribute to the initial state; this can
224
be done in the constructor."""
225
226
def actions(self, state):
227
"""Return a list of the allowable moves at this point."""
228
raise NotImplementedError
229
230
def result(self, state, move):
231
"""Return the state that results from making a move from a state."""
232
raise NotImplementedError
233
234
def utility(self, state, player):
235
"""Return the value of this final state to player."""
236
raise NotImplementedError
237
238
def terminal_test(self, state):
239
"""Return True if this is a final state for the game."""
240
return not self.actions(state)
241
242
def to_move(self, state):
243
"""Return the player whose move it is in this state."""
244
return state.to_move
245
246
def display(self, state):
247
"""Print or otherwise display the state."""
248
print(state)
249
250
def __repr__(self):
251
return '<{}>'.format(self.__class__.__name__)
252
253
def play_game(self, *players):
254
"""Play an n-person, move-alternating game."""
255
state = self.initial
256
while True:
257
for player in players:
258
move = player(self, state)
259
state = self.result(state, move)
260
if self.terminal_test(state):
261
self.display(state)
262
return self.utility(state, self.to_move(self.initial))
263
264
265
class StochasticGame(Game):
266
"""A stochastic game includes uncertain events which influence
267
the moves of players at each state. To create a stochastic game, subclass
268
this class and implement chances and outcome along with the other
269
unimplemented game class methods."""
270
271
def chances(self, state):
272
"""Return a list of all possible uncertain events at a state."""
273
raise NotImplementedError
274
275
def outcome(self, state, chance):
276
"""Return the state which is the outcome of a chance trial."""
277
raise NotImplementedError
278
279
def probability(self, chance):
280
"""Return the probability of occurrence of a chance."""
281
raise NotImplementedError
282
283
def play_game(self, *players):
284
"""Play an n-person, move-alternating stochastic game."""
285
state = self.initial
286
while True:
287
for player in players:
288
chance = random.choice(self.chances(state))
289
state = self.outcome(state, chance)
290
move = player(self, state)
291
state = self.result(state, move)
292
if self.terminal_test(state):
293
self.display(state)
294
return self.utility(state, self.to_move(self.initial))
295
296
297
class Fig52Game(Game):
298
"""The game represented in [Figure 5.2]. Serves as a simple test case."""
299
300
succs = dict(A=dict(a1='B', a2='C', a3='D'),
301
B=dict(b1='B1', b2='B2', b3='B3'),
302
C=dict(c1='C1', c2='C2', c3='C3'),
303
D=dict(d1='D1', d2='D2', d3='D3'))
304
utils = dict(B1=3, B2=12, B3=8, C1=2, C2=4, C3=6, D1=14, D2=5, D3=2)
305
initial = 'A'
306
307
def actions(self, state):
308
return list(self.succs.get(state, {}).keys())
309
310
def result(self, state, move):
311
return self.succs[state][move]
312
313
def utility(self, state, player):
314
if player == 'MAX':
315
return self.utils[state]
316
else:
317
return -self.utils[state]
318
319
def terminal_test(self, state):
320
return state not in ('A', 'B', 'C', 'D')
321
322
def to_move(self, state):
323
return 'MIN' if state in 'BCD' else 'MAX'
324
325
326
class Fig52Extended(Game):
327
"""Similar to Fig52Game but bigger. Useful for visualisation"""
328
329
succs = {i: dict(l=i * 3 + 1, m=i * 3 + 2, r=i * 3 + 3) for i in range(13)}
330
utils = dict()
331
332
def actions(self, state):
333
return sorted(list(self.succs.get(state, {}).keys()))
334
335
def result(self, state, move):
336
return self.succs[state][move]
337
338
def utility(self, state, player):
339
if player == 'MAX':
340
return self.utils[state]
341
else:
342
return -self.utils[state]
343
344
def terminal_test(self, state):
345
return state not in range(13)
346
347
def to_move(self, state):
348
return 'MIN' if state in {1, 2, 3} else 'MAX'
349
350
351
class TicTacToe(Game):
352
"""Play TicTacToe on an h x v board, with Max (first player) playing 'X'.
353
A state has the player to move, a cached utility, a list of moves in
354
the form of a list of (x, y) positions, and a board, in the form of
355
a dict of {(x, y): Player} entries, where Player is 'X' or 'O'."""
356
357
def __init__(self, h=3, v=3, k=3):
358
self.h = h
359
self.v = v
360
self.k = k
361
moves = [(x, y) for x in range(1, h + 1)
362
for y in range(1, v + 1)]
363
self.initial = GameState(to_move='X', utility=0, board={}, moves=moves)
364
365
def actions(self, state):
366
"""Legal moves are any square not yet taken."""
367
return state.moves
368
369
def result(self, state, move):
370
if move not in state.moves:
371
return state # Illegal move has no effect
372
board = state.board.copy()
373
board[move] = state.to_move
374
moves = list(state.moves)
375
moves.remove(move)
376
return GameState(to_move=('O' if state.to_move == 'X' else 'X'),
377
utility=self.compute_utility(board, move, state.to_move),
378
board=board, moves=moves)
379
380
def utility(self, state, player):
381
"""Return the value to player; 1 for win, -1 for loss, 0 otherwise."""
382
return state.utility if player == 'X' else -state.utility
383
384
def terminal_test(self, state):
385
"""A state is terminal if it is won or there are no empty squares."""
386
return state.utility != 0 or len(state.moves) == 0
387
388
def display(self, state):
389
board = state.board
390
for x in range(1, self.h + 1):
391
for y in range(1, self.v + 1):
392
print(board.get((x, y), '.'), end=' ')
393
print()
394
395
def compute_utility(self, board, move, player):
396
"""If 'X' wins with this move, return 1; if 'O' wins return -1; else return 0."""
397
if (self.k_in_row(board, move, player, (0, 1)) or
398
self.k_in_row(board, move, player, (1, 0)) or
399
self.k_in_row(board, move, player, (1, -1)) or
400
self.k_in_row(board, move, player, (1, 1))):
401
return +1 if player == 'X' else -1
402
else:
403
return 0
404
405
def k_in_row(self, board, move, player, delta_x_y):
406
"""Return true if there is a line through move on board for player."""
407
(delta_x, delta_y) = delta_x_y
408
x, y = move
409
n = 0 # n is number of moves in row
410
while board.get((x, y)) == player:
411
n += 1
412
x, y = x + delta_x, y + delta_y
413
x, y = move
414
while board.get((x, y)) == player:
415
n += 1
416
x, y = x - delta_x, y - delta_y
417
n -= 1 # Because we counted move itself twice
418
return n >= self.k
419
420
421
class ConnectFour(TicTacToe):
422
"""A TicTacToe-like game in which you can only make a move on the bottom
423
row, or in a square directly above an occupied square. Traditionally
424
played on a 7x6 board and requiring 4 in a row."""
425
426
def __init__(self, h=7, v=6, k=4):
427
TicTacToe.__init__(self, h, v, k)
428
429
def actions(self, state):
430
return [(x, y) for (x, y) in state.moves
431
if x == self.h or (x + 1 , y ) in state.board]
432
433
class Gomoku(TicTacToe):
434
"""Also known as Five in a row."""
435
436
def __init__(self, h=15, v=16, k=5):
437
TicTacToe.__init__(self, h, v, k)
438
439
440
class Backgammon(StochasticGame):
441
"""A two player game where the goal of each player is to move all the
442
checkers off the board. The moves for each state are determined by
443
rolling a pair of dice."""
444
445
def __init__(self):
446
"""Initial state of the game"""
447
point = {'W': 0, 'B': 0}
448
board = [point.copy() for index in range(24)]
449
board[0]['B'] = board[23]['W'] = 2
450
board[5]['W'] = board[18]['B'] = 5
451
board[7]['W'] = board[16]['B'] = 3
452
board[11]['B'] = board[12]['W'] = 5
453
self.allow_bear_off = {'W': False, 'B': False}
454
self.direction = {'W': -1, 'B': 1}
455
self.initial = StochasticGameState(to_move='W',
456
utility=0,
457
board=board,
458
moves=self.get_all_moves(board, 'W'), chance=None)
459
460
def actions(self, state):
461
"""Return a list of legal moves for a state."""
462
player = state.to_move
463
moves = state.moves
464
if len(moves) == 1 and len(moves[0]) == 1:
465
return moves
466
legal_moves = []
467
for move in moves:
468
board = copy.deepcopy(state.board)
469
if self.is_legal_move(board, move, state.chance, player):
470
legal_moves.append(move)
471
return legal_moves
472
473
def result(self, state, move):
474
board = copy.deepcopy(state.board)
475
player = state.to_move
476
self.move_checker(board, move[0], state.chance[0], player)
477
if len(move) == 2:
478
self.move_checker(board, move[1], state.chance[1], player)
479
to_move = ('W' if player == 'B' else 'B')
480
return StochasticGameState(to_move=to_move,
481
utility=self.compute_utility(board, move, player),
482
board=board,
483
moves=self.get_all_moves(board, to_move), chance=None)
484
485
def utility(self, state, player):
486
"""Return the value to player; 1 for win, -1 for loss, 0 otherwise."""
487
return state.utility if player == 'W' else -state.utility
488
489
def terminal_test(self, state):
490
"""A state is terminal if one player wins."""
491
return state.utility != 0
492
493
def get_all_moves(self, board, player):
494
"""All possible moves for a player i.e. all possible ways of
495
choosing two checkers of a player from the board for a move
496
at a given state."""
497
all_points = board
498
taken_points = [index for index, point in enumerate(all_points)
499
if point[player] > 0]
500
if self.checkers_at_home(board, player) == 1:
501
return [(taken_points[0],)]
502
moves = list(itertools.permutations(taken_points, 2))
503
moves = moves + [(index, index) for index, point in enumerate(all_points)
504
if point[player] >= 2]
505
return moves
506
507
def display(self, state):
508
"""Display state of the game."""
509
board = state.board
510
player = state.to_move
511
print("current state : ")
512
for index, point in enumerate(board):
513
print("point : ", index, " W : ", point['W'], " B : ", point['B'])
514
print("to play : ", player)
515
516
def compute_utility(self, board, move, player):
517
"""If 'W' wins with this move, return 1; if 'B' wins return -1; else return 0."""
518
util = {'W': 1, 'B': -1}
519
for idx in range(0, 24):
520
if board[idx][player] > 0:
521
return 0
522
return util[player]
523
524
def checkers_at_home(self, board, player):
525
"""Return the no. of checkers at home for a player."""
526
sum_range = range(0, 7) if player == 'W' else range(17, 24)
527
count = 0
528
for idx in sum_range:
529
count = count + board[idx][player]
530
return count
531
532
def is_legal_move(self, board, start, steps, player):
533
"""Move is a tuple which contains starting points of checkers to be
534
moved during a player's turn. An on-board move is legal if both the destinations
535
are open. A bear-off move is the one where a checker is moved off-board.
536
It is legal only after a player has moved all his checkers to his home."""
537
dest1, dest2 = vector_add(start, steps)
538
dest_range = range(0, 24)
539
move1_legal = move2_legal = False
540
if dest1 in dest_range:
541
if self.is_point_open(player, board[dest1]):
542
self.move_checker(board, start[0], steps[0], player)
543
move1_legal = True
544
else:
545
if self.allow_bear_off[player]:
546
self.move_checker(board, start[0], steps[0], player)
547
move1_legal = True
548
if not move1_legal:
549
return False
550
if dest2 in dest_range:
551
if self.is_point_open(player, board[dest2]):
552
move2_legal = True
553
else:
554
if self.allow_bear_off[player]:
555
move2_legal = True
556
return move1_legal and move2_legal
557
558
def move_checker(self, board, start, steps, player):
559
"""Move a checker from starting point by a given number of steps"""
560
dest = start + steps
561
dest_range = range(0, 24)
562
board[start][player] -= 1
563
if dest in dest_range:
564
board[dest][player] += 1
565
if self.checkers_at_home(board, player) == 15:
566
self.allow_bear_off[player] = True
567
568
def is_point_open(self, player, point):
569
"""A point is open for a player if the no. of opponent's
570
checkers already present on it is 0 or 1. A player can
571
move a checker to a point only if it is open."""
572
opponent = 'B' if player == 'W' else 'W'
573
return point[opponent] <= 1
574
575
def chances(self, state):
576
"""Return a list of all possible dice rolls at a state."""
577
dice_rolls = list(itertools.combinations_with_replacement([1, 2, 3, 4, 5, 6], 2))
578
return dice_rolls
579
580
def outcome(self, state, chance):
581
"""Return the state which is the outcome of a dice roll."""
582
dice = tuple(map((self.direction[state.to_move]).__mul__, chance))
583
return StochasticGameState(to_move=state.to_move,
584
utility=state.utility,
585
board=state.board,
586
moves=state.moves, chance=dice)
587
588
def probability(self, chance):
589
"""Return the probability of occurrence of a dice roll."""
590
return 1 / 36 if chance[0] == chance[1] else 1 / 18
591
592