244 lines
8.5 KiB
Python
244 lines
8.5 KiB
Python
import numpy as np
|
|
import itertools
|
|
|
|
class Board:
|
|
|
|
initial_state = [ 0,
|
|
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 ]
|
|
|
|
# TODO: Remember to handle pushing other pieces to home
|
|
# TODO: Also remember that a player can't move backwards and the one
|
|
# player goes from 1-47 while the other goes from 47-1
|
|
|
|
# Remember to handle edge case when we're on the last moves and you may go
|
|
# from position 22 -> 24 on a 6, if you have no pieces behind 22. Simply
|
|
# check if any are behind if you're circle or if any are higher if you are
|
|
# X, then it can be allowed.
|
|
|
|
# Also, the check_move will also fail when you're attempting to leave a
|
|
# bar. A fix of this is of course to check if the from_idx = bar and if so,
|
|
# allow some extra stuff!
|
|
|
|
@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
|
|
|
|
# TODO: write tests
|
|
# FIXME: make sure to disallow backwards movement
|
|
# TODO: implement double roll feature (4 dice if dice are equal)
|
|
# TODO: implement moving checkers into home (bearing off)
|
|
# TODO: handle barring in a more elengant way
|
|
# TODO: allow bearing off with non-exact die roll if this is only possible
|
|
# move
|
|
# TODO: Allow not doing anything if and only if no alternatives
|
|
@staticmethod
|
|
def is_move_valid(board, player, face_value, move):
|
|
def sign(a):
|
|
return (a > 0) - (a < 0)
|
|
|
|
from_idx = move[0]
|
|
to_idx = move[1]
|
|
to_state = None
|
|
from_state = board[from_idx]
|
|
delta = to_idx - from_idx
|
|
direction = sign(delta)
|
|
bearing_off = None
|
|
|
|
# FIXME: Use get instead of array-like indexing
|
|
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
|
|
# TODO: add switch here instead of wonky ternary in all
|
|
|
|
|
|
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 ])
|
|
|
|
@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
|
|
|
|
@staticmethod
|
|
def is_winner(board, player):
|
|
return Board.idxs_with_checkers_of_player(board, player) == []
|
|
|
|
@staticmethod
|
|
def calculate_legal_states(board, player, roll):
|
|
# Find all pips with things on them belonging to the player
|
|
# Iterate through each index and check if it's a possible move given the roll
|
|
# If player is O, then check for idx + roll
|
|
# If player is X, then check for idx - roll
|
|
|
|
|
|
# TODO: make sure that it is not possible to do nothing on first part of
|
|
# turn and then do something with the second die
|
|
print("Find legal moves: ",roll,"-"*20)
|
|
|
|
def calc_moves(board, face_value):
|
|
idxs_with_checkers = Board.idxs_with_checkers_of_player(board, player)
|
|
boards = [(do_move(board,
|
|
player,
|
|
(idx, idx + (face_value * player)))
|
|
if is_move_valid(board,
|
|
player,
|
|
face_value,
|
|
(idx, idx + (face_value * player)))
|
|
else None)
|
|
for idx in idxs_with_checkers]
|
|
|
|
# If no move can be made, make sure to include current board
|
|
if is_move_valid(board, player, face_value, None):
|
|
boards.append(board)
|
|
|
|
return list(filter(None, boards)) # Remove None-values
|
|
|
|
# ------------------
|
|
# 1. Determine if dice have identical face value
|
|
# 2. Iterate through remaining dice
|
|
|
|
legal_moves = set()
|
|
|
|
dice_permutations = list(itertools.permutations(roll)) if roll[0] != roll[1] else [[roll[0]]*4]
|
|
|
|
for pair in dice_permutations:
|
|
# Calculate boards resulting from first move
|
|
boards = calc_moves(board, pair[0])
|
|
|
|
for x in dice_permutations[1:]:
|
|
# Calculate boards resulting from second move
|
|
nested_boards = [calc_moves(board, x) for board in boards]
|
|
boards = [board for boards in nested_boards for board in boards]
|
|
# Add resulting unique boards to set of legal boards resulting from roll
|
|
legal_moves = legal_moves | set(boards)
|
|
|
|
return legal_moves
|
|
|
|
@staticmethod
|
|
def pretty(board):
|
|
temp = []
|
|
for x in board:
|
|
if x >= 0:
|
|
temp.append(" {}".format(x))
|
|
else:
|
|
temp.append("{}".format(x))
|
|
|
|
return """
|
|
13 14 15 16 17 18 19 20 21 22 23 24
|
|
--------------------------------------------------------------------------
|
|
| {11}| {10}| {9}| {8}| {7}| {6}| bar -1: {24} | {5}| {4}| {3}| {2}| {1}| {0}| end -1: TODO|
|
|
|---|---|---|---|---|---|-----------|---|---|---|---|---|---|
|
|
| {12}| {13}| {14}| {15}| {16}| {17}| bar 1: {25} | {18}| {19}| {20}| {21}| {22}| {23}| end 1: TODO|
|
|
--------------------------------------------------------------------------
|
|
12 11 10 9 8 7 6 5 4 3 2 1
|
|
""".format(*temp)
|
|
|
|
@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[0] += player
|
|
else:
|
|
board[25] += player
|
|
|
|
board[to_idx] = 0
|
|
return board
|
|
|
|
# TODO: Moving in from bar is handled by the representation
|
|
# TODONE: Handle bearing off
|
|
|
|
from_idx = move[0]
|
|
to_idx = move[1]
|
|
board = list(board) # Clone board
|
|
|
|
# 'Lift' checker
|
|
board[from_idx] -= player
|
|
|
|
# Handle bearing off
|
|
if to_idx < 1 or to_idx > 24:
|
|
return board
|
|
|
|
# Handle hitting checkers
|
|
if board[to_idx] * player == -1:
|
|
board = move_to_bar(board, to_idx)
|
|
|
|
# Put down checker
|
|
board[to_idx] += player
|
|
|
|
return board
|