/*
ctlm, a line mode interface to the ct library
Copyright (C) 2021, tslil clingman
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 this program. If not, see .
*/
#include
#include
#include
#include
#include
#include
static const char *blk = "\033[41m", *wht = "\033[44m";
static const char *und = "\033[4m", *rst = "\033[0m";
#define SQUARE_W 5
#define SQUARE_H 3
#define CHAR_FLT '#'
#define CHAR_STN '/'
#define CHAR_CAP '*'
static void put_stone(const enum STONE_VARIANT stone, const enum COLOUR colour,
const uint8_t top, const uint8_t beyond_carry_limit) {
if (beyond_carry_limit) {
fputs(und, stdout);
} else {
if (colour == C_BLACK)
fputs(blk, stdout);
else
fputs(wht, stdout);
}
if (top) {
switch (stone) {
case STONE_FLAT:
putchar(CHAR_FLT);
break;
case STONE_STANDING:
putchar(CHAR_STN);
break;
case STONE_CAPSTONE:
putchar(CHAR_CAP);
break;
}
} else {
putchar((colour == C_BLACK) ? 'B' : 'W');
}
fputs(rst, stdout);
}
static void print_cell_line(const uint8_t line, const uint8_t col,
const uint8_t row) {
const uint8_t location = THE_COORDS(col, row),
stack_size = COUNT_AT(location);
uint8_t idx;
for (uint8_t k = 0; k < SQUARE_W; k++) {
idx = line + k * SQUARE_H;
// Are we in the last column to be displayed?
if ((k + 1) * SQUARE_H > stack_size) {
// If so, then if we can't fill it offset the starting line so
// that it fills from the bottom up instead of the top down
if (line < (k + 1) * SQUARE_H - stack_size) {
// skip these lines
idx = 0xFF;
} else {
// offset back
idx -= (k + 1) * SQUARE_H - stack_size;
}
}
if (idx < stack_size) {
put_stone(STONE_AT(location),
(colours[location] & (1 << idx)) ? C_BLACK : C_WHITE, idx == 0,
idx >= board_size);
} else {
putchar(' ');
}
}
}
// It takes board_size*(SQUARE_H+1)+1 lines to print the board, they
// may be requested in any order and at any time
static void print_board_line(const uint8_t line) {
const uint8_t mod = line % (SQUARE_H + 1),
row = board_size - line / (SQUARE_H + 1) - 1;
// Print leader, either row number if half-way through square or
// padding spaces otherwise
if (mod == (SQUARE_H + 1) / 2)
printf("%d. ", row + 1);
else
fputs(" ", stdout);
// Top and bottom of squares receive borders
if (mod == 0) {
for (uint8_t x = 0; x < board_size; x++) {
putchar('+');
for (uint8_t k = 0; k < SQUARE_W; k++)
putchar('-');
}
puts("+");
} else {
// Interior of board should be filled by borders and pieces
if (line < board_size * (SQUARE_H + 1)) {
for (uint8_t x = 0; x < board_size; x++) {
putchar('|');
print_cell_line(mod - 1, x, row);
}
puts("|");
} else {
// Bottom of board has column markers
for (uint8_t x = 0; x < board_size; x++) {
for (uint8_t k = 0; k <= SQUARE_W / 2; k++)
putchar(' ');
printf("%c.", x + 'a');
for (uint8_t k = 0; k < SQUARE_W - SQUARE_W / 2 - 2; k++)
putchar(' ');
}
putchar('\n');
}
}
}
// Simple wrapper to print the whole board in one go
static void print_board(void) {
for (uint8_t k = 0; k < board_size * (SQUARE_H + 1) + 2; k++) {
print_board_line(k);
}
}
// Print the contents of a single square
static void print_square(const uint8_t col, const uint8_t row) {
if (row < board_size && col < board_size) {
printf("%c%c: ", 'a' + col, '1' + row);
const uint8_t stack_size = COUNT_AT(THE_COORDS(col, row));
if (stack_size > 0) {
uint8_t mask = 1 << (stack_size - 1);
for (uint8_t k = 0; k < stack_size; k++, mask >>= 1) {
put_stone(STONE_AT(THE_COORDS(col, row)),
(colours[THE_COORDS(col, row)] & mask) ? C_BLACK : C_WHITE,
k + 1 == stack_size, stack_size - k - 1 >= board_size);
}
puts(" <-- top");
} else {
puts("(empty)");
}
} else {
printf("Requested square not on board (%dx%d).\n", board_size, board_size);
}
}
static void print_info(void) {
printf("Turn: %2d, %s%s%s%s\n", ply / 2 + 1, (ply & 1) ? blk : wht,
(ply & 1) ? "Black" : "White", rst,
(ply < 2) ? " (counter-play start)" : "");
printf("Flats/Caps remaining: %s%02d/%d%s, %s%02d/%d%s\n", wht,
white_count & 127, white_count >> 7, rst, blk, black_count & 127,
black_count >> 7, rst);
}
static int human;
static char *gamelog = NULL;
static uint8_t auto_board = 0xFF, auto_info = 0xFF;
static int append_to_gamelog(const char *line, const uint8_t win_line) {
// I _could_ dynamically compute the size but ... don't let
// `perfect' be the enemy of `good' ?
if (gamelog == NULL)
return EXIT_FAILURE;
char prepend[8];
if (win_line) {
prepend[0] = '\n';
prepend[1] = 0;
} else if (ply & 1) {
snprintf(prepend, 7, "%s%d. ", (ply == 1) ? "" : "\n", ply / 2 + 1);
} else {
strcpy(prepend, " ");
}
// Make room for this line
gamelog =
realloc(gamelog, strlen(gamelog) + strlen(prepend) + strlen(line) + 1);
// TODO: trap errno
strcat(gamelog, prepend);
strcat(gamelog, line);
return EXIT_SUCCESS;
}
static void end_game(char *line, char *win) {
append_to_gamelog(line, 0);
append_to_gamelog(win, 1);
print_board();
puts("Game over:");
puts(gamelog);
putchar('\n');
}
static int handle_turn(char *line) {
// Track win state
uint8_t new_win = (won == 0xFF);
switch (do_ptn(line)) {
// Errors
case ACT_INVALID_PTN: {
puts("Invalid PTN.");
return EXIT_FAILURE;
}
case ACT_ILLEGAL: {
puts("Illegal action.");
return EXIT_FAILURE;
}
case ACT_OVERFLOW: {
puts("Move would cause internal overflow, select another.");
return EXIT_FAILURE;
}
// Game has ended
case GAME_END: {
// Did it end this turn?
if (new_win) {
switch (won) {
case WIN_DRAW: {
end_game(line, "1/2-1/2");
break;
}
case WIN_FLAT_BLACK: {
end_game(line, "0-F");
break;
}
case WIN_FLAT_WHITE: {
end_game(line, "F-0");
break;
}
case WIN_ROAD_BLACK: {
end_game(line, "0-R");
break;
}
case WIN_ROAD_WHITE: {
end_game(line, "R-0");
break;
}
}
}
puts("Enter `new' to play again.");
if (!new_win)
return EXIT_FAILURE;
break;
}
// Valid, append to game log
case ACT_OK: {
append_to_gamelog(line, 0);
if (auto_board)
print_board();
if (auto_info)
print_info();
break;
}
}
return EXIT_SUCCESS;
}
static void new_game(uint8_t size) {
reset_state(size);
printf("New %dx%d game! negamax at search depth %d.\n", size, size,
negamax_search_depth);
if (gamelog)
gamelog = realloc(gamelog, sizeof(char));
else
gamelog = malloc(sizeof(char));
gamelog[0] = 0;
}
static int load_ptn(const char *fn) {
FILE *fh = NULL;
fh = fopen(fn, "r");
if (fh == NULL)
return EXIT_FAILURE;
int space1, space2;
enum ACT_RESULT r;
ssize_t read;
size_t alloc_size;
char *line = NULL;
while ((read = getline(&line, &alloc_size, fh)) != -1) {
if (read && line[0] <= '9' && line[0] >= '0') {
// Trim trailing \n
line[read - 1] = 0;
// Find first separator
space1 = 0;
while (space1 < read && line[space1++] != ' ')
;
// If still on line
if (space1 < read) {
// Find next separator or end of line, either way mark the split
space2 = space1;
while (space2 < read && line[space2] != ' ')
space2++;
line[space2] = 0;
// Try the first piece we found
r = handle_turn(line + space1);
if (r) {
printf("Error on: %s\n", line + space1);
break;
}
// If there's a second piece, try it
if (space2 + 1 < read) {
r = handle_turn(line + space2 + 1);
if (r) {
printf("Error on: %s", line + space2 + 1);
break;
}
} else {
break;
}
}
}
}
if (line)
free(line);
fclose(fh);
return EXIT_SUCCESS;
}
static float num_check, progress;
static uint8_t old_depth;
// Set up output function for negamax
inline void negamax_display_progress(const uint8_t cur_depth,
const uint8_t init_depth,
const uint32_t length) {
num_check += 1;
if (cur_depth == init_depth) {
if (init_depth != old_depth) {
old_depth = init_depth;
progress = 0;
}
progress++;
printf("\x1B[0G\x1B[KComputing: %.0f/%d @ D%d", progress, length,
init_depth);
fflush(stdout);
}
}
static int negamax_turn(void) {
if (won == 0xFF) {
// Run the minimax
num_check = 0;
progress = 0;
old_depth = 0;
float minimax = negamax_generate();
putchar('\n');
// Failed to find a non-losing move?
if (minimax <= -infty)
puts("Opponent concedes!");
printf("Result: %s (%.2f, checked %.1e)\n", negamax_ptn, minimax * 100.0,
num_check);
return handle_turn(negamax_ptn);
} else {
return EXIT_FAILURE;
}
}
static int input_is_not_turn(const char *line) {
if (!strcmp(line, "help")) {
puts("Valid commands: auto (board|info), board, depth [0-9], eval,\
help, info, load , log, new, play (b|w), self-play, square\
, tps, .");
} else if (!strcmp(line, "board")) {
print_board();
} else if (!strcmp(line, "info")) {
print_info();
} else if (!strcmp(line, "eval")) {
float eval = cnn1986_evaluate_black_win() * 100;
if (ply & 1) {
printf("Black heuristic chance: %s%.2f%s\n", blk, eval, rst);
} else {
printf("White heruistic chance: %s%.2f%s\n", wht, -eval, rst);
}
} else if (!strcmp(line, "log")) {
puts(gamelog);
} else if (!strcmp(line, "new")) {
new_game(5);
} else if (!strcmp(line, "tps")) {
char buf[1000];
generate_tps(buf);
puts(buf);
} else if (!strcmp(line, "self-play")) {
while (negamax_turn() == 0)
;
} else if (!strncmp(line, "depth", 5)) {
if (strnlen(line, 7) == 7 && line[6] >= '0' && line[6] <= '9') {
negamax_search_depth = line[6] - '0';
printf("New search depth: %d.\n", negamax_search_depth);
} else {
puts("Usage: depth [0-9].");
}
} else if (!strncmp(line, "load", 4)) {
if (strnlen(line, 6) >= 6) {
if (load_ptn(line + 5)) {
printf("Errors in file %s\n", line);
}
} else {
puts("Usage: load .");
}
} else if (!strncmp(line, "auto", 4)) {
if (!strcmp(line, "auto board")) {
auto_board = ~auto_board;
printf("Automatic board display %s.\n",
(auto_board) ? "Enabled" : "Disabled");
} else if (!strcmp(line, "auto info")) {
auto_info = ~auto_info;
printf("Automatic info display %s.\n",
(auto_info) ? "Enabled" : "Disabled");
} else {
puts("Usage: auto (board|info).");
}
} else if (!strncmp(line, "square", 6)) {
if (strnlen(line, 10) == 9 && line[7] >= 'a' &&
line[7] <= '`' + board_size && line[8] >= '1' &&
line[8] <= '0' + board_size) {
print_square(line[7] - 'a', line[8] - '1');
} else {
printf("Usage: square [a-%c][1-%c].\n", '`' + board_size,
'0' + board_size);
}
} else if (!strncmp(line, "play", 4)) {
if (strnlen(line, 7) == 6 && ((line[5] == 'b' || line[5] == 'B') ||
(line[5] == 'w' || line[5] == 'W'))) {
// 'b' is even :)
human = 1 - (line[5] & 1);
new_game(5);
} else {
puts("Usage: play (b|w).");
}
} else {
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
}
const char *license = "ctlm, a line mode interface to the ct library\n\
\n\
Copyright (C) 2021, tslil clingman\n\
\n\
This program comes with ABSOLUTELY NO WARRANTY; and is made available under the terms of the GNU GPL v3 license. This is free software, and you are welcome to redistribute it under certain conditions; see COPYING for details.\n";
int main(int argc, char **argv) {
(void)(argc);
(void)(argv);
puts(license);
negamax_search_depth = 5;
new_game(5);
negamax_init(5);
// Test harness
if (argc > 1) {
negamax_search_depth = 7;
load_ptn("data/0.ptn");
negamax_turn();
return 0;
}
// Test harness
char *line = NULL;
ssize_t read = -1;
size_t alloc_size;
human = 0;
for (int playing = 1; playing;) {
while (human == 0) {
fputs("ctlm> ", stdout);
fflush(stdout);
read = getline(&line, &alloc_size, stdin);
if (read > 0) {
line[read - 1] = 0;
if (input_is_not_turn(line)) {
int r = handle_turn(line);
if (r == EXIT_SUCCESS && won == 0xFF) {
human = 1;
}
}
free(line);
line = NULL;
} else {
playing = 0;
human = 1;
}
}
if (playing) {
negamax_turn();
human = 0;
}
}
free(line);
free(gamelog);
negamax_free();
return EXIT_SUCCESS;
}