@@ -41,6 +41,9 @@ def min_value(state):
4141
4242# ______________________________________________________________________________
4343
44+ dice_rolls = list (itertools .combinations_with_replacement ([1 , 2 , 3 , 4 , 5 , 6 ], 2 ))
45+ direction = {'W' : - 1 , 'B' : 1 }
46+
4447def expectiminimax (state , game ):
4548 """Return the best move for a player after dice are thrown. The game tree
4649 includes chance nodes along with min and max nodes. [Figure 5.11]"""
@@ -66,21 +69,19 @@ def chance_node(state, action):
6669 return game .utility (res_state , player )
6770 sum_chances = 0
6871 num_chances = 21
69- dice_rolls = list (itertools .combinations_with_replacement ([1 , 2 , 3 , 4 , 5 , 6 ], 2 ))
70- if res_state .to_move == 'W' :
71- for val in dice_rolls :
72- game .dice_roll = (- val [0 ], - val [1 ])
73- sum_chances += max_value (res_state ,
74- (- val [0 ], - val [1 ])) * (1 / 36 if val [0 ] == val [1 ] else 1 / 18 )
75- elif res_state .to_move == 'B' :
76- for val in dice_rolls :
77- game .dice_roll = val
78- sum_chances += min_value (res_state , val ) * (1 / 36 if val [0 ] == val [1 ] else 1 / 18 )
72+ for val in dice_rolls :
73+ game .dice_roll = tuple (map ((direction [res_state .to_move ]).__mul__ , val ))
74+ util = 0
75+ if res_state .to_move == player :
76+ util = max_value (res_state , game .dice_roll )
77+ else :
78+ util = min_value (res_state , game .dice_roll )
79+ sum_chances += util * (1 / 36 if val [0 ] == val [1 ] else 1 / 18 )
7980 return sum_chances / num_chances
8081
8182 # Body of expectiminimax:
8283 return argmax (game .actions (state ),
83- key = lambda a : chance_node (state , a ))
84+ key = lambda a : chance_node (state , a ), default = None )
8485
8586
8687def alphabeta_search (state , game ):
@@ -181,18 +182,21 @@ def query_player(game, state):
181182 game .display (state )
182183 print ("available moves: {}" .format (game .actions (state )))
183184 print ("" )
184- move_string = input ('Your move? ' )
185- try :
186- move = eval (move_string )
187- except NameError :
188- move = move_string
185+ move = None
186+ if game .actions (state ):
187+ move_string = input ('Your move? ' )
188+ try :
189+ move = eval (move_string )
190+ except NameError :
191+ move = move_string
192+ else :
193+ print ('no legal moves: passing turn to next player' )
189194 return move
190195
191196
192197def random_player (game , state ):
193198 """A player that chooses a legal move at random."""
194- return random .choice (game .actions (state ))
195-
199+ return random .choice (game .actions (state )) if game .actions (state ) else None
196200
197201def alphabeta_player (game , state ):
198202 return alphabeta_search (state , game )
@@ -396,47 +400,45 @@ class Backgammon(Game):
396400
397401 def __init__ (self ):
398402 """Initial state of the game"""
399- self .dice_roll = ( - random . randint ( 1 , 6 ), - random .randint ( 1 , 6 ))
403+ self .dice_roll = tuple ( map (( direction [ 'W' ]). __mul__ , random .choice ( dice_rolls ) ))
400404 # TODO : Add bar to Board class where a blot is placed when it is hit.
401- point = {'W' :0 , 'B' :0 }
402- self .board = [point .copy () for index in range (24 )]
403- self .board [0 ]['B' ] = self .board [23 ]['W' ] = 2
404- self .board [5 ]['W' ] = self .board [18 ]['B' ] = 5
405- self .board [7 ]['W' ] = self .board [16 ]['B' ] = 3
406- self .board [11 ]['B' ] = self .board [12 ]['W' ] = 5
407- self .allow_bear_off = {'W' : False , 'B' : False }
408-
405+ point = {'W' : 0 , 'B' : 0 }
406+ board = [point .copy () for index in range (24 )]
407+ board [0 ]['B' ] = board [23 ]['W' ] = 2
408+ board [5 ]['W' ] = board [18 ]['B' ] = 5
409+ board [7 ]['W' ] = board [16 ]['B' ] = 3
410+ board [11 ]['B' ] = board [12 ]['W' ] = 5
411+ self .allow_bear_off = {'W' : False , 'B' : False }
409412 self .initial = GameState (to_move = 'W' ,
410- utility = 0 ,
411- board = self . board ,
412- moves = self .get_all_moves (self . board , 'W' ))
413+ utility = 0 ,
414+ board = board ,
415+ moves = self .get_all_moves (board , 'W' ))
413416
414417 def actions (self , state ):
415- """Returns a list of legal moves for a state."""
418+ """Return a list of legal moves for a state."""
416419 player = state .to_move
417420 moves = state .moves
418421 if len (moves ) == 1 and len (moves [0 ]) == 1 :
419422 return moves
420423 legal_moves = []
421424 for move in moves :
422425 board = copy .deepcopy (state .board )
423- if self .is_legal_move (move , self .dice_roll , player ):
426+ if self .is_legal_move (board , move , self .dice_roll , player ):
424427 legal_moves .append (move )
425428 return legal_moves
426429
427430 def result (self , state , move ):
428431 board = copy .deepcopy (state .board )
429432 player = state .to_move
430- self .move_checker (move [0 ], self .dice_roll [0 ], player )
433+ self .move_checker (board , move [0 ], self .dice_roll [0 ], player )
431434 if len (move ) == 2 :
432- self .move_checker (move [1 ], self .dice_roll [1 ], player )
435+ self .move_checker (board , move [1 ], self .dice_roll [1 ], player )
433436 to_move = ('W' if player == 'B' else 'B' )
434437 return GameState (to_move = to_move ,
435438 utility = self .compute_utility (board , move , player ),
436439 board = board ,
437440 moves = self .get_all_moves (board , to_move ))
438441
439-
440442 def utility (self , state , player ):
441443 """Return the value to player; 1 for win, -1 for loss, 0 otherwise."""
442444 return state .utility if player == 'W' else - state .utility
@@ -452,7 +454,7 @@ def get_all_moves(self, board, player):
452454 all_points = board
453455 taken_points = [index for index , point in enumerate (all_points )
454456 if point [player ] > 0 ]
455- if self .checkers_at_home (player ) == 1 :
457+ if self .checkers_at_home (board , player ) == 1 :
456458 return [(taken_points [0 ], )]
457459 moves = list (itertools .permutations (taken_points , 2 ))
458460 moves = moves + [(index , index ) for index , point in enumerate (all_points )
@@ -463,32 +465,28 @@ def display(self, state):
463465 """Display state of the game."""
464466 board = state .board
465467 player = state .to_move
466- print ("Current State : " )
468+ print ("current state : " )
467469 for index , point in enumerate (board ):
468- if point ['W' ] != 0 or point ['B' ] != 0 :
469- print ("Point : " , index , " W : " , point ['W' ], " B : " , point ['B' ])
470- print ("To play : " , player )
470+ print ("point : " , index , " W : " , point ['W' ], " B : " , point ['B' ])
471+ print ("to play : " , player )
471472
472473 def compute_utility (self , board , move , player ):
473474 """If 'W' wins with this move, return 1; if 'B' wins return -1; else return 0."""
474- count = 0
475+ util = { 'W' : 1 , 'B' : '-1' }
475476 for idx in range (0 , 24 ):
476- count = count + board [idx ][player ]
477- if player == 'W' and count == 0 :
478- return 1
479- if player == 'B' and count == 0 :
480- return - 1
481- return 0
477+ if board [idx ][player ] > 0 :
478+ return 0
479+ return util [player ]
482480
483- def checkers_at_home (self , player ):
481+ def checkers_at_home (self , board , player ):
484482 """Return the no. of checkers at home for a player."""
485483 sum_range = range (0 , 7 ) if player == 'W' else range (17 , 24 )
486484 count = 0
487485 for idx in sum_range :
488- count = count + self . board [idx ][player ]
486+ count = count + board [idx ][player ]
489487 return count
490488
491- def is_legal_move (self , start , steps , player ):
489+ def is_legal_move (self , board , start , steps , player ):
492490 """Move is a tuple which contains starting points of checkers to be
493491 moved during a player's turn. An on-board move is legal if both the destinations
494492 are open. A bear-off move is the one where a checker is moved off-board.
@@ -497,31 +495,31 @@ def is_legal_move(self, start, steps, player):
497495 dest_range = range (0 , 24 )
498496 move1_legal = move2_legal = False
499497 if dest1 in dest_range :
500- if self .is_point_open (player , self . board [dest1 ]):
501- self .move_checker (start [0 ], steps [0 ], player )
498+ if self .is_point_open (player , board [dest1 ]):
499+ self .move_checker (board , start [0 ], steps [0 ], player )
502500 move1_legal = True
503501 else :
504502 if self .allow_bear_off [player ]:
505- self .move_checker (start [0 ], steps [0 ], player )
503+ self .move_checker (board , start [0 ], steps [0 ], player )
506504 move1_legal = True
507505 if not move1_legal :
508506 return False
509507 if dest2 in dest_range :
510- if self .is_point_open (player , self . board [dest2 ]):
508+ if self .is_point_open (player , board [dest2 ]):
511509 move2_legal = True
512510 else :
513511 if self .allow_bear_off [player ]:
514512 move2_legal = True
515513 return move1_legal and move2_legal
516514
517- def move_checker (self , start , steps , player ):
515+ def move_checker (self , board , start , steps , player ):
518516 """Move a checker from starting point by a given number of steps"""
519517 dest = start + steps
520518 dest_range = range (0 , 24 )
521- self . board [start ][player ] -= 1
519+ board [start ][player ] -= 1
522520 if dest in dest_range :
523- self . board [dest ][player ] += 1
524- if self .checkers_at_home (player ) == 15 :
521+ board [dest ][player ] += 1
522+ if self .checkers_at_home (board , player ) == 15 :
525523 self .allow_bear_off [player ] = True
526524
527525 def is_point_open (self , player , point ):
@@ -530,3 +528,19 @@ def is_point_open(self, player, point):
530528 move a checker to a point only if it is open."""
531529 opponent = 'B' if player == 'W' else 'W'
532530 return point [opponent ] <= 1
531+
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 ))
0 commit comments