88
99infinity = float ('inf' )
1010GameState = namedtuple ('GameState' , 'to_move, utility, board, moves' )
11+ StochasticGameState = namedtuple ('StochasticGameState' , 'to_move, utility, board, moves, chance' )
1112
1213# ______________________________________________________________________________
1314# Minimax Search
@@ -41,42 +42,38 @@ def min_value(state):
4142
4243# ______________________________________________________________________________
4344
44- dice_rolls = list (itertools .combinations_with_replacement ([1 , 2 , 3 , 4 , 5 , 6 ], 2 ))
45- direction = {'W' : - 1 , 'B' : 1 }
4645
4746def expectiminimax (state , game ):
4847 """Return the best move for a player after dice are thrown. The game tree
4948 includes chance nodes along with min and max nodes. [Figure 5.11]"""
5049 player = game .to_move (state )
5150
52- def max_value (state , dice_roll ):
51+ def max_value (state ):
5352 v = - infinity
5453 for a in game .actions (state ):
5554 v = max (v , chance_node (state , a ))
56- game .dice_roll = dice_roll
5755 return v
5856
59- def min_value (state , dice_roll ):
57+ def min_value (state ):
6058 v = infinity
6159 for a in game .actions (state ):
6260 v = min (v , chance_node (state , a ))
63- game .dice_roll = dice_roll
6461 return v
6562
6663 def chance_node (state , action ):
6764 res_state = game .result (state , action )
6865 if game .terminal_test (res_state ):
6966 return game .utility (res_state , player )
7067 sum_chances = 0
71- num_chances = 21
72- for val in dice_rolls :
73- game . dice_roll = tuple ( map (( direction [ res_state . to_move ]). __mul__ , val ) )
68+ num_chances = len ( game . chances ( res_state ))
69+ for chance in game . chances ( res_state ) :
70+ res_state = game . outcome ( res_state , chance )
7471 util = 0
7572 if res_state .to_move == player :
76- util = max_value (res_state , game . dice_roll )
73+ util = max_value (res_state )
7774 else :
78- util = min_value (res_state , game . dice_roll )
79- sum_chances += util * ( 1 / 36 if val [ 0 ] == val [ 1 ] else 1 / 18 )
75+ util = min_value (res_state )
76+ sum_chances += util * game . probability ( chance )
8077 return sum_chances / num_chances
8178
8279 # Body of expectiminimax:
@@ -256,6 +253,36 @@ def play_game(self, *players):
256253 self .display (state )
257254 return self .utility (state , self .to_move (self .initial ))
258255
256+ class StochasticGame (Game ):
257+ """A stochastic game includes uncertain events which influence
258+ the moves of players at each state. To create a stochastic game, subclass
259+ this class and implement chances and outcome along with the other
260+ unimplemented game class methods."""
261+
262+ def chances (self , state ):
263+ """Return a list of all possible uncertain events at a state."""
264+ raise NotImplementedError
265+
266+ def outcome (self , state , chance ):
267+ """Return the state which is the outcome of a chance trial."""
268+ raise NotImplementedError
269+
270+ def probability (self , chance ):
271+ """Return the probability of occurence of a chance."""
272+ raise NotImplementedError
273+
274+ def play_game (self , * players ):
275+ """Play an n-person, move-alternating stochastic game."""
276+ state = self .initial
277+ while True :
278+ for player in players :
279+ chance = random .choice (self .chances (state ))
280+ state = self .outcome (state , chance )
281+ move = player (self , state )
282+ state = self .result (state , move )
283+ if self .terminal_test (state ):
284+ self .display (state )
285+ return self .utility (state , self .to_move (self .initial ))
259286
260287class Fig52Game (Game ):
261288 """The game represented in [Figure 5.2]. Serves as a simple test case."""
@@ -393,26 +420,25 @@ def actions(self, state):
393420 if y == 1 or (x , y - 1 ) in state .board ]
394421
395422
396- class Backgammon (Game ):
423+ class Backgammon (StochasticGame ):
397424 """A two player game where the goal of each player is to move all the
398425 checkers off the board. The moves for each state are determined by
399426 rolling a pair of dice."""
400427
401428 def __init__ (self ):
402429 """Initial state of the game"""
403- self .dice_roll = tuple (map ((direction ['W' ]).__mul__ , random .choice (dice_rolls )))
404- # TODO : Add bar to Board class where a blot is placed when it is hit.
405430 point = {'W' : 0 , 'B' : 0 }
406431 board = [point .copy () for index in range (24 )]
407432 board [0 ]['B' ] = board [23 ]['W' ] = 2
408433 board [5 ]['W' ] = board [18 ]['B' ] = 5
409434 board [7 ]['W' ] = board [16 ]['B' ] = 3
410435 board [11 ]['B' ] = board [12 ]['W' ] = 5
411436 self .allow_bear_off = {'W' : False , 'B' : False }
412- self .initial = GameState (to_move = 'W' ,
413- utility = 0 ,
414- board = board ,
415- moves = self .get_all_moves (board , 'W' ))
437+ self .direction = {'W' : - 1 , 'B' : 1 }
438+ self .initial = StochasticGameState (to_move = 'W' ,
439+ utility = 0 ,
440+ board = board ,
441+ moves = self .get_all_moves (board , 'W' ), chance = None )
416442
417443 def actions (self , state ):
418444 """Return a list of legal moves for a state."""
@@ -423,21 +449,21 @@ def actions(self, state):
423449 legal_moves = []
424450 for move in moves :
425451 board = copy .deepcopy (state .board )
426- if self .is_legal_move (board , move , self . dice_roll , player ):
452+ if self .is_legal_move (board , move , state . chance , player ):
427453 legal_moves .append (move )
428454 return legal_moves
429455
430456 def result (self , state , move ):
431457 board = copy .deepcopy (state .board )
432458 player = state .to_move
433- self .move_checker (board , move [0 ], self . dice_roll [0 ], player )
459+ self .move_checker (board , move [0 ], state . chance [0 ], player )
434460 if len (move ) == 2 :
435- self .move_checker (board , move [1 ], self . dice_roll [1 ], player )
461+ self .move_checker (board , move [1 ], state . chance [1 ], player )
436462 to_move = ('W' if player == 'B' else 'B' )
437- return GameState (to_move = to_move ,
438- utility = self .compute_utility (board , move , player ),
439- board = board ,
440- moves = self .get_all_moves (board , to_move ))
463+ return StochasticGameState (to_move = to_move ,
464+ utility = self .compute_utility (board , move , player ),
465+ board = board ,
466+ moves = self .get_all_moves (board , to_move ), chance = None )
441467
442468 def utility (self , state , player ):
443469 """Return the value to player; 1 for win, -1 for loss, 0 otherwise."""
@@ -472,7 +498,7 @@ def display(self, state):
472498
473499 def compute_utility (self , board , move , player ):
474500 """If 'W' wins with this move, return 1; if 'B' wins return -1; else return 0."""
475- util = {'W' : 1 , 'B' : '-1' }
501+ util = {'W' : 1 , 'B' : - 1 }
476502 for idx in range (0 , 24 ):
477503 if board [idx ][player ] > 0 :
478504 return 0
@@ -529,18 +555,19 @@ def is_point_open(self, player, point):
529555 opponent = 'B' if player == 'W' else 'W'
530556 return point [opponent ] <= 1
531557
532- def play_game (self , * players ):
533- """Play backgammon."""
534- state = self .initial
535- while True :
536- for player in players :
537- saved_dice_roll = self .dice_roll
538- move = player (self , state )
539- self .dice_roll = saved_dice_roll
540- if move is not None :
541- state = self .result (state , move )
542- self .dice_roll = tuple (map ((direction [player ]).__mul__ ,
543- random .choice (dice_rolls )))
544- if self .terminal_test (state ):
545- self .display (state )
546- return self .utility (state , self .to_move (self .initial ))
558+ def chances (self , state ):
559+ """Return a list of all possible dice rolls at a state."""
560+ dice_rolls = list (itertools .combinations_with_replacement ([1 , 2 , 3 , 4 , 5 , 6 ], 2 ))
561+ return dice_rolls
562+
563+ def outcome (self , state , chance ):
564+ """Return the state which is the outcome of a dice roll."""
565+ dice = tuple (map ((self .direction [state .to_move ]).__mul__ , chance ))
566+ return StochasticGameState (to_move = state .to_move ,
567+ utility = state .utility ,
568+ board = state .board ,
569+ moves = state .moves , chance = dice )
570+
571+ def probability (self , chance ):
572+ """Return the probability of occurence of a dice roll."""
573+ return 1 / 36 if chance [0 ] == chance [1 ] else 1 / 18
0 commit comments