| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
1 parent d1f162b commit d6a175c
1 file changed
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -2,8 +2,9 @@ | |||
| 2 | 2 | ||
| 3 | 3 | from collections import namedtuple | |
| 4 | 4 | import random | |
| 5 | - | ||
| 6 | - from utils import argmax | ||
| 5 | + import itertools | ||
| 6 | + import copy | ||
| 7 | + from utils import argmax, vector_add | ||
| 7 | 8 | ||
| 8 | 9 | infinity = float('inf') | |
| 9 | 10 | GameState = namedtuple('GameState', 'to_move, utility, board, moves') | |
@@ -40,6 +41,47 @@ def min_value(state): | |||
| 40 | 41 | ||
| 41 | 42 | # ______________________________________________________________________________ | |
| 42 | 43 | ||
| 44 | + def expectiminimax(state, game): | ||
| 45 | + """Returns the best move for a player after dice are thrown. The game tree | ||
| 46 | + includes chance nodes along with min and max nodes. [Figure 5.11]""" | ||
| 47 | + player = game.to_move(state) | ||
| 48 | + | ||
| 49 | + def max_value(state): | ||
| 50 | + if game.terminal_test(state): | ||
| 51 | + return game.utility(state, player) | ||
| 52 | + v = -infinity | ||
| 53 | + for a in game.actions(state): | ||
| 54 | + v = max(v, chance_node(state, a)) | ||
| 55 | + return v | ||
| 56 | + | ||
| 57 | + def min_value(state): | ||
| 58 | + if game.terminal_test(state): | ||
| 59 | + return game.utility(state, player) | ||
| 60 | + v = infinity | ||
| 61 | + for a in game.actions(state): | ||
| 62 | + v = min(v, chance_node(state, a)) | ||
| 63 | + return v | ||
| 64 | + | ||
| 65 | + def chance_node(state, action): | ||
| 66 | + res_state = game.result(state, action) | ||
| 67 | + sum_chances = 0 | ||
| 68 | + 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) * (1/36 if val[0] == val[1] else 1/18) | ||
| 74 | + elif res_state.to_move == 'B': | ||
| 75 | + for val in dice_rolls: | ||
| 76 | + game.dice_roll = val | ||
| 77 | + sum_chances += min_value(res_state) * (1/36 if val[0] == val[1] else 1/18) | ||
| 78 | + | ||
| 79 | + return sum_chances / num_chances | ||
| 80 | + | ||
| 81 | + # Body of expectiminimax: | ||
| 82 | + return argmax(game.actions(state), | ||
| 83 | + key=lambda a: chance_node(state, a)) | ||
| 84 | + | ||
| 43 | 85 | ||
| 44 | 86 | def alphabeta_search(state, game): | |
| 45 | 87 | """Search game to determine best action; use alpha-beta pruning. | |
@@ -155,6 +197,9 @@ def random_player(game, state): | |||
| 155 | 197 | def alphabeta_player(game, state): | |
| 156 | 198 | return alphabeta_search(state, game) | |
| 157 | 199 | ||
| 200 | + def expectiminimax_player(game, state): | ||
| 201 | + return expectiminimax(state, game) | ||
| 202 | + | ||
| 158 | 203 | ||
| 159 | 204 | # ______________________________________________________________________________ | |
| 160 | 205 | # Some Sample Games | |
@@ -342,3 +387,162 @@ def __init__(self, h=7, v=6, k=4): | |||
| 342 | 387 | def actions(self, state): | |
| 343 | 388 | return [(x, y) for (x, y) in state.moves | |
| 344 | 389 | if y == 1 or (x, y - 1) in state.board] | |
| 390 | + | ||
| 391 | + | ||
| 392 | + class Backgammon(Game): | ||
| 393 | + """A two player game where the goal of each player is to move all the | ||
| 394 | + checkers off the board. The moves for each state are determined by | ||
| 395 | + rolling a pair of dice.""" | ||
| 396 | + | ||
| 397 | + def __init__(self): | ||
| 398 | + self.dice_roll = (-random.randint(1, 6), -random.randint(1, 6)) | ||
| 399 | + board = Board() | ||
| 400 | + self.initial = GameState(to_move='W', | ||
| 401 | + utility=0, board=board, moves=self.get_all_moves(board, 'W')) | ||
| 402 | + | ||
| 403 | + def actions(self, state): | ||
| 404 | + """Returns a list of legal moves for a state.""" | ||
| 405 | + player = state.to_move | ||
| 406 | + moves = state.moves | ||
| 407 | + legal_moves = [] | ||
| 408 | + for move in moves: | ||
| 409 | + board = copy.deepcopy(state.board) | ||
| 410 | + if board.is_legal_move(move, self.dice_roll, player): | ||
| 411 | + legal_moves.append(move) | ||
| 412 | + return legal_moves | ||
| 413 | + | ||
| 414 | + def result(self, state, move): | ||
| 415 | + board = copy.deepcopy(state.board) | ||
| 416 | + player = state.to_move | ||
| 417 | + board.move_checker(move[0], self.dice_roll[0], player) | ||
| 418 | + board.move_checker(move[1], self.dice_roll[1], player) | ||
| 419 | + to_move = ('W' if player == 'B' else 'B') | ||
| 420 | + return GameState(to_move=to_move, | ||
| 421 | + utility=self.compute_utility(board, move, to_move), | ||
| 422 | + board=board, | ||
| 423 | + moves=self.get_all_moves(board, to_move)) | ||
| 424 | + | ||
| 425 | + | ||
| 426 | + def utility(self, state, player): | ||
| 427 | + """Return the value to player; 1 for win, -1 for loss, 0 otherwise.""" | ||
| 428 | + return state.utility if player == 'W' else -state.utility | ||
| 429 | + | ||
| 430 | + def terminal_test(self, state): | ||
| 431 | + """A state is terminal if one player wins.""" | ||
| 432 | + return state.utility != 0 | ||
| 433 | + | ||
| 434 | + def get_all_moves(self, board, player): | ||
| 435 | + """All possible moves for a player i.e. all possible ways of | ||
| 436 | + choosing two checkers of a player from the board for a move | ||
| 437 | + at a given state.""" | ||
| 438 | + all_points = board.points | ||
| 439 | + taken_points = [index for index, point in enumerate(all_points) | ||
| 440 | + if point.checkers[player] > 0] | ||
| 441 | + moves = list(itertools.permutations(taken_points, 2)) | ||
| 442 | + moves = moves + [(index, index) for index, point in enumerate(all_points) | ||
| 443 | + if point.checkers[player] >= 2] | ||
| 444 | + return moves | ||
| 445 | + | ||
| 446 | + def display(self, state): | ||
| 447 | + """Display state of the game.""" | ||
| 448 | + board = state.board | ||
| 449 | + player = state.to_move | ||
| 450 | + for index, point in enumerate(board.points): | ||
| 451 | + if point.checkers['W'] != 0 or point.checkers['B'] != 0: | ||
| 452 | + print("Point : ", index, " W : ", point.checkers['W'], " B : ", point.checkers['B']) | ||
| 453 | + print("player : ", player) | ||
| 454 | + | ||
| 455 | + | ||
| 456 | + def compute_utility(self, board, move, player): | ||
| 457 | + """If 'W' wins with this move, return 1; if 'B' wins return -1; else return 0.""" | ||
| 458 | + count = 0 | ||
| 459 | + for idx in range(0, 24): | ||
| 460 | + count = count + board.points[idx].checkers[player] | ||
| 461 | + if player == 'W' and count == 0: | ||
| 462 | + return 1 | ||
| 463 | + if player == 'B' and count == 0: | ||
| 464 | + return -1 | ||
| 465 | + return 0 | ||
| 466 | + | ||
| 467 | + | ||
| 468 | + class Board: | ||
| 469 | + """The board consists of 24 points. Each player('W' and 'B') initially | ||
| 470 | + has 15 checkers on board. Player 'W' moves from point 23 to point 0 | ||
| 471 | + and player 'B' moves from point 0 to 23. Points 0-7 are | ||
| 472 | + home for player W and points 17-24 are home for B.""" | ||
| 473 | + | ||
| 474 | + def __init__(self): | ||
| 475 | + """Initial state of the game""" | ||
| 476 | + # TODO : Add bar to Board class where a blot is placed when it is hit. | ||
| 477 | + self.points = [Point() for index in range(24)] | ||
| 478 | + self.points[0].checkers['B'] = self.points[23].checkers['W'] = 2 | ||
| 479 | + self.points[5].checkers['W'] = self.points[18].checkers['B'] = 5 | ||
| 480 | + self.points[7].checkers['W'] = self.points[16].checkers['B'] = 3 | ||
| 481 | + self.points[11].checkers['B'] = self.points[12].checkers['W'] = 5 | ||
| 482 | + self.allow_bear_off = {'W': False, 'B': False} | ||
| 483 | + | ||
| 484 | + def checkers_at_home(self, player): | ||
| 485 | + """Returns the no. of checkers at home for a player.""" | ||
| 486 | + sum_range = range(0, 7) if player == 'W' else range(17, 24) | ||
| 487 | + count = 0 | ||
| 488 | + for idx in sum_range: | ||
| 489 | + count = count + self.points[idx].checkers[player] | ||
| 490 | + return count | ||
| 491 | + | ||
| 492 | + def is_legal_move(self, start, steps, player): | ||
| 493 | + """Move is a tuple which contains starting points of checkers to be | ||
| 494 | + moved during a player's turn. An on-board move is legal if both the destinations | ||
| 495 | + are open. A bear-off move is the one where a checker is moved off-board. | ||
| 496 | + It is legal only after a player has moved all his checkers to his home.""" | ||
| 497 | + dest1, dest2 = vector_add(start, steps) | ||
| 498 | + dest_range = range(0, 24) | ||
| 499 | + move1_legal = move2_legal = False | ||
| 500 | + if dest1 in dest_range: | ||
| 501 | + if self.points[dest1].is_open_for(player): | ||
| 502 | + self.move_checker(start[0], steps[0], player) | ||
| 503 | + move1_legal = True | ||
| 504 | + else: | ||
| 505 | + if self.allow_bear_off[player]: | ||
| 506 | + self.move_checker(start[0], steps[0], player) | ||
| 507 | + move1_legal = True | ||
| 508 | + if not move1_legal: | ||
| 509 | + return False | ||
| 510 | + if dest2 in dest_range: | ||
| 511 | + if self.points[dest2].is_open_for(player): | ||
| 512 | + move2_legal = True | ||
| 513 | + else: | ||
| 514 | + if self.allow_bear_off[player]: | ||
| 515 | + move2_legal = True | ||
| 516 | + return move1_legal and move2_legal | ||
| 517 | + | ||
| 518 | + def move_checker(self, start, steps, player): | ||
| 519 | + """Moves a checker from starting point by a given number of steps""" | ||
| 520 | + dest = start + steps | ||
| 521 | + dest_range = range(0, 24) | ||
| 522 | + self.points[start].remove_checker(player) | ||
| 523 | + if dest in dest_range: | ||
| 524 | + self.points[dest].add_checker(player) | ||
| 525 | + if self.checkers_at_home(player) == 15: | ||
| 526 | + self.allow_bear_off[player] = True | ||
| 527 | + | ||
| 528 | + class Point: | ||
| 529 | + """A point is one of the 24 triangles on the board where | ||
| 530 | + the players' checkers are placed.""" | ||
| 531 | + | ||
| 532 | + def __init__(self): | ||
| 533 | + self.checkers = {'W':0, 'B':0} | ||
| 534 | + | ||
| 535 | + def is_open_for(self, player): | ||
| 536 | + """A point is open for a player if the no. of opponent's | ||
| 537 | + checkers already present on it is 0 or 1. A player can | ||
| 538 | + move a checker to a point only if it is open.""" | ||
| 539 | + opponent = 'B' if player == 'W' else 'W' | ||
| 540 | + return self.checkers[opponent] <= 1 | ||
| 541 | + | ||
| 542 | + def add_checker(self, player): | ||
| 543 | + """Place a player's checker on a point.""" | ||
| 544 | + self.checkers[player] += 1 | ||
| 545 | + | ||
| 546 | + def remove_checker(self, player): | ||
| 547 | + """Remove a player's checker from a point.""" | ||
| 548 | + self.checkers[player] -= 1 | ||
| Back | FazBrowse Home | New Git URL |
0 commit comments