Skip to content

Commit

Permalink
Add Chinese checkers
Browse files Browse the repository at this point in the history
  • Loading branch information
jojolebarjos committed Feb 25, 2024
1 parent de84b1e commit 10f82b8
Show file tree
Hide file tree
Showing 7 changed files with 237 additions and 19 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/pytest.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ jobs:
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install . pytest ruff
pip install . pytest regex ruff
- name: Check code formatting
run: |
Expand Down
2 changes: 1 addition & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ include(FetchContent)
FetchContent_Declare(
game
GIT_REPOSITORY https://github.com/jojolebarjos/board-game-simulator.git
GIT_TAG e475bb908614507709fb651321942df09a050ed4
GIT_TAG 67055d0af27cf3b83864e92b7142a8405143b1bd
GIT_SHALLOW 1
)
FetchContent_MakeAvailable(game)
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ cmake.build-type = "RelWithDebInfo"

[project]
name = "game"
version = "0.0.1"
version = "0.0.2"
requires-python = ">=3.10"
dependencies = [
"numpy",
Expand Down
4 changes: 3 additions & 1 deletion src/module.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
#include "./object.hpp"
#include "./game.hpp"

#include <game/chinese_checkers.hpp>
#include <game/connect.hpp>


Expand All @@ -10,7 +11,8 @@ namespace {

bool define(PyObject* module) {
return
Game<game::connect::Traits<6, 7, 4>>::define(module, "Connect");
Game<game::chinese_checkers::Traits>::define(module, "ChineseCheckers") &&
Game<game::connect::Traits<6, 7, 4>>::define(module, "Connect4");
}


Expand Down
31 changes: 17 additions & 14 deletions src/object.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,8 @@ Object to_object(std::string const& value) noexcept {
template<typename T, size_t... Shape>
Object to_object(game::tensor<T, Shape...> const& value) noexcept {
static_assert(as_dtype<T> != NPY_NOTYPE);
Object array = PyArray_SimpleNew(value.ndim, (npy_intp const*)value.shape, as_dtype<T>);
static_assert(sizeof(npy_intp) == sizeof(size_t));
Object array = PyArray_SimpleNew(value.shape.size(), (npy_intp const*)value.shape.data(), as_dtype<T>);
if (array) {
void const* src = (void const*)&value;
void* dst = PyArray_DATA((PyArrayObject*)array.value);
Expand All @@ -141,19 +142,20 @@ Object to_object(nlohmann::json const& value) noexcept;

template<typename... T>
Object to_object(std::tuple<T...> const& value) noexcept {
size_t size = sizeof...(T);
Object tuple = PyTuple_New(size);
if (tuple) {
size_t i = 0;
auto set = [&](auto v) {
PyObject* o = to_object(v).release();
PyTuple_SET_ITEM(tuple.value, i++, o);
return o;
};
if (!std::apply(set, value))
return nullptr;
}
return tuple;

auto f = [](auto const&... v) -> Object {
size_t size = sizeof...(v);
Object tuple = PyTuple_New(size);
if (tuple) {
size_t i = 0;
PyObject* o;
if (!((o = to_object(v).release(), PyTuple_SET_ITEM(tuple.value, i++, o), o) && ...))
return nullptr;
}
return tuple;
};

return std::apply(f, value);
}


Expand Down Expand Up @@ -308,6 +310,7 @@ nlohmann::json from_object<nlohmann::json>(Object const& object) {
}

// TODO tuple?
// TODO numpy array?

if (PyUnicode_Check(object.value))
return from_object<std::string>(object.value);
Expand Down
213 changes: 213 additions & 0 deletions tests/test_chinese_checkers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
import regex as re

import numpy as np

from game import ChineseCheckersState as State


def parse(string):
# Extract rows
lines = []
min_indent = len(string)
for line in string.split("\n"):
line = line.rstrip("\r ")
if line:
match = re.search(r"[\.OX\*]", line)
assert match
indent = match.start()
min_indent = min(min_indent, indent)
lines.append(line + " ")

# Parse rows
assert len(lines) == 17
PATTERN = r"( *\[?)([\.OX\*][ \]\[])+"
pieces = [[], []]
selections = []
targets = []
for y, line in enumerate(lines):
match = re.fullmatch(PATTERN, line)
assert match
x0 = len(match.group(1)) - min_indent
steps = match.captures(2)
for i, step in enumerate(steps):
x = x0 + i * 2

# Convert to grid coordinates
assert (x + y) % 2 == 0
x_grid = 6 + (x - y) // 2
y_grid = 18 - (x + y) // 2
coordinate = [x_grid, y_grid]

if step[0] == "O":
pieces[0].append(coordinate)
if step[0] == "X":
pieces[1].append(coordinate)
if step[0] == "*":
targets.append(coordinate)
if step[1] == "]":
selections.append(coordinate)

# Sort pieces, so that it is consistent with internal representation
pieces[0].sort()
pieces[1].sort()
selections.sort()
targets.sort

return pieces, selections, targets


def encode(state_or_action):
if isinstance(state_or_action, State):
state = state_or_action
action = None
elif isinstance(state_or_action, Action):
action = state_or_action
state = action.state
else:
raise TypeError

# TODO render state as string, for easier debugging
raise NotImplementedError


def assert_state(state, string):
state_dict = state.to_json()
pieces, selections, targets = parse(string)

# First, check that all pieces are at the expected location
assert state_dict["pieces"] == pieces

# Then, if some pieces are highlighted, check actions
selected_action = None
if len(selections) > 0:
piece_selection = None
target_selection = None
for selection in selections:
if selection in targets:
assert not target_selection
target_selection = selection
else:
assert not piece_selection
piece_selection = selection

# Get current player
player = 0 if piece_selection in pieces[0] else 1
assert player == state_dict["player"]

# Search for related actions
destinations = []
for action in state.actions:
action_dict = action.to_json()
index = action_dict["index"]
source = state_dict["pieces"][player][index]
destination = [action_dict["x"], action_dict["y"]]
if piece_selection == source:
destinations.append(destination)
if target_selection == destination:
assert not selected_action
selected_action = action

# This should be an exact match
targets.sort()
destinations.sort()
assert targets == destinations

if target_selection:
assert selected_action

return selected_action


def assert_string(string):
pieces, selections, targets = parse(string)
[selection] = selections

assert len(pieces[0]) == 10
assert len(pieces[1]) == 10

player = 0 if selection in pieces[0] else 1

state_dict = {
"pieces": pieces,
"player": player,
"winner": -1,
}
state = State.from_json(state_dict)

return assert_state(state, string)


def test_individual():
assert_string(
"""
X
X X
X X X
X . . .
. . . . . . . . . . . . .
. . . . . . . . . . . .
. . . . . . . . . . .
. * . . . . . . . .
. X . O . . . . .
. . * . . . . . . .
. . . X . * * . . . .
. . . . * X[O]* . . . .
. . . . . . O * . . . . .
O * . O
O . O
O O
O
"""
)


def test_sequence():
state = State.sample_initial_state()

# Turn 1
string = """
X
X X
X X X
X X X X
. . . . . . . . . . . . .
. . . . . . . . . . . .
. . . . . . . . . . .
. . . . . . . . . .
. . . . . . . . .
. . . . . . . . . .
. . . . . . . . . . .
. . . . . . . . . . . .
. . . . . *[*]. . . . . .
O[O]O O
O O O
O O
O
"""
action = assert_state(state, string)
state = action.sample_next_state()

# Turn 2
string = """
X
X X
[X]X X
X X X X
. . . . * .[*]. . . . . .
. . . . . . . . . . . .
. . . . . . . . . . .
. . . . . . . . . .
. . . . . . . . .
. . . . . . . . . .
. . . . . . . . . . .
. . . . . . . . . . . .
. . . . . . O . . . . . .
O . O O
O O O
O O
O
"""
action = assert_state(state, string)
state = action.sample_next_state()

# TODO more turns
2 changes: 1 addition & 1 deletion tests/test_connect.py → tests/test_connect4.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import numpy as np

from game import ConnectState as State
from game import Connect4State as State


def to_grid(literal):
Expand Down

0 comments on commit 10f82b8

Please sign in to comment.