Skip to content

Commit

Permalink
Calculate legal moves in Chess Golf, for #27.
Browse files Browse the repository at this point in the history
  • Loading branch information
donkirkby committed Dec 24, 2023
1 parent 122d590 commit 2a9bbfb
Show file tree
Hide file tree
Showing 4 changed files with 369 additions and 2 deletions.
15 changes: 15 additions & 0 deletions docs/journal/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,18 @@ solitaire.

I also converted the scripts for publishing the Donimoes rules as PDF and
website to this project.

### Nov 2023 - Adrenaline Chess
Started with Manna Chess, where manna drops randomly after each capture, and
gives the piece carrying it one extra king move. Players found the random drops
annoying, so we switched to Adrenaline Chess, where you choose which opposing
piece to give an adrenaline boost to after each capture. Made for some
interesting tactics of giving the boost to a piece you could capture it back
from.

### Dec 2023 - Chess Golf
Started with Neighbour Chess Solitaire, where the pieces borrow their
neighbour's move, and then merged it with Ricochet Robots to make Chess Golf.
Struggled with the mechanics of automatically adding pieces back to the board
and what to do when a colour has no neighbouring pairs. Fixed it by adding
special moves that can be done any time, but are expensive.
98 changes: 98 additions & 0 deletions golf.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import typing
from collections import Counter
from textwrap import dedent

import chess

from board_parser import parse_board


class GolfState:
def __init__(self, text: str):
board_lines = text.splitlines()
board_text = '\n'.join(board_lines[:8])
self.board = parse_board(board_text)
self.taking = None
self.taken = Counter()
self.chosen = Counter()
for line in board_lines[8:]:
if line.startswith('chosen:'):
chosen_text = line[7:].strip()
self.chosen = Counter(chess.Piece.from_symbol(c)
for c in chosen_text)
elif line.startswith('taking:'):
self.taking = chess.parse_square(line[7:].strip())
elif line.startswith('taken:'):
taken_text = line[6:].strip()
self.taken = Counter(chess.Piece.from_symbol(c)
for c in taken_text)
else:
label = line.split(':')[0]
raise ValueError(f'Unknown golf label: {label!r}.')

def find_moves(self):
for square, piece in self.board.piece_map().items():
board_copy = self.board.copy()
moving_piece = board_copy.piece_at(square)
can_capture = moving_piece in self.chosen
if can_capture and self.taking is not None:
can_capture = square == self.taking
if not can_capture:
target_counts = None
else:
target_counts = self.chosen - self.taken
target_counts[moving_piece] -= 1
for neighbour_type in get_neighbour_types(self.board, square):
for turn in (chess.WHITE, chess.BLACK):
fake_moving_piece = chess.Piece(neighbour_type, turn)
board_copy.set_piece_at(square, fake_moving_piece)
board_copy.turn = turn
from_bitboard = 1 << square
for move in board_copy.generate_pseudo_legal_moves(
from_bitboard):
is_capture = board_copy.is_capture(move)
if not is_capture:
if turn == chess.BLACK:
continue
else:
if not can_capture:
continue
captured_piece = self.board.piece_at(move.to_square)
if target_counts[captured_piece] <= 0:
continue

yield move


def get_neighbour_types(board: chess.Board,
square: chess.Square) -> typing.Set[chess.PieceType]:
neighbour_types = set()
king_board = chess.Board()
king_board.set_piece_at(square, chess.Piece(chess.KING, chess.WHITE))
start_piece = board.piece_at(square)
for neighbour_square in king_board.attacks(square):
neighbour = board.piece_at(neighbour_square)
if neighbour is None:
continue
if neighbour.color != start_piece.color:
continue
neighbour_types.add(neighbour.piece_type)
return neighbour_types


def main():
board = parse_board(dedent("""\
. . B . . . R .
. . . . . k . .
. r . n . N . .
. r . . b . . .
R . . . . . q .
. B . n . N . .
b . . . . . . K
. . . . . Q . ."""))
for square, piece in board.piece_map().items():
print(square, piece)


if __name__ == '__main__':
main()
4 changes: 2 additions & 2 deletions raw_rules/rules.md
Original file line number Diff line number Diff line change
Expand Up @@ -951,8 +951,8 @@ hearts and spades.
Know some other lighthearted chess variants? Ideas to share? Get in touch at
[https://donkirkby.github.io/chess-kit][github].

Zombie Chess, Masquerade Chess, Crowded House, and Cooperative Chess are
original games designed by [Don Kirkby][don].
Zombie Chess, Masquerade Chess, Chess Golf, Crowded House, and Cooperative Chess
are original games designed by [Don Kirkby][don].

[github]: https://donkirkby.github.io/chess-kit
[don]: https://donkirkby.github.io/
254 changes: 254 additions & 0 deletions tests/test_golf.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,254 @@
from collections import Counter
from textwrap import dedent

import chess
import pytest

from board_parser import parse_board
from golf import get_neighbour_types, GolfState


def test_get_neighbour_types():
board = parse_board(dedent("""\
. . B . . . R .
. . . . . . . .
. r . n . Q . .
. r . . b k . .
R . . . . . q .
. B . n . N . .
b . . . . . . K
. . . . . N . ."""))
expected_neighbour_types = {chess.KNIGHT, chess.KING}

neighbour_types = get_neighbour_types(board, chess.E5)

assert board.piece_at(chess.E5) == chess.Piece(chess.BISHOP, chess.BLACK)
assert neighbour_types == expected_neighbour_types


def test_new_golf_state():
start_text = dedent("""\
. . B . . . R .
. . . . . . . .
. r . n . Q . .
. r . . b k . .
R . . . . . q .
. B . n . N . .
b . . . . . . K
. . . . . N . .
chosen: Bbq""")
expected_board = parse_board(start_text.split('chosen')[0])

state = GolfState(start_text)

assert state.board == expected_board
assert state.taking is None
assert state.taken == Counter()
assert state.chosen == Counter([chess.Piece(chess.BISHOP, chess.WHITE),
chess.Piece(chess.BISHOP, chess.BLACK),
chess.Piece(chess.QUEEN, chess.BLACK)])


def test_captured_golf_state():
start_text = dedent("""\
. . B . . . R .
. . . . . . . .
. r . n . Q . .
. r . . b k . .
R . . . . . q .
. . . n . N . .
B . . . . . . K
. . . . . N . .
chosen: Bbq
taking: a2
taken: b""")

state = GolfState(start_text)

assert state.taking == chess.A2
assert state.taken == Counter([chess.Piece(chess.BISHOP, chess.BLACK)])
assert state.chosen == Counter([chess.Piece(chess.BISHOP, chess.WHITE),
chess.Piece(chess.BISHOP, chess.BLACK),
chess.Piece(chess.QUEEN, chess.BLACK)])


def test_bogus_golf_state():
start_text = dedent("""\
. . B . . . R .
. . . . . . . .
. r . n . Q . .
. r . . b k . .
R . . . . . q .
. . . n . N . .
B . . . . . . K
. . . . . N . .
bogus: Bbq""")

with pytest.raises(ValueError, match=r"Unknown golf label: 'bogus'."):
GolfState(start_text)


def test_find_moves_not_chosen_taker():
state = GolfState(dedent("""\
n k . . . . R N
. . . B r . n .
. . q . . B . .
. . . . . . . b
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
chosen: Bb"""))

expected_moves = {chess.Move(chess.A8, chess.A7), # knight uses king
chess.Move(chess.A8, chess.B7),
chess.Move(chess.B8, chess.A6), # king uses knight
chess.Move(chess.G8, chess.H6), # rook uses knight
chess.Move(chess.H8, chess.H7), # knight uses rook
chess.Move(chess.H8, chess.H6)}

moves = list(state.find_moves())

assert len(moves) == len(expected_moves)
assert set(moves) == expected_moves


# noinspection DuplicatedCode
def test_find_moves_not_chosen_taken():
state = GolfState(dedent("""\
. . . . . . R B
. . . . . b . .
. . . . . . . .
. . . . . . . n
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
chosen: Bb"""))

expected_moves = {chess.Move(chess.H8, chess.H7), # bishop uses rook
chess.Move(chess.H8, chess.H6),
chess.Move(chess.G8, chess.H7)} # rook uses bishop

moves = list(state.find_moves())

assert len(moves) == len(expected_moves)
assert set(moves) == expected_moves


# noinspection DuplicatedCode
def test_find_moves_both_chosen():
state = GolfState(dedent("""\
. . . . . . R B
. . . . . n . .
. . . . . . . .
. . . . . . . b
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
chosen: Bb"""))

expected_moves = {chess.Move(chess.H8, chess.H7), # bishop uses rook
chess.Move(chess.H8, chess.H6),
chess.Move(chess.H8, chess.H5),
chess.Move(chess.G8, chess.H7)} # rook uses bishop

moves = list(state.find_moves())

assert len(moves) == len(expected_moves)
assert set(moves) == expected_moves


# noinspection DuplicatedCode
def test_find_moves_same_type_not_allowed():
state = GolfState(dedent("""\
. . . . . . R B
. . . . . b . .
. . . . . . . .
. . . . . . . B
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
chosen: Bb"""))

expected_moves = {chess.Move(chess.H8, chess.H7), # bishop uses rook
chess.Move(chess.H8, chess.H6),
chess.Move(chess.G8, chess.H7)} # rook uses bishop

moves = list(state.find_moves())

assert len(moves) == len(expected_moves)
assert set(moves) == expected_moves


# noinspection DuplicatedCode
def test_find_moves_same_type_allowed():
state = GolfState(dedent("""\
. . . . . . R B
. . . . . b . .
. . . . . . . .
. . . . . . . B
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
chosen: BB"""))

expected_moves = {chess.Move(chess.H8, chess.H7), # bishop uses rook
chess.Move(chess.H8, chess.H6),
chess.Move(chess.H8, chess.H5),
chess.Move(chess.G8, chess.H7)} # rook uses bishop

moves = list(state.find_moves())

assert len(moves) == len(expected_moves)
assert set(moves) == expected_moves


# noinspection DuplicatedCode
def test_find_moves_single_piece_may_capture():
state = GolfState(dedent("""\
B R . . . . R B
n . q . . k . n
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
chosen: Bbn
taking: a8
taken: b"""))

expected_moves = {chess.Move(chess.A8, chess.A7)} # bishop uses rook

moves = list(state.find_moves())

assert len(moves) == len(expected_moves)
assert set(moves) == expected_moves


# noinspection DuplicatedCode
def test_find_moves():
state = GolfState(dedent("""\
B R . . . . . .
r . . . . . . .
. . . k . . . .
. . . . . . . .
. . . . . q . .
. . . . K . . .
. . . . . . b .
. . . . Q . . n
chosen: Bbr
taking: a8
taken: r"""))

expected_moves = {chess.Move(chess.B8, chess.C7), # rook uses bishop
chess.Move(chess.G2, chess.H4)} # bishop uses knight

moves = list(state.find_moves())

assert len(moves) == len(expected_moves)
assert set(moves) == expected_moves

0 comments on commit 2a9bbfb

Please sign in to comment.