Skip to content

Instantly share code, notes, and snippets.

@mjtb49
Last active March 26, 2025 09:08
Show Gist options
  • Save mjtb49/51020875338082e7369e6b72d7cd3fb0 to your computer and use it in GitHub Desktop.
Save mjtb49/51020875338082e7369e6b72d7cd3fb0 to your computer and use it in GitHub Desktop.
import random
import itertools as iter
KING_SQUARE = (0, 0)
KING_THREATENS = {(-1, -1), (-1, 0), (-1, 1),
(0, 1), (0, -1),
(1, -1), (1, 0), (1, 1)}
MATING_SQUARES = {(-1, -1), (-1, 0), (-1, 1),
(0, 1), (0, 0), (0, -1),
(1, -1), (1, 0), (1, 1)}
assert len(MATING_SQUARES) == 9
assert len(KING_THREATENS) == 8
WINNING_POSITIONS = set()
def move(piece, v):
return piece[0] + v[0], piece[1] + v[1]
# return if v is a natural number multiple of u. Assumes u != (0,0)
def is_natural_multiple(v, direction):
if direction[0] != 0:
scalar = v[0] // direction[0]
else:
scalar = v[1] // direction[1]
return (scalar > 0 and (scalar * direction[0], scalar * direction[1]) == v), scalar
def rider_threatens(direction, piece_square, target_square, board):
works, distance = is_natural_multiple((target_square[0] - piece_square[0], target_square[1] - piece_square[1]), direction)
if works:
for piece in board:
if piece is not None:
a, b = is_natural_multiple((piece[0] - piece_square[0], piece[1] - piece_square[1]), direction)
if a and b < distance:
return False
return True
return False
def move_on_board(board, index, v):
ls = list(board)
ls[index] = move(board[index], v)
return tuple(ls)
class PieceType:
def __init__(self, symbol, riders=(), jumpers=(), rider_bound=None, is_royal=False, is_pawn=False):
assert len(riders) == len(set(riders))
assert len(jumpers) == len(set(jumpers))
assert len(set(jumpers).intersection(set(riders))) == 0
self.riders = set(riders)
self.jumpers = set(jumpers)
self.is_royal = is_royal
self.is_pawn = is_pawn
self.symbol = symbol
self.rider_bound = rider_bound if rider_bound is not None else 2 ** 64
# We don't need to check if the Black king is in the way.
def threatens_from(self, board, my_index, target_square):
piece_square = board[my_index]
if piece_square is None:
return False
if piece_square == target_square:
return False
if self.is_pawn:
if move(piece_square, (-1, 1)) == target_square or move(piece_square, (-1, -1)) == target_square:
return True
return False
if (target_square[0] - piece_square[0], target_square[1] - piece_square[1]) in self.jumpers:
return True
for v in self.riders:
if rider_threatens(v, piece_square, target_square, board):
return True
return False
def get_resulting_board_states(self, board, index, move_bound):
if board[index] is None:
return []
moves = []
for v in self.jumpers:
new_board = move_on_board(board, index, v)
# TODO with black pieces we need to check check and captures.
if not new_board[index] in board:
if (not self.is_royal) or (new_board[index] not in KING_THREATENS):
moves.append(new_board)
for v in self.riders:
k = 1
new_pieces = move_on_board(board, index, v)
while new_pieces[index] not in board and k < min(self.rider_bound, move_bound):
k += 1
if (not self.is_royal) or (new_pieces[index] not in KING_THREATENS):
moves.append(new_pieces)
new_pieces = move_on_board(new_pieces, index, v)
return moves
CHANCELLOR = PieceType("C", riders=[(1, 0), (-1, 0), (0, 1), (0, -1)],
jumpers=[(1, 2), (-1, 2), (2, 1), (2, -1), (1, -2), (-1, -2), (-2, 1), (-2, -1)])
ARCHBISHOP = PieceType("A", riders=[(1, 1), (-1, 1), (1, -1), (-1, -1)],
jumpers=[(1, 2), (-1, 2), (2, 1), (2, -1), (1, -2), (-1, -2), (-2, 1), (-2, -1)])
ROOK = PieceType("R", riders=[(1, 0), (-1, 0), (0, 1), (0, -1)])
QUEEN = PieceType("Q", riders=[(1, 0), (-1, 0), (0, 1), (0, -1), (1, 1), (-1, 1), (-1, -1), (1, -1)])
KNIGHT = PieceType("N", jumpers=[(1, 2), (-1, 2), (2, 1), (2, -1), (1, -2), (-1, -2), (-2, 1), (-2, -1)])
KNIGHTRIDER = PieceType("R", riders=[(1, 2), (-1, 2), (2, 1), (2, -1), (1, -2), (-1, -2), (-2, 1), (-2, -1)])
BISHOP = PieceType("B", riders=[(1, 1), (-1, 1), (-1, -1), (1, -1)])
KING = PieceType("K", jumpers=[(1, 0), (-1, 0), (0, 1), (0, -1), (1, 1), (-1, 1), (-1, -1), (1, -1)], is_royal=True)
GUARD = PieceType("G", jumpers=[(1, 0), (-1, 0), (0, 1), (0, -1), (1, 1), (-1, 1), (-1, -1), (1, -1)])
HAWK = PieceType("H", jumpers=[(2, 0), (-2, 0), (0, 2), (0, -2), (2, 2), (-2, 2), (-2, -2), (2, -2),
(3, 0), (-3, 0), (0, 3), (0, -3), (3, 3), (-3, 3), (-3, -3), (3, -3)])
PAWN = PieceType("P", jumpers=[(-1, 0)], is_pawn=True)
def get_white_moves(board, move_bound):
result = []
for i in range(len(PIECES)):
result += PIECES[i].get_resulting_board_states(board, i, move_bound)
return result
def get_white_preimages(board, move_bound):
result = []
for i in range(len(PIECES)):
if PIECES[i].is_pawn:
if board[i] is not None:
for v in PIECES[i].jumpers:
new_board = move_on_board(board, i, (-v[0], -v[1]))
if not new_board[i] in board:
result.append(new_board)
else:
result += PIECES[i].get_resulting_board_states(board, i, move_bound)
return [w for w in result if KING_SQUARE not in w and not is_threatened(KING_SQUARE, w)]
def get_black_moves(board):
result = []
for v in KING_THREATENS:
new_board = []
for p in board:
if p is not None:
p = move(p, v) # technically this moves the king along -v
new_board.append(p if p != KING_SQUARE else None)
else:
new_board.append(None)
if not is_threatened(KING_SQUARE, tuple(new_board)):
result.append(tuple(new_board))
return result
def get_black_preimages(board):
if is_threatened(KING_SQUARE, board):
return []
result = []
for v in KING_THREATENS:
new_board = []
for p in board:
if p is not None:
new_board.append(move(p, v))
else:
new_board.append(None)
if KING_SQUARE not in new_board and all(piece is None or (not piece.is_royal) or (new_board[i] not in MATING_SQUARES) for i, piece in enumerate(PIECES)):
result.append(tuple(new_board))
return result
def is_threatened(square, board):
return any(PIECES[i].threatens_from(board, i, square) for i in range(len(board)))
def is_mate(piece_positions):
return all(is_threatened(v, piece_positions) for v in MATING_SQUARES)
def is_stalemate(piece_positions):
return all(is_threatened(v, piece_positions) for v in KING_THREATENS) and not is_threatened(KING_SQUARE,
piece_positions)
def branch_value(x):
return len(get_black_moves(x))
def pieces_same_color(s1, s2):
if s1 is None or s2 is None:
return True
return (s1[0] + s1[1] + s2[0] + s2[1]) % 2 == 0
def pieces_different_color(s1, s2):
return not pieces_same_color(s1, s2)
def get_mates(n, parity_condition=lambda x: True):
coordinates = [(a, b) for a in range(-n, n + 1) for b in range(-n, n + 1)] + [None]
coordinates.remove(KING_SQUARE)
royalty = [i for i in range(len(PIECES)) if PIECES[i].is_royal]
positions = {}
for p in iter.permutations(coordinates, len(PIECES)):
if is_mate(p):
if parity_condition(p):
if all(p[i] not in MATING_SQUARES for i in royalty):
positions[p] = 0
return positions
def get_mates_faster(bound, parity_condition=lambda x: True):
coordinates = [(a, b) for a in range(-bound, bound + 1) for b in range(-bound, bound + 1)] + [None]
coordinates.remove(KING_SQUARE)
ms = list(MATING_SQUARES)
royalty = [i for i in range(len(PIECES)) if PIECES[i].is_royal]
patterns = [dict() for _ in PIECES]
keys = [set() for _ in PIECES]
for piece_index, piece in enumerate(PIECES):
for c in coordinates:
pattern = 0
for i, p in enumerate(ms):
if piece.threatens_from((c,), 0, target_square=p):
pattern |= 2**i
if pattern not in patterns[piece_index]:
patterns[piece_index][pattern] = []
keys[piece_index].add(pattern)
patterns[piece_index][pattern].append(c)
positions = dict()
for p in iter.product(*keys):
s = 0
for k in p:
s |= k
if s == 2**9 - 1:
for r in iter.product(*[patterns[i][key] for i, key in enumerate(p)]):
# print(r)
if all(pos is None or r.count(pos) == 1 for pos in r):
if is_mate(r):
if parity_condition(r):
if all(r[i] not in MATING_SQUARES for i in royalty):
positions[r] = 0
# print(positions)
return positions
def print_position(board, indent=0):
min_x = min([b[0] for b in board if b is not None] + [0]) - 2
min_y = min([b[1] for b in board if b is not None] + [0]) - 2
gap_x = max([b[0] for b in board if b is not None] + [0]) - min_x + 2 + 1
gap_y = max([b[1] for b in board if b is not None] + [0]) - min_y + 2 + 1
arr = [['_' for _ in range(gap_y)] for _ in range(gap_x)]
for i, b in enumerate(board):
if b is not None:
arr[b[0]-min_x][b[1]-min_y] = PIECES[i].symbol
arr[KING_SQUARE[0]-min_x][KING_SQUARE[1]-min_y] = 'k'
print("| " * indent + str(board))
# if indent < 4:
for l in arr:
print("| " * indent + (" ".join(l)))
def print_variations(position, white_wins, black_wins, move_bound, indent=0, black_to_move=True):
print_position(position, indent)
if black_to_move:
if black_wins[position] <= 0:
return
for p in sorted(get_black_moves(position), key=lambda x: -white_wins[x]):
assert p in white_wins
print_variations(p, white_wins, black_wins, move_bound, indent+1, black_to_move=False)
else:
if white_wins[position] <= 0:
return
found_position = False
for p in get_white_moves(position, move_bound):
if p in black_wins and black_wins[p] < white_wins[position]:
assert black_wins[p] + 1 == white_wins[position]
print_variations(p, white_wins, black_wins, move_bound, indent+1, black_to_move=True)
found_position = True
break
assert found_position
def get_best_position(positions, position_score):
best = -2**64
best_pos = None
for board in positions:
if position_score(board) > best:
best = position_score(board)
best_pos = board
return best_pos
def rotations_and_reflections(positions):
new_positions = dict()
for p in positions:
for i in range(4):
new_positions[p] = 0
p = tuple((-b, a) for (a, b) in p)
p = tuple((-a, b) for (a, b) in p)
for i in range(4):
new_positions[p] = 0
p = tuple((-b, a) for (a, b) in p)
return new_positions
def get_wins(winning_positions, move_bound, pos_score=None, should_print_variations=False):
print(f"Starting the search {len(winning_positions)}")
black_wins = winning_positions
white_wins = {}
unexplored = winning_positions
for p in winning_positions:
print_position(p)
i = 1
while len(unexplored) > 0:
# forceable_position = ((0, -1), (3, -1), (-4, -2))
# if forceable_position in white_wins:
# print("White to move is a win!")
# print_variations(forceable_position, white_wins, black_wins, move_bound, black_to_move=False)
# print(i)
# if forceable_position in black_wins:
# print("Black to move is a win!")
# print_variations(forceable_position, white_wins, black_wins, move_bound, black_to_move=True)
# print(i)
new_wins = {}
if i % 2 == 0: # Black just moved
for p in unexplored:
for q in get_black_preimages(p):
if q not in black_wins:
if all(r in white_wins or is_mate(r) for r in get_black_moves(q)):
# print("HI")
new_wins[q] = i
unexplored = new_wins
black_wins.update(new_wins)
print(f"{i} {len(new_wins)}")
if len(new_wins) < 10:
print("-" * 80)
for p in new_wins:
print_position(p)
print("-"*80)
pos_to_print = get_best_position(new_wins, pos_score) if pos_score is not None else random.choice(list(new_wins.keys()))
if should_print_variations:
print_variations(pos_to_print, white_wins, black_wins, move_bound, black_to_move=True)
else:
print_position(pos_to_print)
else: # White just moved
for p in unexplored:
for q in get_white_preimages(p, move_bound):
if q not in white_wins:
new_wins[q] = i
unexplored = new_wins
white_wins.update(new_wins)
print(f"{i} {len(new_wins)}")
pos_to_print = get_best_position(new_wins, pos_score) if (pos_score is not None) else random.choice(list(new_wins.keys()))
if should_print_variations:
print_variations(pos_to_print, white_wins, black_wins, move_bound, black_to_move=False)
else:
print_position(pos_to_print)
i += 1
PIECES = [QUEEN, PAWN, KING]
if __name__ == "__main__":
def score(position):
return min(p[1] for p in position[2:] if p is not None)
get_wins(get_mates_faster(5), 20, pos_score=score, should_print_variations=False)
# get_wins(rotations_and_reflections({((-1, -1), (2, -1), (-1, 3)), ((-1, -1), (2, -1), (-1, -3)), ((0, -1), (3, -1), (-4, -2))}), 20, pos_score=score, should_print_variations=False)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment