10 #include "tactics/util.h"
11 #include "uct/dynkomi.h"
12 #include "uct/internal.h"
17 generic_done(struct uct_dynkomi
*d
)
19 if (d
->data
) free(d
->data
);
24 /* NONE dynkomi strategy - never fiddle with komi values. */
27 uct_dynkomi_init_none(struct uct
*u
, char *arg
, struct board
*b
)
29 struct uct_dynkomi
*d
= calloc2(1, sizeof(*d
));
33 d
->done
= generic_done
;
37 fprintf(stderr
, "uct: Dynkomi method none accepts no arguments\n");
45 /* LINEAR dynkomi strategy - Linearly Decreasing Handicap Compensation. */
46 /* At move 0, we impose extra komi of handicap_count*handicap_value, then
47 * we linearly decrease this extra komi throughout the game down to 0
50 struct dynkomi_linear
{
51 int handicap_value
[S_MAX
];
57 linear_permove(struct uct_dynkomi
*d
, struct board
*b
, struct tree
*tree
)
59 struct dynkomi_linear
*l
= d
->data
;
60 enum stone color
= stone_other(tree
->root_color
);
61 int lmoves
= l
->moves
[color
];
62 if (b
->moves
>= lmoves
)
65 floating_t base_komi
= board_effective_handicap(b
, l
->handicap_value
[color
]);
66 floating_t extra_komi
= base_komi
* (lmoves
- b
->moves
) / lmoves
;
71 linear_persim(struct uct_dynkomi
*d
, struct board
*b
, struct tree
*tree
, struct tree_node
*node
)
73 struct dynkomi_linear
*l
= d
->data
;
75 return tree
->extra_komi
;
76 /* We don't reuse computed value from tree->extra_komi,
77 * since we want to use value correct for this node depth.
78 * This also means the values will stay correct after
80 return linear_permove(d
, b
, tree
);
84 uct_dynkomi_init_linear(struct uct
*u
, char *arg
, struct board
*b
)
86 struct uct_dynkomi
*d
= calloc2(1, sizeof(*d
));
88 d
->permove
= linear_permove
;
89 d
->persim
= linear_persim
;
90 d
->done
= generic_done
;
92 struct dynkomi_linear
*l
= calloc2(1, sizeof(*l
));
95 /* Force white to feel behind and try harder, but not to the
96 * point of resigning immediately in high handicap games.
97 * By move 100 white should still be behind but should have
98 * caught up enough to avoid resigning. */
100 l
->moves
[S_BLACK
] = 200;
101 l
->moves
[S_WHITE
] = 100;
103 /* The real value of one stone is twice the komi so about 15 points.
104 * But use a lower value to avoid being too pessimistic as black
105 * or too optimistic as white. */
106 l
->handicap_value
[S_BLACK
] = 7;
107 l
->handicap_value
[S_WHITE
] = 5;
110 char *optspec
, *next
= arg
;
113 next
+= strcspn(next
, ":");
114 if (*next
) { *next
++ = 0; } else { *next
= 0; }
116 char *optname
= optspec
;
117 char *optval
= strchr(optspec
, '=');
118 if (optval
) *optval
++ = 0;
120 if (!strcasecmp(optname
, "moves") && optval
) {
121 /* Dynamic komi in handicap game; linearly
122 * decreases to basic settings until move
123 * #optval. moves=blackmoves%whitemoves */
124 for (int i
= S_BLACK
; *optval
&& i
<= S_WHITE
; i
++, optval
+= strcspn(optval
, "%")) {
126 l
->moves
[i
] = atoi(optval
);
128 } else if (!strcasecmp(optname
, "handicap_value") && optval
) {
129 /* Point value of single handicap stone,
130 * for dynkomi computation. */
131 for (int i
= S_BLACK
; *optval
&& i
<= S_WHITE
; i
++, optval
+= strcspn(optval
, "%")) {
133 l
->handicap_value
[i
] = atoi(optval
);
135 } else if (!strcasecmp(optname
, "rootbased")) {
136 /* If set, the extra komi applied will be
137 * the same for all simulations within a move,
138 * instead of being same for all simulations
139 * within the tree node. */
140 l
->rootbased
= !optval
|| atoi(optval
);
142 fprintf(stderr
, "uct: Invalid dynkomi argument %s or missing value\n", optname
);
152 /* ADAPTIVE dynkomi strategy - Adaptive Situational Compensation */
153 /* We adapt the komi based on current situation:
154 * (i) score-based: We maintain the average score outcome of our
155 * games and adjust the komi by a fractional step towards the expected
157 * (ii) value-based: While winrate is above given threshold, adjust
158 * the komi by a fixed step in the appropriate direction.
159 * These adjustments can be
160 * (a) Move-stepped, new extra komi value is always set only at the
161 * beginning of the tree search for next move;
162 * (b) Continuous, new extra komi value is periodically re-determined
163 * and adjusted throughout a single tree search. */
165 struct dynkomi_adaptive
{
166 /* Do not take measured average score into regard for
167 * first @lead_moves - the variance is just too much.
168 * (Instead, we consider the handicap-based komi provided
169 * by linear dynkomi.) */
171 /* Maximum komi to pretend the opponent to give. */
172 floating_t max_losing_komi
;
173 /* Game portion at which losing komi is not allowed anymore. */
174 floating_t losing_komi_stop
;
175 /* Alternative game portion determination. */
177 floating_t (*indicator
)(struct uct_dynkomi
*d
, struct board
*b
, struct tree
*tree
, enum stone color
);
179 /* Value-based adaptation. */
180 floating_t zone_red
, zone_green
;
182 floating_t score_step_byavg
; // use portion of average score as increment
183 bool use_komi_ratchet
;
184 bool losing_komi_ratchet
; // ratchet even losing komi
185 int komi_ratchet_maxage
;
186 // runtime, not configuration:
187 int komi_ratchet_age
;
188 floating_t komi_ratchet
;
190 /* Score-based adaptation. */
191 floating_t (*adapter
)(struct uct_dynkomi
*d
, struct board
*b
);
192 floating_t adapt_base
; // [0,1)
193 /* Sigmoid adaptation rate parameter; see below for details. */
194 floating_t adapt_phase
; // [0,1]
195 floating_t adapt_rate
; // [1,infty)
196 /* Linear adaptation rate parameter. */
198 floating_t adapt_dir
; // [-1,1]
200 #define TRUSTWORTHY_KOMI_PLAYOUTS 200
203 board_game_portion(struct dynkomi_adaptive
*a
, struct board
*b
)
205 if (!a
->adapt_aport
) {
206 int total_moves
= b
->moves
+ 2 * board_estimated_moves_left(b
);
207 return (floating_t
) b
->moves
/ total_moves
;
209 int brsize
= board_size(b
) - 2;
210 return 1.0 - (floating_t
) b
->flen
/ (brsize
* brsize
);
215 adapter_sigmoid(struct uct_dynkomi
*d
, struct board
*b
)
217 struct dynkomi_adaptive
*a
= d
->data
;
218 /* Figure out how much to adjust the komi based on the game
219 * stage. The adaptation rate is 0 at the beginning,
220 * at game stage a->adapt_phase crosses though 0.5 and
221 * approaches 1 at the game end; the slope is controlled
222 * by a->adapt_rate. */
223 floating_t game_portion
= board_game_portion(a
, b
);
224 floating_t l
= game_portion
- a
->adapt_phase
;
225 return 1.0 / (1.0 + exp(-a
->adapt_rate
* l
));
229 adapter_linear(struct uct_dynkomi
*d
, struct board
*b
)
231 struct dynkomi_adaptive
*a
= d
->data
;
232 /* Figure out how much to adjust the komi based on the game
233 * stage. We just linearly increase/decrease the adaptation
234 * rate for first N moves. */
235 if (b
->moves
> a
->adapt_moves
)
237 if (a
->adapt_dir
< 0)
238 return 1 - (- a
->adapt_dir
) * b
->moves
/ a
->adapt_moves
;
240 return a
->adapt_dir
* b
->moves
/ a
->adapt_moves
;
244 komi_by_score(struct uct_dynkomi
*d
, struct board
*b
, struct tree
*tree
, enum stone color
)
246 struct dynkomi_adaptive
*a
= d
->data
;
247 if (d
->score
.playouts
< TRUSTWORTHY_KOMI_PLAYOUTS
)
248 return tree
->extra_komi
;
250 struct move_stats score
= d
->score
;
251 /* Almost-reset tree->score to gather fresh stats. */
252 d
->score
.playouts
= 1;
254 /* Look at average score and push extra_komi in that direction. */
255 floating_t p
= a
->adapter(d
, b
);
256 p
= a
->adapt_base
+ p
* (1 - a
->adapt_base
);
257 if (p
> 0.9) p
= 0.9; // don't get too eager!
258 floating_t extra_komi
= tree
->extra_komi
+ p
* score
.value
;
260 fprintf(stderr
, "mC += %f * %f\n", p
, score
.value
);
265 komi_by_value(struct uct_dynkomi
*d
, struct board
*b
, struct tree
*tree
, enum stone color
)
267 struct dynkomi_adaptive
*a
= d
->data
;
268 if (d
->value
.playouts
< TRUSTWORTHY_KOMI_PLAYOUTS
)
269 return tree
->extra_komi
;
271 struct move_stats value
= d
->value
;
272 /* Almost-reset tree->value to gather fresh stats. */
273 d
->value
.playouts
= 1;
274 /* Correct color POV. */
275 if (color
== S_WHITE
)
276 value
.value
= 1 - value
.value
;
278 /* We have three "value zones":
279 * red zone | yellow zone | green zone
281 * red zone: reduce komi
282 * yellow zone: do not touch komi
283 * green zone: enlage komi.
285 * Also, at some point komi will be tuned in such way
286 * that it will be in green zone but increasing it will
287 * be unfeasible. Thus, we have a _ratchet_ - we will
288 * remember the last komi that has put us into the
289 * red zone, and not use it or go over it. We use the
290 * ratchet only when giving extra komi, we always want
291 * to try to reduce extra komi we take.
293 * TODO: Make the ratchet expire after a while. */
295 /* We use komi_by_color() first to normalize komi
296 * additions/subtractions, then apply it again on
297 * return value to restore original komi parity. */
298 /* Positive extra_komi means that we are _giving_
299 * komi (winning), negative extra_komi is _taking_
301 floating_t extra_komi
= komi_by_color(tree
->extra_komi
, color
);
302 int score_step_red
= -a
->score_step
;
303 int score_step_green
= a
->score_step
;
305 if (a
->score_step_byavg
!= 0) {
306 struct move_stats score
= d
->score
;
307 /* Almost-reset tree->score to gather fresh stats. */
308 d
->score
.playouts
= 1;
309 /* Correct color POV. */
310 if (color
== S_WHITE
)
311 score
.value
= - score
.value
;
313 score_step_green
= round(score
.value
* a
->score_step_byavg
);
315 score_step_red
= round(-score
.value
* a
->score_step_byavg
);
316 if (score_step_green
< 0 || score_step_red
> 0) {
317 /* The steps are in bad direction - keep still. */
318 return komi_by_color(extra_komi
, color
);
322 /* Wear out the ratchet. */
323 if (a
->use_komi_ratchet
&& a
->komi_ratchet_maxage
> 0) {
324 a
->komi_ratchet_age
+= value
.playouts
;
325 if (a
->komi_ratchet_age
> a
->komi_ratchet_maxage
) {
326 a
->komi_ratchet
= 1000;
327 a
->komi_ratchet_age
= 0;
331 if (value
.value
< a
->zone_red
) {
332 /* Red zone. Take extra komi. */
334 fprintf(stderr
, "[red] %f, step %d | komi ratchet %f age %d/%d -> %f\n",
335 value
.value
, score_step_red
, a
->komi_ratchet
, a
->komi_ratchet_age
, a
->komi_ratchet_maxage
, extra_komi
);
336 if (a
->losing_komi_ratchet
|| extra_komi
> 0) {
337 a
->komi_ratchet
= extra_komi
;
338 a
->komi_ratchet_age
= 0;
340 extra_komi
+= score_step_red
;
341 return komi_by_color(extra_komi
, color
);
343 } else if (value
.value
< a
->zone_green
) {
344 /* Yellow zone, do nothing. */
345 return komi_by_color(extra_komi
, color
);
348 /* Green zone. Give extra komi. */
350 fprintf(stderr
, "[green] %f, step %d | komi ratchet %f age %d/%d\n",
351 value
.value
, score_step_green
, a
->komi_ratchet
, a
->komi_ratchet_age
, a
->komi_ratchet_maxage
);
352 extra_komi
+= score_step_green
;
353 if (a
->use_komi_ratchet
&& extra_komi
>= a
->komi_ratchet
)
354 extra_komi
= a
->komi_ratchet
- 1;
355 return komi_by_color(extra_komi
, color
);
360 bounded_komi(struct dynkomi_adaptive
*a
, struct board
*b
,
361 enum stone color
, floating_t komi
, floating_t max_losing_komi
)
363 /* At the end of game, disallow losing komi. */
364 if (komi_by_color(komi
, color
) < 0
365 && board_game_portion(a
, b
) > a
->losing_komi_stop
)
368 /* Get lower bound on komi we take so that we don't underperform
370 floating_t min_komi
= komi_by_color(- max_losing_komi
, color
);
372 if (komi_by_color(komi
- min_komi
, color
) > 0)
379 adaptive_permove(struct uct_dynkomi
*d
, struct board
*b
, struct tree
*tree
)
381 struct dynkomi_adaptive
*a
= d
->data
;
382 enum stone color
= stone_other(tree
->root_color
);
384 fprintf(stderr
, "m %d/%d ekomi %f permove %f/%d\n",
385 b
->moves
, a
->lead_moves
, tree
->extra_komi
,
386 d
->score
.value
, d
->score
.playouts
);
388 if (b
->moves
<= a
->lead_moves
)
389 return bounded_komi(a
, b
, color
,
390 board_effective_handicap(b
, 7 /* XXX */),
393 floating_t komi
= a
->indicator(d
, b
, tree
, color
);
395 fprintf(stderr
, "dynkomi: %f -> %f\n", tree
->extra_komi
, komi
);
396 return bounded_komi(a
, b
, color
, komi
, a
->max_losing_komi
);
400 adaptive_persim(struct uct_dynkomi
*d
, struct board
*b
, struct tree
*tree
, struct tree_node
*node
)
402 return tree
->extra_komi
;
406 uct_dynkomi_init_adaptive(struct uct
*u
, char *arg
, struct board
*b
)
408 struct uct_dynkomi
*d
= calloc2(1, sizeof(*d
));
410 d
->permove
= adaptive_permove
;
411 d
->persim
= adaptive_persim
;
412 d
->done
= generic_done
;
414 struct dynkomi_adaptive
*a
= calloc2(1, sizeof(*a
));
417 a
->lead_moves
= board_large(b
) ? 20 : 4; // XXX
418 a
->max_losing_komi
= 30;
419 a
->losing_komi_stop
= 1.0f
;
420 a
->indicator
= komi_by_value
;
422 a
->adapter
= adapter_sigmoid
;
424 a
->adapt_phase
= 0.65;
425 a
->adapt_moves
= 200;
429 a
->zone_green
= 0.50;
431 a
->use_komi_ratchet
= true;
432 a
->komi_ratchet_maxage
= 0;
433 a
->komi_ratchet
= 1000;
436 char *optspec
, *next
= arg
;
439 next
+= strcspn(next
, ":");
440 if (*next
) { *next
++ = 0; } else { *next
= 0; }
442 char *optname
= optspec
;
443 char *optval
= strchr(optspec
, '=');
444 if (optval
) *optval
++ = 0;
446 if (!strcasecmp(optname
, "lead_moves") && optval
) {
447 /* Do not adjust komi adaptively for first
449 a
->lead_moves
= atoi(optval
);
450 } else if (!strcasecmp(optname
, "max_losing_komi") && optval
) {
451 a
->max_losing_komi
= atof(optval
);
452 } else if (!strcasecmp(optname
, "losing_komi_stop") && optval
) {
453 a
->losing_komi_stop
= atof(optval
);
454 } else if (!strcasecmp(optname
, "indicator")) {
455 /* Adaptatation indicator - how to decide
456 * the adaptation rate and direction. */
457 if (!strcasecmp(optval
, "value")) {
458 /* Winrate w/ komi so far. */
459 a
->indicator
= komi_by_value
;
460 } else if (!strcasecmp(optval
, "score")) {
461 /* Expected score w/ current komi. */
462 a
->indicator
= komi_by_score
;
464 fprintf(stderr
, "UCT: Invalid indicator %s\n", optval
);
468 /* value indicator settings */
469 } else if (!strcasecmp(optname
, "zone_red") && optval
) {
470 a
->zone_red
= atof(optval
);
471 } else if (!strcasecmp(optname
, "zone_green") && optval
) {
472 a
->zone_green
= atof(optval
);
473 } else if (!strcasecmp(optname
, "score_step") && optval
) {
474 a
->score_step
= atoi(optval
);
475 } else if (!strcasecmp(optname
, "score_step_byavg") && optval
) {
476 a
->score_step_byavg
= atof(optval
);
477 } else if (!strcasecmp(optname
, "use_komi_ratchet")) {
478 a
->use_komi_ratchet
= !optval
|| atoi(optval
);
479 } else if (!strcasecmp(optname
, "losing_komi_ratchet")) {
480 a
->losing_komi_ratchet
= !optval
|| atoi(optval
);
481 } else if (!strcasecmp(optname
, "komi_ratchet_age") && optval
) {
482 a
->komi_ratchet_maxage
= atoi(optval
);
484 /* score indicator settings */
485 } else if (!strcasecmp(optname
, "adapter") && optval
) {
486 /* Adaptatation method. */
487 if (!strcasecmp(optval
, "sigmoid")) {
488 a
->adapter
= adapter_sigmoid
;
489 } else if (!strcasecmp(optval
, "linear")) {
490 a
->adapter
= adapter_linear
;
492 fprintf(stderr
, "UCT: Invalid adapter %s\n", optval
);
495 } else if (!strcasecmp(optname
, "adapt_base") && optval
) {
496 /* Adaptation base rate; see above. */
497 a
->adapt_base
= atof(optval
);
498 } else if (!strcasecmp(optname
, "adapt_rate") && optval
) {
499 /* Adaptation slope; see above. */
500 a
->adapt_rate
= atof(optval
);
501 } else if (!strcasecmp(optname
, "adapt_phase") && optval
) {
502 /* Adaptation phase shift; see above. */
503 a
->adapt_phase
= atof(optval
);
504 } else if (!strcasecmp(optname
, "adapt_moves") && optval
) {
505 /* Adaptation move amount; see above. */
506 a
->adapt_moves
= atoi(optval
);
507 } else if (!strcasecmp(optname
, "adapt_aport")) {
508 a
->adapt_aport
= !optval
|| atoi(optval
);
509 } else if (!strcasecmp(optname
, "adapt_dir") && optval
) {
510 /* Adaptation direction vector; see above. */
511 a
->adapt_dir
= atof(optval
);
514 fprintf(stderr
, "uct: Invalid dynkomi argument %s or missing value\n", optname
);