Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 10 additions & 5 deletions docs/development/pufferlib-4-migration.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,10 +80,13 @@ tests on 3.9 are not evidence that the full trainer supports 3.9.
1. **CPU interface (this PR).** Preserve existing adjudication tests; compare
old/new transitions in standard and welfare modes, independent games,
terminal/reset behavior, output lifetime, logging, and input validation.
2. **Reward blocker.** Add a failing test for Winter reward timing, then fix
the engine once for both bindings. Currently `c_step` advances the phase
before checking whether to emit the Winter welfare delta. Preserving this
bug here does not make rewards suitable for training.
2. **Reward blocker (issue #11).** Four new tests reproduced dropped Winter
rewards in both bindings, including final turns (4 failed, 1 standard-mode
check passed before the fix). `c_step` now remembers the processed phase
instead of testing the next phase. Scoring rules and adjudication are unchanged.
The tests cover positive deltas, no repeat reward, reset bookkeeping and the
adapter retaining final rewards through autoreset. Review and full checks
remain required; this fix alone does not establish a usable training game.
3. **Real 4.0 training.** Isolate the pinned Python package and native module;
run a small CPU PyTorch training job on Python >=3.10. Verify nonzero reward
input, finite losses, policy updates, and checkpoint load behavior. Record
Expand Down Expand Up @@ -122,4 +125,6 @@ they check rejection without calling the unsafe native allocator. A separate
test checks acceptance of the upper bound without allocating thousands of games.
The first Linux CI run failed because strict C11 hid upstream's POSIX clock
declarations. The build now requests POSIX 2008 declarations explicitly;
implicit-declaration checking stays enabled. A passing rerun is still required.
implicit-declaration checking stays enabled. The fixed adapter passed Linux CI
run 38182492879 (247 passed, 46 skipped). The Winter reward fix passed run
38182603605 (252 passed, 46 skipped).
4 changes: 3 additions & 1 deletion pufferlib/ocean/diplomacy/diplomacy.c
Original file line number Diff line number Diff line change
Expand Up @@ -4512,6 +4512,8 @@ void c_step(Env* env) {

GameState* game = env->game;

PhaseType processed_phase = game->phase;

// Decode actions from buffer and populate orders
process_actions(env);

Expand All @@ -4538,7 +4540,7 @@ void c_step(Env* env) {
encode_observations(env);

// Calculate rewards at end of adjustment phase based on welfare deltas
if (game->phase == PHASE_WINTER_ADJUSTMENT) {
if (processed_phase == PHASE_WINTER_ADJUSTMENT) {
for (int i = 0; i < MAX_POWERS; i++) {
int current = game->powers[i].welfare_points;
int delta = current - env->last_welfare[i];
Expand Down
4 changes: 2 additions & 2 deletions tests/diplomacy/c_tests/test_pufferlib4_adapter.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"""Exercise actual pinned 4.0 vecenv + bindings_cpu, not a mock interface.

Build both extensions first. These tests do not establish correct welfare
rewards: the first port deliberately preserves the old engine's reward bug.
Build both extensions first. These comparisons alone do not establish correct
welfare rewards; test_winter_rewards.py checks their timing and final delivery.
"""
import numpy as np
import pytest
Expand Down
80 changes: 80 additions & 0 deletions tests/diplomacy/c_tests/test_winter_rewards.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
"""Paper section 2.2: welfare earned after Winter is delivered that step.

These tests check reward timing, not map ownership or complete order encoding.
The only non-HOLD action is a Winter disband of unit zero (action 61).
"""
import numpy as np
import pytest

from native.pufferlib4 import DiplomacyVec
from pufferlib.ocean.diplomacy import Diplomacy, binding


def old_step(env, action):
env.actions[:] = action
obs, reward, done, _, _ = env.step(env.actions)
return obs.copy(), reward.copy(), done.copy()


@pytest.mark.parametrize("max_years", [1, 2])
def test_old_binding_delivers_winter_delta_even_on_final_step(max_years):
env = Diplomacy(max_years=max_years)
try:
env.reset()
for _ in range(2):
assert not old_step(env, 0)[1].any()
assert binding.query_game_state(env.env_handle)["phase"] == 4
_, reward, done = old_step(env, 61)
state = binding.query_game_state(env.env_handle)
assert [p["welfare_points"] for p in state["powers"]] == [1] * 7
np.testing.assert_array_equal(reward, np.ones(7))
assert bool(done.all()) == (max_years == 1)
if max_years == 2:
for _ in range(2):
assert not old_step(env, 0)[1].any()
_, reward, done = old_step(env, 0)
np.testing.assert_array_equal(reward, np.ones(7))
assert done.all()
assert [p["welfare_points"] for p in binding.query_game_state(env.env_handle)["powers"]] == [2] * 7
env.reset()
for _ in range(3):
assert not old_step(env, 0)[1].any()
finally:
env.close()


@pytest.mark.parametrize("max_years", [1, 2])
def test_native_adapter_preserves_winter_delta_through_autoreset(max_years):
with DiplomacyVec(max_years=max_years) as env:
initial = env.reset()
for _ in range(2):
assert not env.step(np.zeros(7))[1].any()
obs, reward, done = env.step(np.full(7, 61))
np.testing.assert_array_equal(reward, np.ones(7))
assert bool(done.all()) == (max_years == 1)
if max_years == 2:
for _ in range(2):
assert not env.step(np.zeros(7))[1].any()
obs, reward, done = env.step(np.zeros(7))
np.testing.assert_array_equal(reward, np.ones(7))
assert done.all()
np.testing.assert_array_equal(obs, initial)
# The next game starts with fresh welfare bookkeeping and units.
for _ in range(3):
assert not env.step(np.zeros(7))[1].any()


def test_standard_mode_has_no_welfare_reward():
old = Diplomacy(welfare_mode=False, max_years=1)
try:
old.reset()
with DiplomacyVec(welfare_mode=False, max_years=1) as new:
for action in [0, 0, 61]:
old_result = old_step(old, action)
new_result = new.step(np.full(7, action))
assert not old_result[1].any()
assert not new_result[1].any()
np.testing.assert_array_equal(old_result[2], new_result[2])
assert [p["welfare_points"] for p in binding.query_game_state(old.env_handle)["powers"]] == [0] * 7
finally:
old.close()
Loading