backgammon/board.py

283 lines
9.7 KiB
Python
Raw Normal View History

2018-02-05 21:31:34 +00:00
import numpy as np
2018-02-13 13:38:49 +00:00
import itertools
2018-02-05 21:31:34 +00:00
class Board:
initial_state = ( 0,
2018-02-13 13:38:49 +00:00
2, 0, 0, 0, 0, -5,
0, -3, 0, 0, 0, 5,
-5, 0, 0, 0, 3, 0,
5, 0, 0, 0, 0, -2,
0 )
2018-02-13 13:38:49 +00:00
@staticmethod
def idxs_with_checkers_of_player(board, player):
idxs = []
for idx, checker_count in enumerate(board):
if checker_count * player >= 1:
idxs.append(idx)
return idxs
2018-02-13 13:38:49 +00:00
@staticmethod
def is_move_valid(board, player, face_value, move):
2018-02-14 10:48:07 +00:00
def sign(a):
return (a > 0) - (a < 0)
2018-02-13 13:38:49 +00:00
from_idx = move[0]
to_idx = move[1]
2018-02-14 10:48:07 +00:00
to_state = None
2018-02-13 13:38:49 +00:00
from_state = board[from_idx]
2018-02-14 10:48:07 +00:00
delta = to_idx - from_idx
direction = sign(delta)
bearing_off = None
# FIXME: Use get instead of array-like indexing
2018-02-14 10:48:07 +00:00
if to_idx >= 1 and to_idx <= 24:
to_state = board[to_idx]
bearing_off = False
else: # Bearing off
to_state = 0
bearing_off = True
# print("_"*20)
# print("board:", board)
# print("to_idx:", to_idx, "board[to_idx]:", board[to_idx], "to_state:", to_state)
# print("+"*20)
def is_forward_move():
return direction == player
def face_value_match_move_length():
return abs(delta) == face_value
def bear_in_if_checker_on_bar():
if player == 1:
bar = 0
else:
bar = 25
bar_state = board[bar]
if bar_state != 0:
return from_idx == bar
else:
return True
def checkers_at_from_idx():
return sign(from_state) == player
def no_block_at_to_idx():
if -sign(to_state) == player:
return abs(to_state) == 1
else:
return True
def can_bear_off():
checker_idxs = Board.idxs_with_checkers_of_player(board, player)
def is_moving_backmost_checker():
if player == 1:
return all([(idx >= from_idx) for idx in checker_idxs])
else:
return all([(idx <= from_idx) for idx in checker_idxs])
def all_checkers_in_last_quadrant():
if player == 1:
return all([(idx >= 19) for idx in checker_idxs])
else:
return all([(idx <= 6) for idx in checker_idxs])
return all([ is_moving_backmost_checker(),
all_checkers_in_last_quadrant() ])
# TODO: add switch here instead of wonky ternary in all
2018-02-14 10:48:07 +00:00
return all([ is_forward_move(),
face_value_match_move_length(),
bear_in_if_checker_on_bar(),
checkers_at_from_idx(),
no_block_at_to_idx(),
can_bear_off() if bearing_off else True ])
2018-02-15 11:41:38 +00:00
@staticmethod
def any_move_valid(board, player, roll):
for die in roll:
idxs = Board.idxs_with_checkers_of_player(board, player)
for idx in idxs:
if Board.is_move_valid(board, player, die,
(idx, idx + (die * player))):
return True
return False
2018-02-15 12:57:59 +00:00
@staticmethod
def num_of_checkers_for_player(board,player):
return player * sum([board[idx] for idx in Board.idxs_with_checkers_of_player(board, player)])
2018-02-13 13:38:49 +00:00
@staticmethod
2018-02-15 12:57:59 +00:00
def outcome(board):
def all_checkers_in_first_quadrant(player):
checker_idxs = Board.idxs_with_checkers_of_player(board, player)
if player == 1:
return all([(idx <= 6) for idx in checker_idxs])
else:
return all([(idx >= 19) for idx in checker_idxs])
winner = None
for player in [1, -1]:
if Board.idxs_with_checkers_of_player(board, player) == []:
winner = player
if winner == None:
return None
#backgammon = all_checkers_in_first_quadrant(-winner)
gammon = Board.num_of_checkers_for_player(board, -winner) == 15
score = 2 if gammon else 1
return {winner: score, -winner: -score}
@staticmethod
def apply_moves_to_board(board, player, moves):
for move in moves:
from_idx, to_idx = move.split("/")
board[int(from_idx)] -= int(player)
board[int(to_idx)] += int(player)
return board
2018-02-13 13:38:49 +00:00
@staticmethod
def calculate_legal_states(board, player, roll):
# Find all points with checkers on them belonging to the player
2018-02-05 22:36:32 +00:00
# Iterate through each index and check if it's a possible move given the roll
2018-02-14 10:48:07 +00:00
# TODO: make sure that it is not possible to do nothing on first part of
# turn and then do something with the second die
2018-02-05 21:31:34 +00:00
2018-02-13 13:38:49 +00:00
def calc_moves(board, face_value):
idxs_with_checkers = Board.idxs_with_checkers_of_player(board, player)
if len(idxs_with_checkers) == 0:
return [board]
boards = [(Board.do_move(board,
2018-02-13 13:38:49 +00:00
player,
(idx, idx + (face_value * player)))
if Board.is_move_valid(board,
player,
face_value,
(idx, idx + (face_value * player)))
else None)
for idx in idxs_with_checkers]
board_list = list(filter(None, boards)) # Remove None-values
# if len(board_list) == 0:
# return [board]
2018-02-13 13:38:49 +00:00
return board_list
# Problem with cal_moves: Method can return empty list (should always contain at least same board).
# *Update*: Seems to be fixed.
2018-02-13 13:38:49 +00:00
# ------------------
# 1. Determine if dice have identical face value
# 2. Iterate through remaining dice
legal_moves = set()
if not Board.any_move_valid(board, player, roll):
return { board }
2018-02-13 13:38:49 +00:00
dice_permutations = list(itertools.permutations(roll)) if roll[0] != roll[1] else [[roll[0]]*4]
for roll in dice_permutations:
2018-02-13 13:38:49 +00:00
# Calculate boards resulting from first move
#print("initial board: ", board)
#print("roll:", roll)
boards = calc_moves(board, roll[0])
#print("boards after first die: ", boards)
2018-02-13 13:38:49 +00:00
for die in roll[1:]:
2018-02-13 13:38:49 +00:00
# Calculate boards resulting from second move
nested_boards = [calc_moves(board, die) for board in boards]
#print("nested boards: ", nested_boards)
2018-02-13 13:38:49 +00:00
boards = [board for boards in nested_boards for board in boards]
# What the fuck
#for board in boards:
# print(board)
# print("type__:",type(board))
2018-02-13 13:38:49 +00:00
# Add resulting unique boards to set of legal boards resulting from roll
#print("printing boards from calculate_legal_states: ", boards)
legal_moves = legal_moves | set(boards)
# print("legal moves: ", legal_moves)
if len(legal_moves) == 0:
legal_moves = { tuple(board) }
return legal_moves
2018-02-13 13:38:49 +00:00
@staticmethod
def pretty(board):
def black(count):
return "\033[0;30m\033[47m{}\033[0m\033[47m".format(count)
def white(count):
return "\033[0;31m\033[47m{}\033[0m\033[47m".format(count)
temp = []
2018-02-13 13:38:49 +00:00
for x in board:
if x > 0:
2018-03-04 16:35:36 +00:00
temp.append(" {}".format(x))
elif x < 0:
2018-03-04 16:35:36 +00:00
temp.append("{}".format(x))
else: temp.append(" ")
2018-02-07 15:27:03 +00:00
2018-03-04 16:35:36 +00:00
return """
2018-02-07 23:00:18 +00:00
13 14 15 16 17 18 19 20 21 22 23 24
+--------------------------------------------------------------------------+
2018-03-06 12:01:56 +00:00
| {12}| {11}| {10}| {9}| {8}| {7}| bar -1: {0} | {6}| {5}| {4}| {3}| {2}| {1}| end -1: TODO|
|---|---|---|---|---|---|------------|---|---|---|---|---|---| |
2018-03-06 12:01:56 +00:00
| {13}| {14}| {15}| {16}| {17}| {18}| bar 1: {25} | {19}| {20}| {21}| {22}| {23}| {24}| end 1: TODO|
+--------------------------------------------------------------------------+
2018-02-07 23:00:18 +00:00
12 11 10 9 8 7 6 5 4 3 2 1
2018-03-04 16:35:36 +00:00
""".format(*temp)
2018-02-07 15:09:09 +00:00
2018-02-13 13:38:49 +00:00
@staticmethod
def do_move(board, player, move):
# Implies that move is valid; make sure to check move validity before calling do_move(...)
def move_to_bar(board, to_idx):
board = list(board)
if player == 1:
board[25] -= player
2018-02-13 13:38:49 +00:00
else:
board[0] -= player
2018-02-13 13:38:49 +00:00
board[to_idx] = 0
return board
2018-02-07 15:09:09 +00:00
2018-02-13 13:38:49 +00:00
# TODO: Moving in from bar is handled by the representation
# TODONE: Handle bearing off
2018-02-05 21:31:34 +00:00
2018-02-13 13:38:49 +00:00
from_idx = move[0]
#print("from_idx: ", from_idx)
2018-02-13 13:38:49 +00:00
to_idx = move[1]
#print("to_idx: ", to_idx)
# pdb.set_trace()
board = list(board) # Make mutable copy of board
2018-02-13 13:38:49 +00:00
# 'Lift' checker
board[from_idx] -= player
# Handle bearing off
if to_idx < 1 or to_idx > 24:
return tuple(board)
2018-02-13 13:38:49 +00:00
# Handle hitting checkers
if board[to_idx] * player == -1:
board = move_to_bar(board, to_idx)
# Put down checker
board[to_idx] += player
return tuple(board)
2018-03-06 10:43:10 +00:00
@staticmethod
def flip(board):
return tuple((-x for x in reversed(board)))