Add JAX PPO ladder v2 expert pass

This commit is contained in:
2026-07-05 02:06:22 +09:00
parent 7fe0bfdcfe
commit 6037b650f3
11 changed files with 854 additions and 3 deletions
+88 -1
View File
@@ -13,6 +13,7 @@ from lost_cities_jax.opponents import (
discard_only_action,
heuristic_balanced_action,
heuristic_cautious_action,
heuristic_expert_action,
)
from lost_cities_jax.ppo import (
JaxPPOConfig,
@@ -65,7 +66,12 @@ def tiny_config(tmp_path) -> JaxPPOConfig:
def test_static_opponents_return_legal_actions():
state = reset(jax.random.PRNGKey(0))
mask = np.asarray(legal_action_mask(state), dtype=bool)
for fn in [discard_only_action, heuristic_balanced_action, heuristic_cautious_action]:
for fn in [
discard_only_action,
heuristic_balanced_action,
heuristic_cautious_action,
heuristic_expert_action,
]:
action = int(fn(state, jnp.int32(0), jax.random.PRNGKey(1)))
assert mask[action]
@@ -112,6 +118,66 @@ def test_cautious_uses_own_board_when_playing_as_p1():
assert hand[hand_slot] == playable
def test_expert_opens_strong_ev_hand():
strong_cards = [
_hs_card(0, 0),
_rank_card(0, 7),
_rank_card(0, 8),
_rank_card(0, 9),
_rank_card(0, 10),
_rank_card(1, 2),
_rank_card(2, 2),
_rank_card(3, 2),
]
state = _manual_state(p0_hand=strong_cards, to_move=0)
action = int(heuristic_expert_action(state, jnp.int32(0), jax.random.PRNGKey(1)))
hand_slot, place_type, _ = [int(x) for x in decode_action(jnp.asarray(action))]
hand = [int(x) for x in current_hand_sorted(state, 0)]
assert place_type == PLAY
assert hand[hand_slot] // CARDS_PER_COLOR == 0
def test_expert_rejects_weak_new_open():
weak_cards = [
_rank_card(0, 2),
_rank_card(0, 3),
_rank_card(0, 4),
_rank_card(1, 2),
_rank_card(2, 2),
_rank_card(3, 2),
_rank_card(4, 2),
_rank_card(4, 3),
]
state = _manual_state(p0_hand=weak_cards, to_move=0)
action = int(heuristic_expert_action(state, jnp.int32(0), jax.random.PRNGKey(1)))
_, place_type, _ = [int(x) for x in decode_action(jnp.asarray(action))]
assert place_type != PLAY
def test_expert_avoids_discarding_immediately_useful_opponent_card():
dangerous = _rank_card(0, 8)
safe = _rank_card(4, 2)
state = _manual_state(
p0_hand=[
dangerous,
safe,
_rank_card(1, 2),
_rank_card(1, 3),
_rank_card(2, 2),
_rank_card(2, 3),
_rank_card(3, 2),
_rank_card(3, 3),
],
p1_board=[_rank_card(0, 7)],
to_move=0,
)
action = int(heuristic_expert_action(state, jnp.int32(0), jax.random.PRNGKey(1)))
hand_slot, place_type, _ = [int(x) for x in decode_action(jnp.asarray(action))]
hand = [int(x) for x in current_hand_sorted(state, 0)]
if place_type != PLAY:
assert hand[hand_slot] != dangerous
@pytest.mark.parametrize(
"policy_name", ["discard_only", "heuristic_balanced", "heuristic_cautious"]
)
@@ -128,6 +194,23 @@ def test_static_policy_duplicate_mirror_score_diff_is_zero(policy_name):
assert result["wins"] == result["losses"]
def test_expert_duplicate_mirror_regression_stats():
result = evaluate_static_mirror(
"heuristic_expert",
games=1000,
duplicate=True,
shuffle_bank_seed=20260704,
batch_games=1000,
)
assert result["games"] == 2000
assert abs(result["mean_score_diff"]) <= 1.0e-6
assert result["wins"] == result["losses"]
assert result["max_steps_rate"] < 0.05
assert 2.0 <= result["opened_colors_per_game"] <= 3.5
assert 0.30 <= result["play_action_rate"] <= 0.60
assert result["mean_game_length"] <= 70.0
def test_gate3_checkpoint_duplicate_self_mirror_score_diff_is_zero():
checkpoint = Path(GATE3_CHECKPOINT)
if not checkpoint.exists():
@@ -189,6 +272,10 @@ def _rank_card(color: int, rank: int) -> int:
return color * CARDS_PER_COLOR + rank + 1
def _hs_card(color: int, slot: int = 0) -> int:
return color * CARDS_PER_COLOR + slot
def _manual_state(
*,
p0_hand: list[int] | None = None,