From 2c2543a0a1e69c4194a17994237886f273044403 Mon Sep 17 00:00:00 2001 From: Jean-loup Gailly Date: Mon, 1 Feb 2010 17:26:30 +0100 Subject: [PATCH] Add support for time_left, time_setting and kgs-time_settings gtp commands. --- gtp.c | 75 +++++++++++++++++++++++++++++++++++++++--- timeinfo.c | 109 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++- timeinfo.h | 28 ++++++++++++++-- zzgo.c | 15 ++++++--- 4 files changed, 215 insertions(+), 12 deletions(-) diff --git a/gtp.c b/gtp.c index 8397519..1c392da 100644 --- a/gtp.c +++ b/gtp.c @@ -104,7 +104,24 @@ gtp_parse(struct board *board, struct engine *engine, struct time_info *ti, char /* TODO: known_command */ } else if (!strcasecmp(cmd, "list_commands")) { - gtp_reply(id, "protocol_version\nname\nversion\nlist_commands\nquit\nboardsize\nclear_board\nkomi\nplay\ngenmove\nkgs-genmove_cleanup\nset_free_handicap\nplace_free_handicap\nfinal_status_list\nkgs-chat", NULL); + gtp_reply(id, "protocol_version\n" + "name\n" + "version\n" + "list_commands\n" + "quit\n" + "boardsize\n" + "clear_board\n" + "komi\n" + "play\n" + "genmove\n" + "kgs-genmove_cleanup\n" + "set_free_handicap\n" + "place_free_handicap\n" + "final_status_list\n" + "kgs-chat\n" + "time_left\n" + "time_settings\n" + "kgs-time_settings", NULL); } else if (!strcasecmp(cmd, "quit")) { gtp_reply(id, NULL); @@ -166,9 +183,9 @@ gtp_parse(struct board *board, struct engine *engine, struct time_info *ti, char char *arg; next_tok(arg); enum stone color = str2stone(arg); - time_prepare_move(ti, board); + time_prepare_move(&ti[color], board); - coord_t *c = engine->genmove(engine, board, ti, color, !strcasecmp(cmd, "kgs-genmove_cleanup")); + coord_t *c = engine->genmove(engine, board, &ti[color], color, !strcasecmp(cmd, "kgs-genmove_cleanup")); struct move m = { *c, color }; board_play(board, &m); char *str = coord2str(*c, board); @@ -290,7 +307,7 @@ next_group:; char *arg; next_tok(arg); enum stone color = str2stone(arg); - if (uct_genbook(engine, board, ti, color)) + if (uct_genbook(engine, board, &ti[color], color)) gtp_reply(id, NULL); else gtp_error(id, "error generating book", NULL); @@ -317,6 +334,56 @@ next_group:; else gtp_error(id, "unknown 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); + time_left(&ti[color], time, stones); + + gtp_reply(id, NULL); + + } else if (!strcasecmp(cmd, "time_settings") || !strcasecmp(cmd, "kgs-time_settings")) { + char *time_system = "canadian"; + char *arg; + int main_time = -1, byoyomi_time = 0, byoyomi_stones = 0, byoyomi_periods = 0; + if (!strcasecmp(cmd, "kgs-time_settings")) { + next_tok(time_system); + if (!strcasecmp(time_system, "none")) { + byoyomi_time = 1; // time > 0, stones 0 : convention for unlimited + main_time = 0; + } 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); + byoyomi_stones = 1; + next_tok(arg); + byoyomi_periods = atoi(arg); + } + } + if (main_time < 0) { // canadian time system + 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); + time_settings(&ti[S_BLACK], main_time, byoyomi_time, byoyomi_stones, byoyomi_periods); + ti[S_WHITE] = ti[S_BLACK]; + + gtp_reply(id, NULL); + } else { gtp_error(id, "unknown command", NULL); } diff --git a/timeinfo.c b/timeinfo.c index ffb0685..fc497f4 100644 --- a/timeinfo.c +++ b/timeinfo.c @@ -10,6 +10,8 @@ #include "debug.h" #include "timeinfo.h" +#define MAX_NET_LAG 2.0 /* Max net lag in seconds. TODO: estimate dynamically. */ +#define RESERVED_BYOYOMI_PERCENT 15 /* Reserve 15% of byoyomi time as safety margin if risk of losing on time */ bool time_parse(struct time_info *ti, char *s) @@ -28,11 +30,63 @@ time_parse(struct time_info *ti, char *s) return false; ti->dim = TD_WALLTIME; ti->len.t.recommended_time = atof(s); + ti->len.t.net_lag = MAX_NET_LAG; + ti->len.t.timer_start = 0; + ti->len.t.byoyomi_time = 0.0; + ti->len.t.byoyomi_periods = 0; break; } return true; } +/* Update time settings according to gtp time_settings or kgs-time_settings command. */ +void +time_settings(struct time_info *ti, int main_time, int byoyomi_time, int byoyomi_stones, int byoyomi_periods) +{ + if (byoyomi_time > 0 && byoyomi_stones == 0) { + ti->period = TT_NULL; // no time limit, rely on engine default + } else { + ti->period = TT_TOTAL; + ti->dim = TD_WALLTIME; + ti->len.t.max_time = (double) main_time; // byoyomi will be added at next genmove + ti->len.t.recommended_time = ti->len.t.max_time; + ti->len.t.timer_start = 0; + ti->len.t.net_lag = MAX_NET_LAG; + ti->len.t.byoyomi_time = (double) byoyomi_time; + if (byoyomi_stones > 0) + ti->len.t.byoyomi_time /= byoyomi_stones; + ti->len.t.byoyomi_periods = byoyomi_periods; + } +} + +/* Update time information according to gtp time_left command. + * kgs doesn't give time_left for the first move, so make sure + * that just time_settings + time_select_best still work. */ +void +time_left(struct time_info *ti, int time_left, int stones_left) +{ + assert(ti->period != TT_NULL); + ti->dim = TD_WALLTIME; + ti->len.t.max_time = (double)time_left; + + if (ti->len.t.byoyomi_periods > 0 && stones_left > 0) { + ti->len.t.byoyomi_periods = stones_left; // field misused by kgs + stones_left = 1; + } + /* For non-canadian byoyomi, we use all periods as main time. */ + if (stones_left == 0 || ti->len.t.byoyomi_periods > 1) { + /* Main time */ + ti->period = TT_TOTAL; + ti->len.t.recommended_time = ti->len.t.max_time; + /* byoyomi_time, net_lag & timer_start unchanged. */ + } else { + ti->period = TT_MOVE; + ti->len.t.byoyomi_time = ((double)time_left)/stones_left; + ti->len.t.recommended_time = ti->len.t.byoyomi_time; + /* net_lag & timer_start unchanged. */ + } +} + /* Set correct time information before making a move, and * always make it time per move for the engine. */ void @@ -52,13 +106,66 @@ time_prepare_move(struct time_info *ti, struct board *board) return; double now = time_now(); + double lag; if (!ti->len.t.timer_start) { ti->len.t.timer_start = now; // we're playing the first game move + lag = 0; + } else { + lag = now - ti->len.t.timer_start; + // TODO: keep statistics to get good estimate of lag not just current move + ti->len.t.max_time -= lag; // can become < 0, taken into account below + ti->len.t.recommended_time -= lag; + if (DEBUGL(2) && lag > MAX_NET_LAG) + fprintf(stderr, "lag %0.2f > max_net_lag %0.2f\n", lag, MAX_NET_LAG); } if (ti->period == TT_TOTAL) { + /* For non-canadian byoyomi, we use all periods as main time, just making sure + * to avoid running out of the last one. */ + if (ti->len.t.byoyomi_periods > 1) { + ti->len.t.max_time += (ti->len.t.byoyomi_periods - 1) * ti->len.t.byoyomi_time; + // Will add 1 more byoyomi_time just below + } + if (ti->len.t.byoyomi_time > 0) { + ti->len.t.max_time += ti->len.t.byoyomi_time; + ti->len.t.recommended_time = ti->len.t.max_time; + + /* Maximize the number of moves played uniformly in main time, while + * not playing faster in main time than in byoyomi. At this point, + * the main time remaining is ti->len.t.max_time and already includes + * the first (canadian) or all byoyomi periods. + * main_speed = max_time / main_moves >= byoyomi_time + * => main_moves <= max_time / byoyomi_time */ + double actual_byoyomi = ti->len.t.byoyomi_time - MAX_NET_LAG; + if (actual_byoyomi > 0) { + int main_moves = (int)(ti->len.t.max_time / actual_byoyomi); + if (moves_left > main_moves) + moves_left = main_moves; // will do the rest in byoyomi + if (moves_left <= 0) // possible if too much lag + moves_left = 1; + } + } ti->period = TT_MOVE; - ti->len.t.recommended_time /= moves_left; + ti->len.t.recommended_time /= moves_left; // may be < 0 if too much lag + } + // To simplify the engine code, do not leave negative times: + if (ti->len.t.recommended_time < 0) + ti->len.t.recommended_time = 0; + if (ti->len.t.max_time < 0) + ti->len.t.max_time = 0; + assert(ti->len.t.recommended_time <= ti->len.t.max_time + 0.001); + + /* Use a larger safety margin if we risk losing on time on this move: */ + double safe_margin = RESERVED_BYOYOMI_PERCENT * ti->len.t.byoyomi_time/100; + if (safe_margin > MAX_NET_LAG && ti->len.t.recommended_time >= ti->len.t.max_time - MAX_NET_LAG) { + ti->len.t.net_lag = safe_margin; + } else { + ti->len.t.net_lag = MAX_NET_LAG; } + + if (DEBUGL(1)) + fprintf(stderr, "recommended_time %0.2f, max_time %0.2f, byoyomi %0.2f, lag %0.2f max %0.2f\n", + ti->len.t.recommended_time, ti->len.t.max_time, ti->len.t.byoyomi_time, lag, + ti->len.t.net_lag); } /* Start our timer. kgs does this (correctly) on "play" not "genmove" diff --git a/timeinfo.h b/timeinfo.h index 010f87b..f0ad017 100644 --- a/timeinfo.h +++ b/timeinfo.h @@ -2,7 +2,8 @@ #define ZZGO_TIMEINFO_H /* Time-keeping information about time to spend on the next move and/or - * rest of the game. */ + * rest of the game. This is only a hint, an engine may decide to spend + * more or less time on a given move, provided it never forfeits on time. */ /* Note that some ways of specifying time (TD_GAMES) may not make sense * with all engines. */ @@ -16,7 +17,7 @@ struct time_info { enum time_period { TT_NULL, // No time limit. Other structure elements are undef. TT_MOVE, // Time for the next move. - TT_TOTAL, // Time for the rest of the game. + TT_TOTAL, // Time for the rest of the game. Never seen by engine. } period; /* How are we counting the time? */ enum time_dimension { @@ -30,9 +31,26 @@ struct time_info { * include net lag. Play asap if 0. */ double recommended_time; + /* Maximum wall time for next move or game. Will lose on time + * if exceeded. Does not include net lag. Play asap if 0. */ + double max_time; + + /* Minimum net lag (seconds) to be reserved by the engine. The engine + * may use a larger safety margin. */ + double net_lag; + /* Absolute time at which our timer started for current move, 0 if * not yet known. The engine always sees > 0. */ double timer_start; + + /* --- PRIVATE DATA --- */ + /* Byoyomi time per move (even for TT_TOTAL). This time must + * be remembered to avoid rushing at the end of the main + * period. 0 if no byoyomi. An engine should only consider + * recommended_time, the generic time control code always sets it to + * the best option (play on main time or on byoyomi time). */ + double byoyomi_time; + int byoyomi_periods; /* > 0 only for non-canadian byoyomi */ } t; } len; }; @@ -45,6 +63,12 @@ struct time_info { * Returns false on parse error. */ bool time_parse(struct time_info *ti, char *s); +/* Update time settings according to gtp time_settings command: */ +void time_settings(struct time_info *ti, int main_time, int byoyomi_time, int byoyomi_stones, int byoyomi_periods); + +/* Update time information according to gtp time_left command: */ +void time_left(struct time_info *ti, int time_left, int stones_left); + /* Start our timer. kgs does this (correctly) on "play" not "genmove" * unless we are making the first move of the game. */ void time_start_timer(struct time_info *ti); diff --git a/zzgo.c b/zzgo.c index da58755..eb93050 100644 --- a/zzgo.c +++ b/zzgo.c @@ -64,7 +64,8 @@ bool engine_reset = false; int main(int argc, char *argv[]) { enum engine_id engine = E_UCT; - struct time_info ti_default = { .period = TT_NULL }; + /* time_info for none(ignored), black, white: */ + struct time_info ti_default[] = { { .period = TT_NULL }, { .period = TT_NULL }, { .period = TT_NULL }}; char *testfile = NULL; seed = time(NULL) ^ getpid(); @@ -99,10 +100,11 @@ int main(int argc, char *argv[]) * time_left GTP commands are received). Please * see timeinfo.h:time_parse() description for * syntax details. */ - if (!time_parse(&ti_default, optarg)) { + if (!time_parse(&ti_default[S_BLACK], optarg)) { fprintf(stderr, "%s: Invalid -t argument %s\n", argv[0], optarg); exit(1); } + ti_default[S_WHITE] = ti_default[S_BLACK]; break; case 'u': testfile = strdup(optarg); @@ -119,7 +121,9 @@ int main(int argc, char *argv[]) fprintf(stderr, "Random seed: %d\n", seed); struct board *b = board_init(); - struct time_info ti = ti_default; + struct time_info ti[S_WHITE+1]; + ti[S_BLACK] = ti_default[S_BLACK]; + ti[S_WHITE] = ti_default[S_WHITE]; char *e_arg = NULL; if (optind < argc) @@ -135,13 +139,14 @@ int main(int argc, char *argv[]) while (fgets(buf, 4096, stdin)) { if (DEBUGL(1)) fprintf(stderr, "IN: %s", buf); - gtp_parse(b, e, &ti, buf); + gtp_parse(b, e, ti, buf); if (engine_reset) { if (!e->keep_on_clear) { b->es = NULL; done_engine(e); e = init_engine(engine, e_arg, b); - ti = ti_default; + ti[S_BLACK] = ti_default[S_BLACK]; + ti[S_WHITE] = ti_default[S_WHITE]; } engine_reset = false; } -- 2.11.4.GIT