Skip to content

Commit

Permalink
Backgammon implementation (#783)
Browse files Browse the repository at this point in the history
* Create model classes for backgammon

* Add game functions to model

* Implement expectiminimax function

* Correct logic in some functions

* Correct expectiminimax logic

* Refactor code and add docstrings

* Remove print statements
  • Loading branch information
AdityaDaflapurkar authored and norvig committed Mar 2, 2018
1 parent d1f162b commit d6a175c
Showing 1 changed file with 206 additions and 2 deletions.
208 changes: 206 additions & 2 deletions games.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@

from collections import namedtuple
import random

from utils import argmax
import itertools
import copy
from utils import argmax, vector_add

infinity = float('inf')
GameState = namedtuple('GameState', 'to_move, utility, board, moves')
Expand Down Expand Up @@ -40,6 +41,47 @@ def min_value(state):

# ______________________________________________________________________________

def expectiminimax(state, game):
"""Returns the best move for a player after dice are thrown. The game tree
includes chance nodes along with min and max nodes. [Figure 5.11]"""
player = game.to_move(state)

def max_value(state):
if game.terminal_test(state):
return game.utility(state, player)
v = -infinity
for a in game.actions(state):
v = max(v, chance_node(state, a))
return v

def min_value(state):
if game.terminal_test(state):
return game.utility(state, player)
v = infinity
for a in game.actions(state):
v = min(v, chance_node(state, a))
return v

def chance_node(state, action):
res_state = game.result(state, action)
sum_chances = 0
num_chances = 21
dice_rolls = list(itertools.combinations_with_replacement([1, 2, 3, 4, 5, 6], 2))
if res_state.to_move == 'W':
for val in dice_rolls:
game.dice_roll = (-val[0], -val[1])
sum_chances += max_value(res_state) * (1/36 if val[0] == val[1] else 1/18)
elif res_state.to_move == 'B':
for val in dice_rolls:
game.dice_roll = val
sum_chances += min_value(res_state) * (1/36 if val[0] == val[1] else 1/18)

return sum_chances / num_chances

# Body of expectiminimax:
return argmax(game.actions(state),
key=lambda a: chance_node(state, a))


def alphabeta_search(state, game):
"""Search game to determine best action; use alpha-beta pruning.
Expand Down Expand Up @@ -155,6 +197,9 @@ def random_player(game, state):
def alphabeta_player(game, state):
return alphabeta_search(state, game)

def expectiminimax_player(game, state):
return expectiminimax(state, game)


# ______________________________________________________________________________
# Some Sample Games
Expand Down Expand Up @@ -342,3 +387,162 @@ def __init__(self, h=7, v=6, k=4):
def actions(self, state):
return [(x, y) for (x, y) in state.moves
if y == 1 or (x, y - 1) in state.board]


class Backgammon(Game):
"""A two player game where the goal of each player is to move all the
checkers off the board. The moves for each state are determined by
rolling a pair of dice."""

def __init__(self):
self.dice_roll = (-random.randint(1, 6), -random.randint(1, 6))
board = Board()
self.initial = GameState(to_move='W',
utility=0, board=board, moves=self.get_all_moves(board, 'W'))

def actions(self, state):
"""Returns a list of legal moves for a state."""
player = state.to_move
moves = state.moves
legal_moves = []
for move in moves:
board = copy.deepcopy(state.board)
if board.is_legal_move(move, self.dice_roll, player):
legal_moves.append(move)
return legal_moves

def result(self, state, move):
board = copy.deepcopy(state.board)
player = state.to_move
board.move_checker(move[0], self.dice_roll[0], player)
board.move_checker(move[1], self.dice_roll[1], player)
to_move = ('W' if player == 'B' else 'B')
return GameState(to_move=to_move,
utility=self.compute_utility(board, move, to_move),
board=board,
moves=self.get_all_moves(board, to_move))


def utility(self, state, player):
"""Return the value to player; 1 for win, -1 for loss, 0 otherwise."""
return state.utility if player == 'W' else -state.utility

def terminal_test(self, state):
"""A state is terminal if one player wins."""
return state.utility != 0

def get_all_moves(self, board, player):
"""All possible moves for a player i.e. all possible ways of
choosing two checkers of a player from the board for a move
at a given state."""
all_points = board.points
taken_points = [index for index, point in enumerate(all_points)
if point.checkers[player] > 0]
moves = list(itertools.permutations(taken_points, 2))
moves = moves + [(index, index) for index, point in enumerate(all_points)
if point.checkers[player] >= 2]
return moves

def display(self, state):
"""Display state of the game."""
board = state.board
player = state.to_move
for index, point in enumerate(board.points):
if point.checkers['W'] != 0 or point.checkers['B'] != 0:
print("Point : ", index, " W : ", point.checkers['W'], " B : ", point.checkers['B'])
print("player : ", player)


def compute_utility(self, board, move, player):
"""If 'W' wins with this move, return 1; if 'B' wins return -1; else return 0."""
count = 0
for idx in range(0, 24):
count = count + board.points[idx].checkers[player]
if player == 'W' and count == 0:
return 1
if player == 'B' and count == 0:
return -1
return 0


class Board:
"""The board consists of 24 points. Each player('W' and 'B') initially
has 15 checkers on board. Player 'W' moves from point 23 to point 0
and player 'B' moves from point 0 to 23. Points 0-7 are
home for player W and points 17-24 are home for B."""

def __init__(self):
"""Initial state of the game"""
# TODO : Add bar to Board class where a blot is placed when it is hit.
self.points = [Point() for index in range(24)]
self.points[0].checkers['B'] = self.points[23].checkers['W'] = 2
self.points[5].checkers['W'] = self.points[18].checkers['B'] = 5
self.points[7].checkers['W'] = self.points[16].checkers['B'] = 3
self.points[11].checkers['B'] = self.points[12].checkers['W'] = 5
self.allow_bear_off = {'W': False, 'B': False}

def checkers_at_home(self, player):
"""Returns the no. of checkers at home for a player."""
sum_range = range(0, 7) if player == 'W' else range(17, 24)
count = 0
for idx in sum_range:
count = count + self.points[idx].checkers[player]
return count

def is_legal_move(self, start, steps, player):
"""Move is a tuple which contains starting points of checkers to be
moved during a player's turn. An on-board move is legal if both the destinations
are open. A bear-off move is the one where a checker is moved off-board.
It is legal only after a player has moved all his checkers to his home."""
dest1, dest2 = vector_add(start, steps)
dest_range = range(0, 24)
move1_legal = move2_legal = False
if dest1 in dest_range:
if self.points[dest1].is_open_for(player):
self.move_checker(start[0], steps[0], player)
move1_legal = True
else:
if self.allow_bear_off[player]:
self.move_checker(start[0], steps[0], player)
move1_legal = True
if not move1_legal:
return False
if dest2 in dest_range:
if self.points[dest2].is_open_for(player):
move2_legal = True
else:
if self.allow_bear_off[player]:
move2_legal = True
return move1_legal and move2_legal

def move_checker(self, start, steps, player):
"""Moves a checker from starting point by a given number of steps"""
dest = start + steps
dest_range = range(0, 24)
self.points[start].remove_checker(player)
if dest in dest_range:
self.points[dest].add_checker(player)
if self.checkers_at_home(player) == 15:
self.allow_bear_off[player] = True

class Point:
"""A point is one of the 24 triangles on the board where
the players' checkers are placed."""

def __init__(self):
self.checkers = {'W':0, 'B':0}

def is_open_for(self, player):
"""A point is open for a player if the no. of opponent's
checkers already present on it is 0 or 1. A player can
move a checker to a point only if it is open."""
opponent = 'B' if player == 'W' else 'W'
return self.checkers[opponent] <= 1

def add_checker(self, player):
"""Place a player's checker on a point."""
self.checkers[player] += 1

def remove_checker(self, player):
"""Remove a player's checker from a point."""
self.checkers[player] -= 1

0 comments on commit d6a175c

Please sign in to comment.