Add discard_only opponent_policy + analyze.py merges
- Plumb discard_only through config validator and interleaved_traversal. Bypasses PolicyRequest for opponent nodes, uses DiscardOnlyBot via Snapshot. Recursive scheduler explicitly rejected (Cython unchanged). - analyze.py: merge per-opponent eval plots into multi-line plots, add twin y-axis support (PlotSpec.secondary_metrics), per-axes translucent legends instead of one global legend, add avg_game_length to GameFlow. - Tests: discard_only smoke run, validator accept/reject, all 57 pass.
This commit is contained in:
@@ -138,7 +138,9 @@ def test_deep_cfr_config_accepts_interleaved_scheduler() -> None:
|
||||
|
||||
|
||||
def test_deep_cfr_config_rejects_unsupported_interleaved_options() -> None:
|
||||
with pytest.raises(ValueError, match="opponent_policy='network' or 'average_strategy'"):
|
||||
with pytest.raises(
|
||||
ValueError, match="opponent_policy='network', 'average_strategy', or 'discard_only'"
|
||||
):
|
||||
_deep_cfr_config(
|
||||
{"traversal": {"scheduler": "interleaved", "opponent_policy": "self_play_league"}}
|
||||
)
|
||||
@@ -154,6 +156,20 @@ def test_deep_cfr_config_rejects_unsupported_interleaved_options() -> None:
|
||||
)
|
||||
|
||||
|
||||
def test_deep_cfr_config_accepts_discard_only_with_interleaved() -> None:
|
||||
config = _deep_cfr_config(
|
||||
{"traversal": {"scheduler": "interleaved", "opponent_policy": "discard_only"}}
|
||||
)
|
||||
assert config.traversal.opponent_policy == "discard_only"
|
||||
|
||||
|
||||
def test_deep_cfr_config_rejects_discard_only_with_recursive() -> None:
|
||||
with pytest.raises(ValueError, match="discard_only.*scheduler='interleaved'"):
|
||||
_deep_cfr_config(
|
||||
{"traversal": {"scheduler": "recursive", "opponent_policy": "discard_only"}}
|
||||
)
|
||||
|
||||
|
||||
def test_deep_cfr_train_cli_checkpoint_save_overrides() -> None:
|
||||
args = type(
|
||||
"Args",
|
||||
@@ -452,6 +468,42 @@ def test_deep_cfr_trainer_interleaved_scheduler_smoke_run(tmp_path) -> None:
|
||||
assert runtime["interleaved/avg_batch_size"] >= 1.0
|
||||
|
||||
|
||||
def test_deep_cfr_trainer_discard_only_opponent_smoke_run(tmp_path) -> None:
|
||||
trainer = DeepCFRTrainer(
|
||||
_deep_cfr_config(
|
||||
{
|
||||
"run": {"max_iterations": 1, "seed": 25},
|
||||
"network": {"hidden_size": 16},
|
||||
"traversal": {
|
||||
"scheduler": "interleaved",
|
||||
"opponent_policy": "discard_only",
|
||||
"traversals_per_player": 2,
|
||||
"max_depth": 3,
|
||||
"max_nodes_per_traversal": 64,
|
||||
"interleave_width": 4,
|
||||
"interleave_max_batch": 8,
|
||||
},
|
||||
"optimization": {
|
||||
"advantage_updates_per_iteration": 1,
|
||||
"strategy_updates_per_iteration": 1,
|
||||
"advantage_batch_size": 2,
|
||||
"strategy_batch_size": 2,
|
||||
},
|
||||
"checkpoint": {"save_every": 0, "save_latest": False},
|
||||
"evaluation": {"eval_every": 0},
|
||||
}
|
||||
),
|
||||
LostCitiesConfig(seed=25),
|
||||
run_dir=tmp_path / "discard_only",
|
||||
)
|
||||
|
||||
metrics = trainer.train()
|
||||
|
||||
assert len(metrics) == 1
|
||||
assert metrics[0].advantage_samples > 0
|
||||
assert metrics[0].traversal_nodes > 0
|
||||
|
||||
|
||||
def test_deep_cfr_interleaved_scheduler_matches_recursive_single_traversal() -> None:
|
||||
config = _deep_cfr_config(
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user