/*
ctaklm, a line mode interface to the ctak 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
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 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 = 0;
static uint8_t auto_board = 0xFF, auto_info = 0xFF;
static void
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' ?
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);
strcat(gamelog,prepend);
strcat(gamelog,line);
}
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 PTN_INVALID: { puts("Invalid PTN."); return -1; break; }
case ACT_ILLEGAL: { puts("Illegal action."); return -1; break; }
case ACT_OVERFLOW: {
puts("Move would cause internal overflow, select another.");
return EXIT_FAILURE;
break;
}
// 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 PTN_VALID:
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 E_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;
// Set up output function for negamax
inline void
negamax_display_progress(const uint8_t depth, const uint32_t length) {
num_check += 1;
if (depth == negamax_search_depth) {
progress++;
printf("\x1B[0GComputing: %.0f/%d",
progress,length);
fflush(stdout);
}
}
static int
negamax_turn(void) {
if (won == 0xFF) {
// Run the minimax
num_check = 0; progress = 0;
float minimax = negamax_generate();
putchar('\n');
// Failed to find a move?
if (minimax <= -infty) {
puts("Opponent failed to find a move!");
return EXIT_FAILURE;
} else {
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 , .");
} 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,"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 = "ctaklm, a line mode interface to the ctak 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 = 3;
new_game(5);
negamax_init(5);
// Test harness
if (argc > 1) {
negamax_search_depth = 5;
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("ctaklm> ", 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;
}