From ded3a4996ccda059c506a2ef35a6831e02456165 Mon Sep 17 00:00:00 2001 From: akashjainn Date: Tue, 8 Sep 2026 20:36:30 -0400 Subject: [PATCH] fix(textarena_env): forward reset(seed=...) so episodes are reproducible TextArenaEnvironment.reset() accepts a seed, as every Environment does, and called self._ta_env.reset(num_players=...) without it. The seed had no effect and nothing reported that, so a caller reading the signature would reasonably believe an episode was reproducible when it was not. Six resets at seed=1234 on Wordle-v0 gave six different secret words (clock, sugar, month, price, flame, steam). With the seed forwarded they give "slope" every time, and a fresh environment at the same seed gives it too. TextArena's own reset takes seed on Env and on Wrapper, ObservationWrapper, ActionWrapper and RenderWrapper alike, so forwarding is safe through the wrapper chain OpenEnv builds. Checked determinism on Wordle, GuessTheNumber, Hangman, Crosswords, Sudoku and FifteenPuzzle: unseeded resets vary, a fixed seed is stable, and different seeds differ. __init__ still resets unseeded. It only exists to leave the env in a valid state before the first step(), and reset() replaces that state, so threading a seed through the constructor would widen the change without changing behaviour. Fixes #1102 --- envs/textarena_env/server/environment.py | 2 +- tests/envs/test_textarena_environment.py | 51 ++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 1 deletion(-) diff --git a/envs/textarena_env/server/environment.py b/envs/textarena_env/server/environment.py index 83b7d6c91..b1dd0ebf8 100644 --- a/envs/textarena_env/server/environment.py +++ b/envs/textarena_env/server/environment.py @@ -147,7 +147,7 @@ def reset( if hasattr(env, "full_observations"): env.full_observations = {} - self._ta_env.reset(num_players=self.num_players) + self._ta_env.reset(num_players=self.num_players, seed=seed) for provider in self._reward_providers: provider.reset() diff --git a/tests/envs/test_textarena_environment.py b/tests/envs/test_textarena_environment.py index 673a3d667..d4cb26292 100644 --- a/tests/envs/test_textarena_environment.py +++ b/tests/envs/test_textarena_environment.py @@ -65,3 +65,54 @@ def test_wordle_reset_clears_accumulated_state(): # Verify the prompts are actually the same content assert obs1.prompt == obs2.prompt assert obs2.prompt == obs3.prompt + + +def _secret_word(env: TextArenaEnvironment) -> str: + """Read the puzzle from TextArena's own game state, not from the prompt. + + Wordle's prompt is static instructions, so hashing the observation would + report every episode as identical whether or not the seed took effect. + """ + return env._ta_env.state.game_state["secret_word"] + + +def test_reset_seed_is_forwarded_to_textarena(): + """A fixed seed must give the same episode, and different seeds different ones.""" + pytest.importorskip("textarena", reason="textarena not installed") + env = TextArenaEnvironment(env_id="Wordle-v0", num_players=1) + + seeded = [] + for _ in range(3): + env.reset(seed=1234) + seeded.append(_secret_word(env)) + + env.reset(seed=999) + other_seed = _secret_word(env) + + assert len(set(seeded)) == 1, f"seed=1234 produced {sorted(set(seeded))}" + assert other_seed != seeded[0], "a different seed produced the same episode" + + +def test_reset_seed_is_reproducible_across_instances(): + """The same seed must survive constructing a fresh environment.""" + pytest.importorskip("textarena", reason="textarena not installed") + first = TextArenaEnvironment(env_id="Wordle-v0", num_players=1) + first.reset(seed=1234) + + second = TextArenaEnvironment(env_id="Wordle-v0", num_players=1) + second.reset(seed=1234) + + assert _secret_word(first) == _secret_word(second) + + +def test_reset_without_seed_still_varies(): + """Forwarding the seed must not accidentally pin unseeded episodes.""" + pytest.importorskip("textarena", reason="textarena not installed") + env = TextArenaEnvironment(env_id="Wordle-v0", num_players=1) + + words = set() + for _ in range(8): + env.reset() + words.add(_secret_word(env)) + + assert len(words) > 1, "unseeded resets should not be deterministic"