eval.doubles

eval.doubles

What Raccoon’s two-step doubles execution costs in play (exp020).

OpenSpiel splits a doubles turn into two consecutive decisions by the same player, two of the four half-moves each. GNUBG’s side of the arena (:func:raccoon.eval.gnubg_adapter.candidate_equities) recurses through both halves and optimises the turn jointly. Raccoon’s side historically did not: it ranked half 1 by the static value of the intermediate position — which :func:encode_pre_roll hands to the value head with the dice and the mid-doubles flag cleared, so the net is told “you are about to roll” about a position where the mover still owes two half-moves — and then picked half 2 greedily. On 1/6 of turns the two engines were not playing by the same rule, and only one of them was handicapped.

This module measures the gap directly, on the distribution where it matters:

concession = eq_gnubg(final_joint) - eq_gnubg(final_greedy)

Both finals come from the same network, so the teacher’s own move choice never enters and this is purely a question of how the net’s value function is searched. The main line plays the greedy arm by default (play_joint=False), so the sample is the exp019 playing distribution — the one whose ppg shortfall prompted the experiment — and the joint arm is scored as a counterfactual on each turn.

Two properties keep the measurement honest:

  • Non-doubles turns are a control, not filler. A turn that needs one OpenSpiel action has one leaf per legal action, so joint and greedy enumerate the identical set and must agree exactly. Any non-zero concession there is a harness bug — a bad resting signature, a sign error, a mis-judged terminal — not a strength difference. tests/test_doubles.py pins the same property at the child_values level.
  • Measuring does not perturb the games. Both arms are explored on clones, and neither select_move at temperature 0 nor evaluate_equity touches the RNG, so a run with the same seed visits the same positions as a plain gnubg_arena.

Judged at judge_ply (default 2 — the strongest affordable referee, and the tier the distillation teacher used). GNUBG is only consulted when the two arms actually disagree; when they land on the same position the concession is exactly zero and no evaluation is needed, which is what keeps the run affordable.

Functions

Name Description
doubles_concession_arena Play net vs GNUBG and score both doubles arms at every net turn.

doubles_concession_arena

eval.doubles.doubles_concession_arena(
    net,
    device,
    games,
    gnubg_ply=0,
    judge_ply=2,
    seed=0,
    max_moves=2000,
    play_joint=False,
)

Play net vs GNUBG and score both doubles arms at every net turn.

Returns per-turn arrays (two_step, disagreed, concession, concession_0ply, n_paths) plus per-game points, so a caller can compute an empirical CI on the concession and cross-check the raw ppg against exp019. Seats alternated; seeds the global numpy RNG (dice), matching :func:raccoon.train.td_selfplay.gnubg_arena.