pachi_py/pachi/gtp.c (624 lines of code) (raw):
#define DEBUG
#include <assert.h>
#include <ctype.h>
#include <math.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include "board.h"
#include "debug.h"
#include "engine.h"
#include "fbook.h"
#include "gtp.h"
#include "mq.h"
#include "uct/uct.h"
#include "version.h"
#include "timeinfo.h"
#include "gogui.h"
#define NO_REPLY (-2)
/* Sleep 5 seconds after a game ends to give time to kill the program. */
#define GAME_OVER_SLEEP 5
void
gtp_prefix(char prefix, int id)
{
if (id == NO_REPLY) return;
if (id >= 0)
printf("%c%d ", prefix, id);
else
printf("%c ", prefix);
}
void
gtp_flush(void)
{
putchar('\n');
fflush(stdout);
}
void
gtp_output(char prefix, int id, va_list params)
{
if (id == NO_REPLY) return;
gtp_prefix(prefix, id);
char *s;
while ((s = va_arg(params, char *))) {
fputs(s, stdout);
}
putchar('\n');
gtp_flush();
}
void
gtp_reply(int id, ...)
{
va_list params;
va_start(params, id);
gtp_output('=', id, params);
va_end(params);
}
void
gtp_error(int id, ...)
{
va_list params;
va_start(params, id);
gtp_output('?', id, params);
va_end(params);
}
static void
gtp_final_score(struct board *board, struct engine *engine, char *reply, int len)
{
struct move_queue q = { .moves = 0 };
if (engine->dead_group_list)
engine->dead_group_list(engine, board, &q);
floating_t score = board_official_score(board, &q);
if (DEBUGL(1))
fprintf(stderr, "counted score %.1f\n", score);
if (score == 0)
snprintf(reply, len, "0");
else if (score > 0)
snprintf(reply, len, "W+%.1f", score);
else
snprintf(reply, len, "B+%.1f", -score);
}
/* List of known gtp commands. The internal command pachi-genmoves is not exported,
* it should only be used between master and slaves of the distributed engine. */
static char *known_commands_base =
"protocol_version\n"
"echo\n"
"name\n"
"version\n"
"list_commands\n"
"known_command\n"
"quit\n"
"boardsize\n"
"clear_board\n"
"kgs-game_over\n"
"komi\n"
"kgs-rules\n"
"play\n"
"genmove\n"
"kgs-genmove_cleanup\n"
"set_free_handicap\n"
"place_free_handicap\n"
"fixed_handicap\n"
"final_score\n"
"final_status_list\n"
"undo\n"
"pachi-evaluate\n"
"pachi-result\n"
"pachi-gentbook\n"
"pachi-dumptbook\n"
"kgs-chat\n"
"time_left\n"
"time_settings\n"
"kgs-time_settings";
static char*
known_commands(struct engine *engine)
{
static char *str = 0;
if (str)
return str;
if (strcmp(engine->name, "UCT")) /* Not uct ? */
return known_commands_base;
/* For now only uct supports gogui-analyze_commands */
str = malloc(strlen(known_commands_base) + 32);
sprintf(str, "%s\ngogui-analyze_commands", known_commands_base);
return str;
}
static char *gogui_analyze_commands =
"string/ Final Score/final_score\n"
"gfx/gfx Best Moves B/gogui-best_moves b\n"
"gfx/gfx Best Moves W/gogui-best_moves w\n"
"gfx/gfx Winrates B/gogui-winrates b\n"
"gfx/gfx Winrates W/gogui-winrates w\n"
"gfx/gfx Owner Map/gogui-owner_map\n"
"gfx/Live gfx = Best Moves/gogui-live_gfx best_moves\n"
"gfx/Live gfx = Best Sequence/gogui-live_gfx best_seq\n"
"gfx/Live gfx = Winrates/gogui-live_gfx winrates\n";
char gogui_gfx_buf[5000];
enum gogui_reporting gogui_live_gfx = 0;
static void
gogui_set_live_gfx(struct engine *engine, char *arg)
{
if (!strcmp(arg, "best_moves"))
gogui_live_gfx = UR_GOGUI_CAN;
if (!strcmp(arg, "best_seq"))
gogui_live_gfx = UR_GOGUI_SEQ;
if (!strcmp(arg, "winrates"))
gogui_live_gfx = UR_GOGUI_WR;
engine->live_gfx_hook(engine);
}
static char *
gogui_best_moves(struct board *b, struct engine *engine, char *arg, bool winrates)
{
enum stone color = str2stone(arg);
assert(color != S_NONE);
enum gogui_reporting prev = gogui_live_gfx;
gogui_set_live_gfx(engine, (winrates ? "winrates" : "best_moves"));
gogui_gfx_buf[0] = 0;
engine->best_moves(engine, b, color);
gogui_live_gfx = prev;
return gogui_gfx_buf;
}
/* XXX Completely unsafe if reply buffer is not big enough */
static void
gogui_owner_map(struct board *b, struct engine *engine, char *reply)
{
char str2[32];
reply[0] = 0;
if (!engine->owner_map)
return;
sprintf(reply, "INFLUENCE");
foreach_point(b) {
if (board_at(b, c) == S_OFFBOARD)
continue;
float p = engine->owner_map(engine, b, c);
// p = -1 for WHITE, 1 for BLACK absolute ownership of point i
if (p < -.8)
p = -1.0;
else if (p < -.5)
p = -0.7;
else if (p < -.2)
p = -0.4;
else if (p < 0.2)
p = 0.0;
else if (p < 0.5)
p = 0.4;
else if (p < 0.8)
p = 0.7;
else
p = 1.0;
sprintf(str2, " %3s %.1lf", coord2sstr(c, b), p);
strcat(reply, str2);
} foreach_point_end;
strcat(reply, "\nTEXT Score Est: ");
gtp_final_score(b, engine, str2, sizeof(str2));
strcat(reply, str2);
}
/* Return true if cmd is a valid gtp command. */
bool
gtp_is_valid(struct engine *e, const char *cmd)
{
if (!cmd || !*cmd) return false;
const char *s = strcasestr(known_commands(e), cmd);
if (!s) return false;
if (s != known_commands(e) && s[-1] != '\n') return false;
int len = strlen(cmd);
return s[len] == '\0' || s[len] == '\n';
}
/* XXX: THIS IS TOTALLY INSECURE!!!!
* Even basic input checking is missing. */
enum parse_code
gtp_parse(struct board *board, struct engine *engine, struct time_info *ti, char *buf)
{
#define next_tok(to_) \
to_ = next; \
next = next + strcspn(next, " \t\r\n"); \
if (*next) { \
*next = 0; next++; \
next += strspn(next, " \t\r\n"); \
}
if (strchr(buf, '#'))
*strchr(buf, '#') = 0;
char *cmd, *next = buf;
next_tok(cmd);
int id = -1;
if (isdigit(*cmd)) {
id = atoi(cmd);
next_tok(cmd);
}
if (!*cmd)
return P_OK;
if (!strcasecmp(cmd, "protocol_version")) {
gtp_reply(id, "2", NULL);
return P_OK;
} else if (!strcasecmp(cmd, "name")) {
/* KGS hack */
gtp_reply(id, "Pachi ", engine->name, NULL);
return P_OK;
} else if (!strcasecmp(cmd, "echo")) {
gtp_reply(id, next, NULL);
return P_OK;
} else if (!strcasecmp(cmd, "version")) {
gtp_reply(id, PACHI_VERSION, ": ", engine->comment, " Have a nice game!", NULL);
return P_OK;
} else if (!strcasecmp(cmd, "list_commands")) {
gtp_reply(id, known_commands(engine), NULL);
return P_OK;
} else if (!strcasecmp(cmd, "known_command")) {
char *arg;
next_tok(arg);
if (gtp_is_valid(engine, arg)) {
gtp_reply(id, "true", NULL);
} else {
gtp_reply(id, "false", NULL);
}
return P_OK;
}
if (engine->notify && gtp_is_valid(engine, cmd)) {
char *reply;
enum parse_code c = engine->notify(engine, board, id, cmd, next, &reply);
if (c == P_NOREPLY) {
id = NO_REPLY;
} else if (c == P_DONE_OK) {
gtp_reply(id, reply, NULL);
return P_OK;
} else if (c == P_DONE_ERROR) {
gtp_error(id, reply, NULL);
/* This is an internal error for the engine, but
* it is still OK from main's point of view. */
return P_OK;
} else if (c != P_OK) {
return c;
}
}
if (!strcasecmp(cmd, "quit")) {
gtp_reply(id, NULL);
exit(0);
} else if (!strcasecmp(cmd, "boardsize")) {
char *arg;
next_tok(arg);
int size = atoi(arg);
if (size < 1 || size > BOARD_MAX_SIZE) {
gtp_error(id, "illegal board size", NULL);
return P_OK;
}
board_resize(board, size);
board_clear(board);
gtp_reply(id, NULL);
return P_ENGINE_RESET;
} else if (!strcasecmp(cmd, "clear_board")) {
board_clear(board);
if (DEBUGL(3) && debug_boardprint)
board_print(board, stderr);
gtp_reply(id, NULL);
return P_ENGINE_RESET;
} else if (!strcasecmp(cmd, "kgs-game_over")) {
/* The game may not be really over, just adjourned.
* Do not clear the board to avoid illegal moves
* if the game is resumed immediately after. KGS
* may start directly with genmove on resumption. */
if (DEBUGL(1)) {
fprintf(stderr, "game is over\n");
fflush(stderr);
}
if (engine->stop)
engine->stop(engine);
/* Sleep before replying, so that kgs doesn't
* start another game immediately. */
sleep(GAME_OVER_SLEEP);
gtp_reply(id, NULL);
} else if (!strcasecmp(cmd, "komi")) {
char *arg;
next_tok(arg);
sscanf(arg, PRIfloating, &board->komi);
if (DEBUGL(3) && debug_boardprint)
board_print(board, stderr);
gtp_reply(id, NULL);
} else if (!strcasecmp(cmd, "kgs-rules")) {
char *arg;
next_tok(arg);
if (!board_set_rules(board, arg)) {
gtp_error(id, "unknown rules", NULL);
return P_OK;
}
gtp_reply(id, NULL);
} else if (!strcasecmp(cmd, "play")) {
struct move m;
char *arg;
next_tok(arg);
m.color = str2stone(arg);
next_tok(arg);
coord_t *c = str2coord(arg, board_size(board));
m.coord = *c; coord_done(c);
next_tok(arg);
char *enginearg = arg;
char *reply = NULL;
if (DEBUGL(5))
fprintf(stderr, "got move %d,%d,%d\n", m.color, coord_x(m.coord, board), coord_y(m.coord, board));
// This is where kgs starts the timer, not at genmove!
time_start_timer(&ti[stone_other(m.color)]);
if (engine->notify_play)
reply = engine->notify_play(engine, board, &m, enginearg);
if (board_play(board, &m) < 0) {
if (DEBUGL(0)) {
fprintf(stderr, "! ILLEGAL MOVE %d,%d,%d\n", m.color, coord_x(m.coord, board), coord_y(m.coord, board));
board_print(board, stderr);
}
gtp_error(id, "illegal move", NULL);
} else {
if (DEBUGL(4) && debug_boardprint)
board_print_custom(board, stderr, engine->printhook);
gtp_reply(id, reply, NULL);
}
} else if (!strcasecmp(cmd, "genmove") || !strcasecmp(cmd, "kgs-genmove_cleanup")) {
char *arg;
next_tok(arg);
enum stone color = str2stone(arg);
coord_t *c = NULL;
if (DEBUGL(2) && debug_boardprint)
board_print_custom(board, stderr, engine->printhook);
if (!ti[color].len.t.timer_start) {
/* First game move. */
time_start_timer(&ti[color]);
}
coord_t cf = pass;
if (board->fbook)
cf = fbook_check(board);
if (!is_pass(cf)) {
c = coord_copy(cf);
} else {
c = engine->genmove(engine, board, &ti[color], color, !strcasecmp(cmd, "kgs-genmove_cleanup"));
}
struct move m = { *c, color };
if (board_play(board, &m) < 0) {
fprintf(stderr, "Attempted to generate an illegal move: [%s, %s]\n", coord2sstr(m.coord, board), stone2str(m.color));
abort();
}
char *str = coord2str(*c, board);
if (DEBUGL(4))
fprintf(stderr, "playing move %s\n", str);
if (DEBUGL(1) && debug_boardprint) {
board_print_custom(board, stderr, engine->printhook);
}
gtp_reply(id, str, NULL);
free(str); coord_done(c);
/* Account for spent time. If our GTP peer keeps our clock, this will
* be overriden by next time_left GTP command properly. */
/* (XXX: Except if we pass to byoyomi and the peer doesn't, but that
* should be absolutely rare situation and we will just spend a little
* less time than we could on next few moves.) */
if (ti[color].period != TT_NULL && ti[color].dim == TD_WALLTIME)
time_sub(&ti[color], time_now() - ti[color].len.t.timer_start, true);
} else if (!strcasecmp(cmd, "pachi-genmoves") || !strcasecmp(cmd, "pachi-genmoves_cleanup")) {
char *arg;
next_tok(arg);
enum stone color = str2stone(arg);
void *stats;
int stats_size;
char *reply = engine->genmoves(engine, board, &ti[color], color, next,
!strcasecmp(cmd, "pachi-genmoves_cleanup"),
&stats, &stats_size);
if (!reply) {
gtp_error(id, "genmoves error", NULL);
return P_OK;
}
if (DEBUGL(3))
fprintf(stderr, "proposing moves %s\n", reply);
if (DEBUGL(4) && debug_boardprint)
board_print_custom(board, stderr, engine->printhook);
gtp_reply(id, reply, NULL);
if (stats_size > 0) {
double start = time_now();
fwrite(stats, 1, stats_size, stdout);
fflush(stdout);
if (DEBUGVV(2))
fprintf(stderr, "sent reply %d bytes in %.4fms\n",
stats_size, (time_now() - start)*1000);
}
} else if (!strcasecmp(cmd, "set_free_handicap")) {
struct move m;
m.color = S_BLACK;
char *arg;
next_tok(arg);
do {
coord_t *c = str2coord(arg, board_size(board));
m.coord = *c; coord_done(c);
if (DEBUGL(4))
fprintf(stderr, "setting handicap %d,%d\n", coord_x(m.coord, board), coord_y(m.coord, board));
if (board_play(board, &m) < 0) {
if (DEBUGL(0))
fprintf(stderr, "! ILLEGAL MOVE %d,%d,%d\n", m.color, coord_x(m.coord, board), coord_y(m.coord, board));
gtp_error(id, "illegal move", NULL);
}
board->handicap++;
next_tok(arg);
} while (*arg);
if (DEBUGL(1) && debug_boardprint)
board_print(board, stderr);
gtp_reply(id, NULL);
/* TODO: Engine should choose free handicap; however, it tends to take
* overly long to think it all out, and unless it's clever its
* handicap stones won't be of much help. ;-) */
} else if (!strcasecmp(cmd, "place_free_handicap")
|| !strcasecmp(cmd, "fixed_handicap")) {
char *arg;
next_tok(arg);
int stones = atoi(arg);
gtp_prefix('=', id);
board_handicap(board, stones, id == NO_REPLY ? NULL : stdout);
if (DEBUGL(1) && debug_boardprint)
board_print(board, stderr);
if (id == NO_REPLY) return P_OK;
putchar('\n');
gtp_flush();
} else if (!strcasecmp(cmd, "final_score")) {
char str[64];
gtp_final_score(board, engine, str, sizeof(str));
gtp_reply(id, str, NULL);
/* XXX: This is a huge hack. */
} else if (!strcasecmp(cmd, "final_status_list")) {
if (id == NO_REPLY) return P_OK;
char *arg;
next_tok(arg);
struct move_queue q = { .moves = 0 };
if (engine->dead_group_list)
engine->dead_group_list(engine, board, &q);
/* else we return empty list - i.e. engine not supporting
* this assumes all stones alive at the game end. */
if (!strcasecmp(arg, "dead")) {
gtp_prefix('=', id);
for (unsigned int i = 0; i < q.moves; i++) {
foreach_in_group(board, q.move[i]) {
printf("%s ", coord2sstr(c, board));
} foreach_in_group_end;
putchar('\n');
}
if (!q.moves)
putchar('\n');
gtp_flush();
} else if (!strcasecmp(arg, "seki") || !strcasecmp(arg, "alive")) {
gtp_prefix('=', id);
bool printed_group = false;
foreach_point(board) { // foreach_group, effectively
group_t g = group_at(board, c);
if (!g || g != c) continue;
for (unsigned int i = 0; i < q.moves; i++) {
if (q.move[i] == g)
goto next_group;
}
foreach_in_group(board, g) {
printf("%s ", coord2sstr(c, board));
} foreach_in_group_end;
putchar('\n');
printed_group = true;
next_group:;
} foreach_point_end;
if (!printed_group)
putchar('\n');
gtp_flush();
} else {
gtp_error(id, "illegal status specifier", NULL);
}
} else if (!strcasecmp(cmd, "undo")) {
if (board_undo(board) < 0) {
if (DEBUGL(1)) {
fprintf(stderr, "undo on non-pass move %s\n", coord2sstr(board->last_move.coord, board));
board_print(board, stderr);
}
gtp_error(id, "cannot undo", NULL);
return P_OK;
}
char *reply = NULL;
if (engine->undo)
reply = engine->undo(engine, board);
if (DEBUGL(3) && debug_boardprint)
board_print(board, stderr);
gtp_reply(id, reply, NULL);
/* Custom commands for handling the tree opening tbook */
} else if (!strcasecmp(cmd, "pachi-gentbook")) {
/* Board must be initialized properly, as if for genmove;
* makes sense only as 'uct_gentbook b'. */
char *arg;
next_tok(arg);
enum stone color = str2stone(arg);
if (uct_gentbook(engine, board, &ti[color], color))
gtp_reply(id, NULL);
else
gtp_error(id, "error generating tbook", NULL);
} else if (!strcasecmp(cmd, "pachi-dumptbook")) {
char *arg;
next_tok(arg);
enum stone color = str2stone(arg);
uct_dumptbook(engine, board, color);
gtp_reply(id, NULL);
} else if (!strcasecmp(cmd, "pachi-evaluate")) {
char *arg;
next_tok(arg);
enum stone color = str2stone(arg);
if (!engine->evaluate) {
gtp_error(id, "pachi-evaluate not supported by engine", NULL);
} else {
gtp_prefix('=', id);
floating_t vals[board->flen];
engine->evaluate(engine, board, &ti[color], vals, color);
for (int i = 0; i < board->flen; i++) {
if (!board_coord_in_symmetry(board, board->f[i])
|| isnan(vals[i]) || vals[i] < 0.001)
continue;
printf("%s %.3f\n", coord2sstr(board->f[i], board), (double) vals[i]);
}
gtp_flush();
}
} else if (!strcasecmp(cmd, "pachi-result")) {
/* More detailed result of the last genmove. */
/* For UCT, the output format is: = color move playouts winrate dynkomi */
char *reply = NULL;
if (engine->result)
reply = engine->result(engine, board);
if (reply)
gtp_reply(id, reply, NULL);
else
gtp_error(id, "unknown pachi-result command", NULL);
} else if (!strcasecmp(cmd, "kgs-chat")) {
char *loc;
next_tok(loc);
bool opponent = !strcasecmp(loc, "game");
char *from;
next_tok(from);
char *msg = next;
msg += strspn(msg, " \n\t");
char *end = strchr(msg, '\n');
if (end) *end = '\0';
char *reply = NULL;
if (engine->chat) {
reply = engine->chat(engine, board, opponent, from, msg);
}
if (reply)
gtp_reply(id, reply, NULL);
else
gtp_error(id, "unknown kgs-chat command", NULL);
} else if (!strcasecmp(cmd, "time_left")) {
char *arg;
next_tok(arg);
enum stone color = str2stone(arg);
next_tok(arg);
int time = atoi(arg);
next_tok(arg);
int stones = atoi(arg);
if (!ti[color].ignore_gtp) {
time_left(&ti[color], time, stones);
} else {
if (DEBUGL(2)) fprintf(stderr, "ignored time info\n");
}
gtp_reply(id, NULL);
} else if (!strcasecmp(cmd, "time_settings") || !strcasecmp(cmd, "kgs-time_settings")) {
char *time_system;
char *arg;
if (!strcasecmp(cmd, "kgs-time_settings")) {
next_tok(time_system);
} else {
time_system = "canadian";
}
int main_time = 0, byoyomi_time = 0, byoyomi_stones = 0, byoyomi_periods = 0;
if (!strcasecmp(time_system, "none")) {
main_time = -1;
} else if (!strcasecmp(time_system, "absolute")) {
next_tok(arg);
main_time = atoi(arg);
} else if (!strcasecmp(time_system, "byoyomi")) {
next_tok(arg);
main_time = atoi(arg);
next_tok(arg);
byoyomi_time = atoi(arg);
next_tok(arg);
byoyomi_periods = atoi(arg);
} else if (!strcasecmp(time_system, "canadian")) {
next_tok(arg);
main_time = atoi(arg);
next_tok(arg);
byoyomi_time = atoi(arg);
next_tok(arg);
byoyomi_stones = atoi(arg);
}
if (DEBUGL(1))
fprintf(stderr, "time_settings %d %d/%d*%d\n",
main_time, byoyomi_time, byoyomi_stones, byoyomi_periods);
if (!ti[S_BLACK].ignore_gtp) {
time_settings(&ti[S_BLACK], main_time, byoyomi_time, byoyomi_stones, byoyomi_periods);
ti[S_WHITE] = ti[S_BLACK];
} else {
if (DEBUGL(1)) fprintf(stderr, "ignored time info\n");
}
gtp_reply(id, NULL);
} else if (!strcasecmp(cmd, "gogui-analyze_commands")) {
gtp_reply(id, gogui_analyze_commands, NULL);
} else if (!strcasecmp(cmd, "gogui-live_gfx")) {
char *arg;
next_tok(arg);
gogui_set_live_gfx(engine, arg);
gtp_reply(id, NULL);
} else if (!strcasecmp(cmd, "gogui-owner_map")) {
char reply[5000];
gogui_owner_map(board, engine, reply);
gtp_reply(id, reply, NULL);
} else if (!strcasecmp(cmd, "gogui-best_moves")) {
char *arg;
next_tok(arg);
char *reply = gogui_best_moves(board, engine, arg, false);
gtp_reply(id, reply, NULL);
} else if (!strcasecmp(cmd, "gogui-winrates")) {
char *arg;
next_tok(arg);
char *reply = gogui_best_moves(board, engine, arg, true);
gtp_reply(id, reply, NULL);
} else {
gtp_error(id, "unknown command", NULL);
return P_UNKNOWN_COMMAND;
}
return P_OK;
#undef next_tok
}