/*
This file is part of ct.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
General Public License for more details.
You should have received a copy of the GNU General Public License
along with ct. If not, see .
*/
#include "tak.h"
#include
// ===================================================================
// Helpers
// ===================================================================
#define NUM_SQUARES(board_size) (board_size * board_size)
tak_state_p new_tak_state(const uint8_t board_size) {
tak_state_p state = malloc(sizeof(struct tak_state_s));
reset_state(state, board_size);
return state;
}
void free_tak_state(tak_state_p state) { free(state); }
// ===================================================================
// General state stuff
// ===================================================================
void reset_state(tak_state_p state, const uint8_t new_board_size) {
if (new_board_size == 6) {
state->board_size = 6;
state->white_count = 128 | 30;
state->black_count = 128 | 30;
} else {
state->board_size = 5;
state->white_count = 128 | 21;
state->black_count = 128 | 21;
}
state->ply = 0;
state->won = 0xFF; // i may live to regret this hack
state->current_colour = C_BLACK;
for (uint8_t k = 0; k < NUM_SQUARES(new_board_size); k++) {
state->celldat[k] = 0;
}
}
void next_ply(tak_state_p state) {
state->ply++;
if (state->ply == 2) {
state->current_colour = C_WHITE;
} else {
if (state->current_colour == C_BLACK)
state->current_colour = C_WHITE;
else
state->current_colour = C_BLACK;
}
}
// ===================================================================
// Placing stones
// ===================================================================
enum ACT_RESULT try_place(tak_state_p state, const int8_t location,
const enum COLOUR colour,
const enum STONE_VARIANT stone) {
// Game is over?
if (state->won < 0xFF)
return GAME_END;
// Can't place on an occupied square
if (COUNT_AT(state, location)) {
return ACT_ILLEGAL;
} else {
switch (stone) {
case STONE_STANDING:
if (state->ply < 2)
return ACT_ILLEGAL;
// behold the magic GCC comment which defeates
// -Wimplicit-fallthrough:
// fall through
case STONE_FLAT: {
if (colour == C_BLACK) {
if (state->black_count & 127)
state->black_count--;
else
return ACT_ILLEGAL;
} else {
if (state->white_count & 127)
state->white_count--;
else
return ACT_ILLEGAL;
}
break;
}
case STONE_CAPSTONE: {
if (state->ply < 2)
return ACT_ILLEGAL;
if (colour == C_BLACK) {
if (state->black_count & 128)
state->black_count &= 127;
else
return ACT_ILLEGAL;
} else {
if (state->white_count & 128)
state->white_count &= 127;
else
return ACT_ILLEGAL;
}
break;
}
}
state->colours[location] = colour;
state->celldat[location] = NUM_INC | stone;
return ACT_OK;
}
}
// ===================================================================
// Moving stacks
// ===================================================================
static inline void push_stones(tak_state_p state, const int8_t location,
const uint8_t count, const uint8_t new_colours,
const enum STONE_VARIANT top_stone) {
state->colours[location] = (state->colours[location] << count) | new_colours;
state->celldat[location] =
top_stone |
((state->celldat[location] + ((count << NUM_SHIFT))) & NUM_MASK);
}
enum ACT_RESULT try_move(tak_state_p state, const int8_t location,
const enum MOVE_DIRECTION direction,
const uint8_t steps, const uint8_t drops[5]) {
// Game is over?
if (state->won < 0xFF)
return GAME_END;
// Can't do this
if (steps == 0 || steps > state->board_size)
return ACT_ILLEGAL;
// Check for stones at all
const uint8_t avail = COUNT_AT(state, location);
if (avail == 0)
return ACT_ILLEGAL;
// Does the current player own the pile?
if ((state->colours[location] & 1) != state->current_colour)
return ACT_ILLEGAL;
// Is the desired direction and count on the board?
int8_t delta = 0;
switch (direction) {
case M_UP: {
delta = +state->board_size;
if (location + delta * steps > NUM_SQUARES(state->board_size))
return ACT_ILLEGAL;
break;
};
case M_DOWN: {
delta = -state->board_size;
if (location + delta * steps < 0)
return ACT_ILLEGAL;
break;
};
case M_RIGHT: {
delta = +1;
if ((location + steps * delta) / state->board_size >
location / state->board_size)
return ACT_ILLEGAL;
break;
};
case M_LEFT: {
delta = -1;
// We need the extra check for zero here because, irritatingly,
// -1 / board_size == 1 / board_size
if ((location + steps * delta < 0) ||
((location + steps * delta) / state->board_size <
location / state->board_size))
return ACT_ILLEGAL;
break;
};
};
// For every square in the direction
uint8_t total = 0;
for (uint8_t k = 0; k < steps; k++) {
// Can't drop 0 anywhere because we're past the first square
if (drops[k] == 0)
return ACT_ILLEGAL;
// Can't drop more than BOARD_SIZE stones in a square
if (drops[k] > state->board_size)
return ACT_ILLEGAL;
// Check for overflows
if (COUNT_AT(state, location + (k + 1) * delta) + drops[k] > 0x0F)
return ACT_OVERFLOW;
// Check for capstone
if (STONE_AT(state, location + (k + 1) * delta) == STONE_CAPSTONE)
return ACT_ILLEGAL;
// Check for wall
if ((STONE_AT(state, location + (k + 1) * delta) == STONE_STANDING)
// If not last drop, or not dropping just one, or not a cap
&& ((k + 1 < steps) || (drops[k] != 1) ||
(STONE_AT(state, location) != STONE_CAPSTONE)))
return ACT_ILLEGAL;
total += drops[k];
}
// Can't ask to move 0, more than board_size, or stones available
if ((total == 0) || (total > state->board_size) || (total > avail))
return ACT_ILLEGAL;
// Nothing illegal, do it. First we add the stones to the
// destination squares
uint8_t j = total;
for (uint8_t k = 0; k < steps; k++) {
j -= drops[k];
push_stones(state, location + (k + 1) * delta, drops[k],
(state->colours[location] >> j) & (0xFFFF >> (0x10 - drops[k])),
(k == steps - 1) ? STONE_AT(state, location) : STONE_FLAT);
}
// Then we drop them from the source
state->colours[location] >>= total;
const uint8_t dec_count = state->celldat[location] - (total << NUM_SHIFT);
state->celldat[location] = dec_count & NUM_MASK;
return ACT_OK;
}
// ===================================================================
// Checking win
// ===================================================================
// Check for the presence of a road connecting opposite sides
static enum WIN_TYPE check_road_colour(tak_state_p state,
const enum COLOUR colour) {
int component[NUM_SQUARES(state->board_size)],
touching[NUM_SQUARES(state->board_size)];
/*
We're doing a poor version of a disjoint set data structure to
track and merge connected components. The array `component' stores
indices to the representative cells of each connected component. A
cell is representative if component[cell] = cell. We track only
whether representatives are touching sides, and do a 2-dimensional
DP approach to forming these from the board.
Note: we don't track the rank/size of each tree, because we're not
interested in good asymptotic complexity in the size of the board,
merely good performance for a single board size in practice. For
the same reason we also don't do path flattening/halving or
anything.
*/
for (int k = 0; k < NUM_SQUARES(state->board_size); k++) {
component[k] = k; // every square is in its own connected
// component initially
touching[k] = 0; // and not connected to any sides
}
// touching is the bit mask for connectivity,
// bottom | top | left | right
// 1 2 4 8
int touch = 5;
for (int row = 0; row < state->board_size; row++) {
for (int col = 0; col < state->board_size; col++) {
const int cur = THE_COORDS(state->board_size, col, row);
if (COUNT_AT(state, cur) && (state->colours[cur] & 1) == colour &&
STONE_AT(state, cur) != STONE_STANDING) {
// do we have any neighbours to the left and below?
const int left_neighbour =
((cur % state->board_size > 0) &&
(COUNT_AT(state, cur - 1)) // wont ever be out of bounds
&& ((state->colours[cur - 1] & 1) == colour) &&
(STONE_AT(state, cur - 1) != STONE_STANDING));
const int lowr_neighbour =
((cur >= state->board_size) &&
(COUNT_AT(state, cur - state->board_size)) &&
((state->colours[cur - state->board_size] & 1) == colour) &&
(STONE_AT(state, cur - state->board_size) != STONE_STANDING));
// always take the component of the lower neighbour if
// possible, failing that take the left neighbour, otherwise
// we're not yet connected, so update our own component.
if (lowr_neighbour) {
// look up the root of the lower neighbour
int root = cur - state->board_size;
while (root != component[root])
root = component[root];
// join the set
component[cur] = root;
if (touch) {
// something new
touching[root] |= touch;
// are we done?
if ((touching[root] & 0x3) == 0x3 || (touching[root] & 0xC) == 0xC)
return (colour == C_BLACK) ? WIN_ROAD_BLACK : WIN_ROAD_WHITE;
}
// if we also have a left neighbour then we should `merge'
// sets, and here we assume that the left neighbour set is
// always smaller (may not be) for the direction of merge
if (left_neighbour) {
int left_root = cur - 1;
while (left_root != component[left_root])
left_root = component[left_root];
// merge
const int left_touch = touching[left_root];
if (left_touch) {
touching[root] |= left_touch;
if ((touching[root] & 0x3) == 0x3 ||
(touching[root] & 0xC) == 0xC)
return (colour == C_BLACK) ? WIN_ROAD_BLACK : WIN_ROAD_WHITE;
}
component[left_root] = root;
}
} else if (left_neighbour) {
int root = cur - 1;
while (root != component[root])
root = component[root];
component[cur] = root;
if (touch) {
touching[root] |= touch;
if ((touching[root] & 0x3) == 0x3 || (touching[root] & 0xC) == 0xC)
return (colour == C_BLACK) ? WIN_ROAD_BLACK : WIN_ROAD_WHITE;
}
} else if (touch) {
// we had no left or lower neighbour, so we're on our own
touching[cur] = touch;
}
}
if (col + 2 == state->board_size)
touch |= 8;
else
touch &= 0x3;
}
if (row + 2 == state->board_size)
touch = 6;
else
touch = 4;
}
return 0xFF;
}
enum WIN_TYPE check_win(tak_state_p state) {
// Road?
enum WIN_TYPE rb, rw;
rb = check_road_colour(state, C_BLACK);
rw = check_road_colour(state, C_WHITE);
if (rb == WIN_ROAD_BLACK && rw == WIN_ROAD_WHITE) {
return (state->ply & 1) ? rb : rw; // Dragons
} else if (rw == WIN_ROAD_WHITE) {
return rw;
} else if (rb == WIN_ROAD_BLACK) {
return rb;
}
// Do we do a flat count?
int8_t total = 0, board_full = 1;
for (uint8_t k = 0; k < NUM_SQUARES(state->board_size); k++) {
if (COUNT_AT(state, k) == 0) {
board_full = 0;
} else if (STONE_AT(state, k) == STONE_FLAT) {
total += ((state->colours[k] & 1) == C_BLACK) ? +1 : -1;
}
}
if (state->black_count == 0 || state->white_count == 0 || board_full) {
// Decide based on count
if (total > 0)
return WIN_FLAT_BLACK;
else if (total < 0)
return WIN_FLAT_WHITE;
else
return WIN_DRAW;
}
return 0xFF;
}
// ===================================================================
// PTN place parser
// ===================================================================
#define ASSERT_NONEMPTY \
{ \
if (ptn == NULL || *ptn == 0) \
return PTN_INVALID; \
}
#define ASSERT_MORE \
{ \
if (*ptn == 0) \
return PTN_INVALID; \
}
enum PTN_RESULT parse_place(const uint8_t board_size, char *ptn,
uint8_t *out_location,
enum STONE_VARIANT *out_stone) {
ASSERT_NONEMPTY;
*out_stone = STONE_FLAT;
switch (*ptn) {
case 'C': {
ptn++;
*out_stone = STONE_CAPSTONE;
break;
};
case 'S': {
ptn++;
*out_stone = STONE_STANDING;
break;
};
case 'F': {
ptn++;
break;
};
}
ASSERT_MORE;
if ((*ptn < 'a') || (*ptn > '`' + board_size))
return PTN_INVALID;
*out_location = *ptn - 'a';
ptn++;
ASSERT_MORE;
if ((*ptn < '1') || (*ptn > board_size + '0'))
return PTN_INVALID;
*out_location += board_size * (*ptn - '1');
if (*(++ptn) > 0)
return PTN_INVALID;
return PTN_OK;
}
// ===================================================================
// PTN move parser
// ===================================================================
enum PTN_RESULT parse_move(const uint8_t board_size, char *ptn,
uint8_t *out_location,
enum MOVE_DIRECTION *out_direction,
uint8_t *out_steps, uint8_t out_drops[5]) {
ASSERT_NONEMPTY;
uint8_t picked_up = 1;
// Optionally indicate how many stones picked up
if ((*ptn >= '1') && (*ptn <= '0' + board_size)) {
picked_up = *ptn - '0';
ptn++;
ASSERT_MORE;
}
// column must be on the board
if ((*ptn < 'a') || (*ptn > '`' + board_size))
return PTN_INVALID;
*out_location = *ptn - 'a';
ptn++;
ASSERT_MORE;
// row must be on the board
if ((*ptn < '1') || (*ptn > board_size + '0'))
return PTN_INVALID;
*out_location += board_size * (*ptn - '1');
ptn++;
ASSERT_MORE;
// valid direction
switch (*ptn) {
case '+': {
*out_direction = M_UP;
break;
}
case '-': {
*out_direction = M_DOWN;
break;
}
case '<': {
*out_direction = M_LEFT;
break;
}
case '>': {
*out_direction = M_RIGHT;
break;
}
default:
return PTN_INVALID;
}
// Handle the case 'n' as
// 'nn' for convenience, if n is omitted
// assume n = 1
ptn++;
if (*ptn == 0) {
*out_steps = 1;
out_drops[0] = picked_up;
return PTN_OK;
}
// Parse the drops in each subsequent square
*out_steps = 0;
uint8_t total = 0;
while (*ptn) {
// can't drop more than the carry limit, or less than 1
if ((*ptn < '1') || (*ptn > '0' + board_size))
return PTN_INVALID;
// can't move more than the size of the board in any direction
if ((*out_steps + 1 >= board_size) && *ptn)
return PTN_INVALID;
out_drops[*out_steps] = *ptn - '0';
total += out_drops[*out_steps];
*out_steps += 1;
ptn++;
}
// Mismatch between number of stones picked up and total dropped
if (total != picked_up)
return PTN_INVALID;
return PTN_OK;
}
// ===================================================================
// Generate PTN for place
// ===================================================================
void generate_place(const uint8_t board_size, const uint8_t in_location,
const enum STONE_VARIANT in_stone, char out_ptn[4]) {
switch (in_stone) {
case STONE_FLAT: {
break;
}
case STONE_STANDING: {
*out_ptn = 'S';
out_ptn++;
break;
}
case STONE_CAPSTONE: {
*out_ptn = 'C';
out_ptn++;
break;
}
}
*out_ptn = 'a' + (in_location % board_size);
out_ptn++;
*out_ptn = '1' + (in_location / board_size);
out_ptn++;
*out_ptn = 0;
}
// ===================================================================
// Generate PTN for move
// ===================================================================
void generate_move(const uint8_t board_size, const uint8_t in_location,
const enum MOVE_DIRECTION in_direction,
const uint8_t in_steps, const uint8_t in_drops[5],
char out_ptn[10]) {
uint8_t total = 0;
for (uint8_t k = 0; k < in_steps; k++)
total += in_drops[k];
if (total > 1) {
*out_ptn = '0' + total;
out_ptn++;
}
*out_ptn = 'a' + (in_location % board_size);
out_ptn++;
*out_ptn = '1' + (in_location / board_size);
out_ptn++;
switch (in_direction) {
case M_UP: {
*out_ptn = '+';
break;
}
case M_DOWN: {
*out_ptn = '-';
break;
}
case M_LEFT: {
*out_ptn = '<';
break;
}
case M_RIGHT: {
*out_ptn = '>';
break;
}
};
out_ptn++;
for (uint8_t k = 0; (total > 1) && (k < in_steps); k++) {
*out_ptn = '0' + in_drops[k];
out_ptn++;
}
*out_ptn = 0;
}
// ===================================================================
// Driver
// ===================================================================
static uint8_t is_not_placement(char *ptn) {
if (ptn == 0)
return 0;
for (;; ptn++) {
switch (*ptn) {
case '+':
case '-':
case '>':
case '<':
return 1;
case 0:
return 0;
}
}
}
enum ACT_RESULT do_ptn(tak_state_p state, char *ptn) {
// Game over?
if (state->won < 0xFF)
return GAME_END;
enum PTN_RESULT ptn_res;
enum ACT_RESULT act_res;
uint8_t location;
// Placing or moving?
if (is_not_placement(ptn)) {
uint8_t steps, drops[5];
enum MOVE_DIRECTION direction;
// Parse it as a move
ptn_res = parse_move(state->board_size, ptn, &location, &direction, &steps,
drops);
// If valid PTN, try to do it
if (ptn_res == PTN_OK) {
if (state->ply < 2)
return ACT_ILLEGAL;
act_res = try_move(state, location, direction, steps, drops);
} else {
return ACT_INVALID_PTN;
}
} else {
// It was not a move
enum STONE_VARIANT stone;
// Was it a valid placement?
ptn_res = parse_place(state->board_size, ptn, &location, &stone);
// If so, try it
if (ptn_res == PTN_OK)
act_res = try_place(state, location, state->current_colour, stone);
else
return ACT_INVALID_PTN;
}
// A valid ply occured
if (act_res == ACT_OK) {
// Don't bother checking that the game was won early on, could be
// more conservative here :)
if (state->ply >= state->board_size) {
state->won = check_win(state);
if (state->won < 0xFF) {
// Winning move, but no need to update current colour
state->ply++;
return GAME_END;
}
}
// Only step if the game isn't over yet
next_ply(state);
}
return act_res;
}