magicciv/tooling/rl_self_play/tests/test_harness_client.py

74 lines
2.8 KiB
Python

"""Unit tests for `tooling.rl_self_play.harness_client.HarnessConfig`.
We don't spawn the Godot subprocess here (smoke.py already exercises
that). We test the env-var serialisation contract added in commit
e103928d2: `player_slots`, `player_controllers`, and the
`effective_player_slots` back-compat fallback. The `mc-player-api`
dispatcher reads CP_PLAYER_SLOTS / CP_PLAYER_CONTROLLERS verbatim, so
a regression in this serialisation silently corrupts multi-slot RL
training.
"""
from __future__ import annotations
from tooling.rl_self_play.harness_client import HarnessConfig
def test_defaults_serialise_single_slot_layout() -> None:
cfg = HarnessConfig()
env = cfg.to_env()
assert env["CP_SEED"] == "42"
assert env["CP_PLAYERS"] == "2"
assert env["CP_PLAYER_SLOT"] == "0"
# Back-compat: when player_slots is empty, the wire still receives
# a single-slot list derived from player_slot.
assert env["CP_PLAYER_SLOTS"] == "0"
assert env["CP_MAP_SIZE"] == "duel"
assert env["CP_MAP_TYPE"] == "continents"
assert env["CP_OMNISCIENT"] == "0"
assert env["CP_VICTORY_MODE"] == "domination"
# Controllers are emitted ONLY when explicitly configured — empty
# means "every AI slot uses scripted:default".
assert "CP_PLAYER_CONTROLLERS" not in env
def test_effective_player_slots_back_compat_fallback() -> None:
cfg = HarnessConfig(player_slot=2)
assert cfg.effective_player_slots == (2,)
cfg = HarnessConfig(player_slot=0, player_slots=(0, 1, 3))
assert cfg.effective_player_slots == (0, 1, 3)
def test_multi_slot_serialisation_uses_first_slot_for_legacy_var() -> None:
"""When driving multiple slots, CP_PLAYER_SLOT keeps the legacy
single-slot consumer happy by naming the first slot, while
CP_PLAYER_SLOTS carries the full tuple."""
cfg = HarnessConfig(player_slots=(0, 2, 4))
env = cfg.to_env()
assert env["CP_PLAYER_SLOT"] == "0"
assert env["CP_PLAYER_SLOTS"] == "0,2,4"
def test_player_controllers_serialised_when_set() -> None:
cfg = HarnessConfig(
player_controllers=("learned:duel-v1b", "", "scripted:aggressive"),
)
env = cfg.to_env()
assert env["CP_PLAYER_CONTROLLERS"] == "learned:duel-v1b,,scripted:aggressive"
def test_omniscient_and_victory_mode_round_trip() -> None:
cfg = HarnessConfig(omniscient=True, victory_mode="science")
env = cfg.to_env()
assert env["CP_OMNISCIENT"] == "1"
assert env["CP_VICTORY_MODE"] == "science"
def test_config_is_frozen_dataclass() -> None:
cfg = HarnessConfig()
import dataclasses
assert dataclasses.is_dataclass(cfg)
try:
cfg.seed = 99 # type: ignore[misc]
except dataclasses.FrozenInstanceError:
return
raise AssertionError("HarnessConfig must remain a frozen dataclass")