Fixed a bug in 15puzzle and updated logic for Wordle and 15puzzle + Sample Multilingual code - #169
Fixed a bug in 15puzzle and updated logic for Wordle and 15puzzle + Sample Multilingual code#169MadBonzz wants to merge 6 commits into
Conversation
MadBonzz
commented
Aug 26, 2025
- There was a bug in 15puzzle: puzzle was being generated entirely randomly, but half of the random generations are unsolvable. Fixed this by taking the final board and making random swaps of the Blank tile with any of its neighboring tiles, ensuring that all generations are solvable.
- All puzzles in 15puzzle are not equally difficult. Its unfair to compare a puzzle close to final state than a very shuffled puzzle. Made difficulty levels to address this by adding 3 difficulty levels based on the number of swaps required to solve the puzzle.
- Updated Wordle to take a list of numbers as input rather than a single integer. This helps in case a user wants to eval over multiple lengths at once.
- Updated max_turns logic for 15puzzle as twice of the number of swaps required to solve it.
- Updated num guesses logic for Wordle as length of word + 1
- Made system prompts for 15puzzle multilingual, as a sample. Need to discuss how to make other games multilingual
|
Hey @MadBonzz , thanks for fixing that issue in 15 puzzle. Random swapping N times, based on difficulty, from the ordered outcome makes total sense. Multilingual is a great suggestion too. Just not sure if adding a player prompt file for every environment is the most efficient way - we have 100 environments and counting hahah. How about writing a translation module that uses a model from huggingface? Nice touch on Wordle. |
|
Hey @bobbycxy I understand adding separate files for each game is quite redundant, but I don't think using a module is a good idea, the translation quality is not good enough, and it might be slower to do on the fly. The translation quality really matters because bad translation can put the model at a disadvantage in a language, making the evaluation unfair. Alternatively, we can probably use some model on OpenRouter itself to translate, have a translation prompt set up, and then use it to get the translation. Will have to implement caching to save credits, but having files with translations seemed the best idea in terms of ensuring translation quality and speed. I also wanted to discuss on this:
|
|
great idea adding additional languages! Looks good! More broadly speaking, what would you think about an Observation Wrapper that takes care of the translation (i.e. adding one here (https://github.com/LeonGuertler/TextArena/blob/main/textarena/wrappers/ObservationWrappers/llm_observation_wrapper.py) that will take care of the translation into arbitrary languages; might have the added advantage that no additional code changes for new environments are necessary. Maybe something along these lines: from google.cloud import translate_v2 as translate
class GoogleLLMObservationWrapper(ObservationWrapper):
def __init__(self, env: Env, target_lang: str = "en"):
super().__init__(env)
self.full_observations: Dict[int, List[Tuple[int, str]]] = {}
self.client = translate.Client()
self.target_lang = target_lang
def _convert_obs_to_str(self, player_id: int) -> Observations:
str_observation = ""
if player_id in self.full_observations:
for sender_id, message, _ in self.full_observations[player_id]:
if sender_id == ta.GAME_ID: sender_name = "GAME"
else: sender_name = self.env.state.role_mapping.get(sender_id, f"Player {sender_id}")
str_observation += f"\n[{sender_name}] {message}"
return str_observation
def observation(self, player_id: int, observation: Optional[ta.Observations]):
if observation is None: return self._convert_obs_to_str(player_id=player_id)
if player_id not in self.full_observations: self.full_observations[player_id] = []
# translate each incoming observation's message (index 1) and preserve tuple shape
translated = []
for obs in observation:
sender = obs[0]
msg = "" if obs[1] is None else str(obs[1])
try:
res = self.client.translate(msg, target_language=self.target_lang)
msg_tr = res.get("translatedText", msg)
except Exception: msg_tr = msg # on failure, keep original
rest = tuple(obs[2:]) if len(obs) > 2 else ()
translated.append((sender, msg_tr) + rest)
self.full_observations[player_id].extend(translated) # Append translated observations in sequence
return self._convert_obs_to_str(player_id=player_id)Might have the additional advantage of allowing models to play games like Negotiation or SecretMafia in different languages. What do you think? Very happy to merge this already, the wrapper is just a suggested in the long-run. |
|
To summarize the meet,
I'll try to make the updated PR in 2-3 days. |
|
Added translation feature but not tested yet. |
|
Updated and fixed some issues with translation wrapper. Tested the OpenRouter version, it is working correctly. |
|
The solvability idea here is genuinely valuable — as-is on For this PR to move as-is, a few things still block it — flagging so you can decide how to proceed:
Happy to help rebase the Wordle piece separately if useful. Not trying to step on the work — just unblocking the solvability win so it isn't held up by the rest. |
|
Thanks for this, @MadBonzz — three separate improvements are bundled here, so I reviewed each against 1. FifteenPuzzle — solvability + difficulty The solvability intent is exactly right: The difficulty redesign can't merge as-is:
Difficulty tiers are a nice idea — best done later as an additive (non-breaking) option on top of the #196 generator. 2. Wordle — variable word length
The one Wordle bug worth fixing — the player's guess logged as coming from GAME — is addressed in #186 ( 3. Observation translation wrapper
Recommendation: close in favor of #196 (solvability) and #186 (Wordle logging), which deliver both fixes without the breaking API changes. Happy to help shape the difficulty-tier option or the translation wrapper as their own PRs if you'd like them. Thanks again for surfacing both the solvability and logging bugs — they're getting fixed because of this. |
|
Hey I won't be able to do the translation wrapper PRs at the moment so I will be closing the PR. |
Add an optional `difficulty` argument (easy/medium/hard) to FifteenPuzzleEnv. When set, the board is scrambled a bounded number of legal slides from the solved layout, so its distance from solved scales with the tier and the result is solvable by construction. The default (difficulty=None) is unchanged: a full random shuffle repaired to a solvable layout, keeping FifteenPuzzle-v0 exactly as before. Register FifteenPuzzle-v0-easy/-medium/-hard alongside the base id. The difficulty-tier idea comes from @MadBonzz (TextArena#169); this reimplements it additively so it composes with the solvable-board generator without changing the existing constructor or registration.