pachi_py/pachi/timeinfo.c (300 lines of code) (raw):

#include <assert.h> #include <ctype.h> #include <stdio.h> #include <stdlib.h> #include <math.h> #include <time.h> #include <sys/time.h> #define DEBUG #include "debug.h" #include "tactics/util.h" #include "timeinfo.h" /* Max net lag in seconds. TODO: estimate dynamically. */ #define MAX_NET_LAG 2.0 /* Minimal thinking time; in case reserved time gets smaller than MAX_NET_LAG, * this makes sure we play minimally sensible moves even in massive time * pressure; we still keep MAX_NET_LAG-MIN_THINK_WITH_LAG safety margin. * Note that this affects only lag adjustmnet - if reserved time *before* * lag adjustment gets too small, we still respect it and don't apply * MIN_THINK_WITH_LAG. */ #define MIN_THINK_WITH_LAG (MAX_NET_LAG / 2) /* Reserve 15% of byoyomi time as safety margin if risk of losing on time */ #define RESERVED_BYOYOMI_PERCENT 15 /* For safety, use at most 2 times the desired time on a single move * in sudden death and 1.1 times in byoyomi. */ #define MAX_SUDDEN_DEATH_RATIO 2.0 #define MAX_BYOYOMI_TIME_RATIO 1.1 bool time_parse(struct time_info *ti, char *s) { switch (s[0]) { case '_': ti->period = TT_TOTAL; s++; break; default: ti->period = TT_MOVE; break; } switch (s[0]) { case '=': ti->dim = TD_GAMES; ti->len.games = atoi(++s); break; default: if (!isdigit(s[0])) return false; ti->dim = TD_WALLTIME; ti->len.t.timer_start = 0; if (ti->period == TT_TOTAL) { ti->len.t.main_time = atof(s); ti->len.t.byoyomi_time = 0.0; ti->len.t.byoyomi_time_max = 0.0; ti->len.t.byoyomi_periods = 0; ti->len.t.byoyomi_stones = 0; ti->len.t.byoyomi_stones_max = 0; } else { assert(ti->period == TT_MOVE); ti->len.t.main_time = 0.0; ti->len.t.byoyomi_time = atof(s); ti->len.t.byoyomi_time_max = ti->len.t.byoyomi_time; ti->len.t.byoyomi_periods = 1; ti->len.t.byoyomi_stones = 1; ti->len.t.byoyomi_stones_max = 1; } 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 (main_time < 0) { ti->period = TT_NULL; // no time limit, rely on engine default } else { ti->period = main_time > 0 ? TT_TOTAL : TT_MOVE; ti->dim = TD_WALLTIME; ti->len.t.timer_start = 0; ti->len.t.main_time = (double) main_time; ti->len.t.byoyomi_time = (double) byoyomi_time; ti->len.t.byoyomi_periods = byoyomi_periods; ti->len.t.byoyomi_stones = byoyomi_stones; ti->len.t.canadian = byoyomi_stones > 0; if (byoyomi_time > 0) { /* Normally, only one of byoyomi_periods and * byoyomi_stones arguments will be > 0. However, * our data structure uses generalized byoyomi * specification that will assume "1 byoyomi period * of N stones" for Canadian byoyomi and "N byoyomi * periods of 1 stone" for Japanese byoyomi. */ if (ti->len.t.byoyomi_periods < 1) ti->len.t.byoyomi_periods = 1; if (ti->len.t.byoyomi_stones < 1) ti->len.t.byoyomi_stones = 1; } else { assert(!ti->len.t.byoyomi_periods && !ti->len.t.byoyomi_stones); } ti->len.t.byoyomi_time_max = ti->len.t.byoyomi_time; ti->len.t.byoyomi_stones_max = ti->len.t.byoyomi_stones; } } /* 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_stop_conditions still work. */ void time_left(struct time_info *ti, int time_left, int stones_left) { assert(ti->period != TT_NULL); ti->dim = TD_WALLTIME; if (!time_left && !stones_left) { /* Some GTP peers send time_left 0 0 at the end of main time. */ ti->period = TT_MOVE; ti->len.t.main_time = 0; ti->len.t.byoyomi_time = ti->len.t.byoyomi_time_max; ti->len.t.byoyomi_stones = ti->len.t.byoyomi_stones_max; } else if (!stones_left) { /* Main time */ ti->period = TT_TOTAL; ti->len.t.main_time = time_left; ti->len.t.byoyomi_time = ti->len.t.byoyomi_time_max; ti->len.t.byoyomi_stones = ti->len.t.byoyomi_stones_max; } else { /* Byoyomi */ ti->period = TT_MOVE; ti->len.t.main_time = 0; ti->len.t.byoyomi_time = time_left; if (ti->len.t.canadian) { ti->len.t.byoyomi_stones = stones_left; } else { // field misused by kgs ti->len.t.byoyomi_periods = 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) { if (ti->period != TT_NULL && ti->dim == TD_WALLTIME) ti->len.t.timer_start = time_now(); } void time_sub(struct time_info *ti, double interval, bool new_move) { assert(ti->dim == TD_WALLTIME && ti->period != TT_NULL); if (ti->period == TT_TOTAL) { ti->len.t.main_time -= interval; if (ti->len.t.main_time >= 0) return; if (ti->len.t.byoyomi_time <= 0) { /* No byoyomi to save us. */ fprintf(stderr, "*** LOST ON TIME internally! (%0.2f, spent %0.2fs on last move)\n", ti->len.t.main_time, interval); /* What can we do? Pretend this didn't happen. */ ti->len.t.main_time = 1.0f; return; } /* Fall-through to byoyomi. */ ti->period = TT_MOVE; interval = -ti->len.t.main_time; ti->len.t.main_time = 0; } ti->len.t.byoyomi_time -= interval; if (ti->len.t.byoyomi_time < 0) { /* Lost a period. */ if (--ti->len.t.byoyomi_periods < 1) { fprintf(stderr, "*** LOST ON TIME internally! (%0.2f, spent %0.2fs on last move)\n", ti->len.t.byoyomi_time, interval); /* Well, what can we do? Pretend this didn't happen. */ ti->len.t.byoyomi_periods = 1; } ti->len.t.byoyomi_time = ti->len.t.byoyomi_time_max; ti->len.t.byoyomi_stones = ti->len.t.byoyomi_stones_max; return; } if (new_move && --ti->len.t.byoyomi_stones < 1) { /* Finished a period. */ ti->len.t.byoyomi_time = ti->len.t.byoyomi_time_max; ti->len.t.byoyomi_stones = ti->len.t.byoyomi_stones_max; } } /* Returns the current time. */ double time_now(void) { #if _POSIX_TIMERS > 0 struct timespec now; clock_gettime(CLOCK_REALTIME, &now); return now.tv_sec + now.tv_nsec/1000000000.0; #else struct timeval now; gettimeofday(&now, NULL); return now.tv_sec + now.tv_usec/1000000.0; #endif } /* Sleep for a given interval (in seconds). Return immediately if interval < 0. */ void time_sleep(double interval) { #ifdef _WIN32 unsigned int t = interval * 1000.0; Sleep(t); #else struct timespec ts; double sec; ts.tv_nsec = (int)(modf(interval, &sec)*1000000000.0); ts.tv_sec = (int)sec; nanosleep(&ts, NULL); /* ignore error if interval was < 0 */ #endif } /* Returns true if we are in byoyomi (or should play as if in byo yomi * because remaining time per move in main time is less than byoyomi time * per move). */ static bool time_in_byoyomi(struct time_info *ti) { assert(ti->dim == TD_WALLTIME); if (!ti->len.t.byoyomi_time) return false; // there is no byoyomi! assert(ti->len.t.byoyomi_stones > 0); if (!ti->len.t.main_time) return true; // we _are_ in byoyomi if (ti->len.t.main_time <= ti->len.t.byoyomi_time / ti->len.t.byoyomi_stones + 0.001) return true; // our basic time left is less than byoyomi time per move return false; } /* Set worst.time to all available remaining time (main time plus usable * byoyomi), to be spread over returned number of moves (expected game * length minus moves to be played in final byoyomi - if we would not be * able to spend more time on them in main time anyway). */ static int time_stop_set_remaining(struct time_info *ti, struct board *b, double net_lag, struct time_stop *stop) { int moves_left = board_estimated_moves_left(b); stop->worst.time = ti->len.t.main_time; if (!ti->len.t.byoyomi_time) return moves_left; /* Time for one move in byoyomi. */ assert(ti->len.t.byoyomi_stones > 0); double move_time = ti->len.t.byoyomi_time / ti->len.t.byoyomi_stones; /* (i) Plan to extend our thinking time to make use of byoyom. */ /* For Japanese byoyomi with N>1 periods, we use N-1 periods * as main time, keeping the last one as insurance against * unexpected net lag. */ if (ti->len.t.byoyomi_periods > 2) { stop->worst.time += (ti->len.t.byoyomi_periods - 2) * move_time; // Will add 1 more byoyomi_time just below } /* In case of Canadian byoyomi, include time that can be spent * on its first move. */ stop->worst.time += move_time; /* (ii) Do not play faster in main time than we would in byoyomi. */ /* 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 stop->worst.time and * already includes the first (canadian) or N-1 byoyomi periods. */ double real_move_time = move_time - net_lag; if (real_move_time > 0) { int main_moves = stop->worst.time / real_move_time; if (moves_left > main_moves) { /* We plan to do too many moves in main time, * do the rest in byoyomi. */ moves_left = main_moves; } if (moves_left <= 0) // possible if too much lag moves_left = 1; } return moves_left; } /* Adjust the recommended per-move time based on the current game phase. * We expect stop->worst to be total time available, stop->desired the current * per-move time allocation, and set stop->desired to adjusted per-move time. */ static void time_stop_phase_adjust(struct board *b, int fuseki_end, int yose_start, struct time_stop *stop) { int bsize = (board_size(b)-2)*(board_size(b)-2); fuseki_end = fuseki_end * bsize / 100; // move nb at fuseki end yose_start = yose_start * bsize / 100; // move nb at yose start assert(fuseki_end < yose_start); /* No adjustments in yose. */ if (b->moves >= yose_start) return; int moves_to_yose = (yose_start - b->moves) / 2; // ^- /2 because we only consider the moves we have to play ourselves int left_at_yose_start = board_estimated_moves_left(b) - moves_to_yose; if (left_at_yose_start < MIN_MOVES_LEFT) left_at_yose_start = MIN_MOVES_LEFT; /* This particular value of middlegame_time will continuously converge * to effective "yose_time" value as we approach yose_start. */ double middlegame_time = stop->worst.time / left_at_yose_start; if (middlegame_time < stop->desired.time) return; if (b->moves < fuseki_end) { assert(fuseki_end > 0); /* At the game start, use stop->desired.time (rather * conservative estimate), then gradually prolong it. */ double beta = b->moves / fuseki_end; stop->desired.time = middlegame_time * beta + stop->desired.time * (1 - beta); } else { assert(b->moves < yose_start); /* Middlegame, start with relatively large value, then * converge to the uniform-timeslice yose value. */ stop->desired.time = middlegame_time; } } void lag_adjust(double *time, double net_lag) { double nolag_time = *time; *time -= net_lag; if (*time < MIN_THINK_WITH_LAG && nolag_time > MIN_THINK_WITH_LAG) *time = MIN_THINK_WITH_LAG; } /* Pre-process time_info for search control and sets the desired stopping conditions. */ void time_stop_conditions(struct time_info *ti, struct board *b, int fuseki_end, int yose_start, floating_t max_maintime_ratio, struct time_stop *stop) { /* We must have _some_ limits by now, be it random default values! */ assert(ti->period != TT_NULL); /* Special-case limit by number of simulations. */ if (ti->dim == TD_GAMES) { if (ti->period == TT_TOTAL) { ti->period = TT_MOVE; ti->len.games /= board_estimated_moves_left(b); } stop->desired.playouts = ti->len.games; /* We force worst == desired, so note that we will NOT loop * until best == winner. */ stop->worst.playouts = ti->len.games; return; } assert(ti->dim == TD_WALLTIME); /* Minimum net lag (seconds) to be reserved in the time for move. */ double net_lag = MAX_NET_LAG; net_lag += time_now() - ti->len.t.timer_start; // TODO: keep statistics to get good estimate of lag not just current move if (ti->period == TT_TOTAL && time_in_byoyomi(ti)) { /* Technically, we are still in main time, but we can * effectively switch to byoyomi scheduling since we * have less time available than one byoyomi move takes. */ ti->period = TT_MOVE; } if (ti->period == TT_MOVE) { /* We are in byoyomi, or almost! */ /* The period can still include some tiny remnant of main * time if we are just switching to byoyomi. */ double period_len = ti->len.t.byoyomi_time + ti->len.t.main_time; stop->worst.time = period_len; assert(ti->len.t.byoyomi_stones > 0); stop->desired.time = period_len / ti->len.t.byoyomi_stones; /* Use a larger safety margin if we risk losing on time on * this move; it makes no sense to have 30s byoyomi and wait * until 28s to play our move). */ if (stop->desired.time >= period_len - net_lag) { double safe_margin = RESERVED_BYOYOMI_PERCENT * stop->desired.time / 100; if (safe_margin > net_lag) net_lag = safe_margin; } /* Make recommended_old == average(recommended_new, max) */ double worst_time = stop->desired.time * MAX_BYOYOMI_TIME_RATIO; if (worst_time < stop->worst.time) stop->worst.time = worst_time; stop->desired.time *= (2 - MAX_BYOYOMI_TIME_RATIO); } else { assert(ti->period == TT_TOTAL); /* We are in main time. */ assert(ti->len.t.main_time > 0); /* Set worst.time to all available remaining time, to be spread * over returned number of moves. */ int moves_left = time_stop_set_remaining(ti, b, net_lag, stop); /* Allocate even slice of the remaining time for next move. */ stop->desired.time = stop->worst.time / moves_left; assert(stop->desired.time > 0 && stop->worst.time > 0); assert(stop->desired.time <= stop->worst.time + 0.001); /* Furthermore, tweak the slice based on the game phase. */ time_stop_phase_adjust(b, fuseki_end, yose_start, stop); /* Put final upper bound on maximal time spent on the move. * Keep enough time for sudden death (or near SD) games. */ double worst_time = stop->desired.time; if (ti->len.t.byoyomi_time_max > ti->len.t.byoyomi_stones_max) { worst_time *= max_maintime_ratio; } else { worst_time *= MAX_SUDDEN_DEATH_RATIO; } if (worst_time < stop->worst.time) stop->worst.time = worst_time; if (stop->desired.time > stop->worst.time) stop->desired.time = stop->worst.time; } if (DEBUGL(1)) fprintf(stderr, "desired %0.2f, worst %0.2f, clock [%d] %0.2f + %0.2f/%d*%d, lag %0.2f\n", stop->desired.time, stop->worst.time, ti->dim, ti->len.t.main_time, ti->len.t.byoyomi_time, ti->len.t.byoyomi_stones, ti->len.t.byoyomi_periods, net_lag); /* Account for lag. */ lag_adjust(&stop->desired.time, net_lag); lag_adjust(&stop->worst.time, net_lag); }