# Load State Dict Implementation Guide This document summarizes the implementation of `load_state_dict` functionality for Bridges puzzle, which allows loading a Python state dict into a C game state. This can be used as a guide for implementing the same functionality for other puzzles (e.g., undead). ## Overview The `load_state_dict` feature enables: - Loading arbitrary puzzle states from Python dictionaries - Verifying if an ASCII board state (from LLM or other sources) is solved - Swapping states between puzzle instances - Round-trip testing: state → dict → load → dict → compare ## Architecture The implementation follows a three-layer architecture: 1. **C Layer**: Defines repr structs and reconstruction function 2. **Python ctypes Layer**: Defines Python structs matching C structs 3. **Python API Layer**: Provides user-facing functions with validation ## Implementation Steps ### 1. C Layer: Define Repr Structs **Location**: In the puzzle's `.c` file (e.g., `puzzles/bridges.c`) **Add before the `game_state` struct**: ```c /* Repr structs for Python state dict loading */ typedef struct puzzle_island_repr { /* Adjust name for puzzle */ int x; int y; int count; /* or other relevant fields */ } puzzle_island_repr; typedef struct puzzle_state_repr { int w; int h; /* Add puzzle-specific canonical fields */ int maxb; /* Example: bridges-specific */ bool allowloops; /* Example: bridges-specific */ int n_islands; /* Adjust for puzzle structure */ const puzzle_island_repr *islands; /* Adjust type */ const grid_type *grid; /* Adjust type */ const unsigned char *lines; /* Adjust type */ /* Only include fields needed for reconstruction */ } puzzle_state_repr; ``` **Key Points**: - Only include **canonical fields** needed for reconstruction - Do NOT include derived fields (computed by backend) - Do NOT include generation parameters (islands, expansion, difficulty) - Do NOT include UI/solver flags ### 2. C Layer: Implement Reconstruction Function **Location**: After existing parse functions (e.g., after `puzzle_text_parse`) **Function signature**: ```c game_state *puzzle_state_from_repr(const puzzle_state_repr *r) ``` **Implementation pattern**: ```c game_state *puzzle_state_from_repr(const puzzle_state_repr *r) { game_params params; game_state *st; int wh, i; if (!r) { return NULL; } /* Fill params from repr - only canonical fields */ params.w = r->w; params.h = r->h; /* Add puzzle-specific params */ params.maxb = r->maxb; params.allowloops = r->allowloops; /* Generation parameters - set to 0 (not needed for verification) */ params.islands = 0; params.expansion = 0; params.difficulty = 0; /* Create new state */ st = new_state(¶ms); if (!st) { return NULL; } wh = r->w * r->h; /* Copy canonical arrays */ if (r->grid) { for (i = 0; i < wh; i++) { st->grid[i] = r->grid[i]; } } if (r->lines) { for (i = 0; i < wh; i++) { st->lines[i] = r->lines[i]; } } /* Rebuild puzzle-specific structures */ /* Example for bridges: rebuild islands */ sfree(st->islands); st->islands = NULL; st->n_islands = 0; st->n_islands_alloc = 0; /* Clear flags first */ for (i = 0; i < wh; i++) { st->grid[i] &= ~G_ISLAND; /* Adjust for puzzle */ } /* Add structures from repr */ if (r->islands && r->n_islands > 0) { for (i = 0; i < r->n_islands; i++) { const puzzle_island_repr *ir = &r->islands[i]; puzzle_add_island(st, ir->x, ir->y, ir->count); /* Adjust function name */ } } /* Fix up internal structures */ puzzle_fixup_internal(st); /* Adjust function name */ puzzle_find_connections(st); /* Adjust function name */ /* Recompute derived fields */ puzzle_update_derived(st); /* Adjust function name */ /* Verify state */ st->completed = puzzle_check(st); /* Adjust function name */ st->solved = false; /* IMPORTANT: Clear solver flags that verification might set */ for (i = 0; i < wh; i++) { st->grid[i] &= ~(G_SWEEP | G_WARN); /* Adjust flags for puzzle */ } return st; } ``` **Critical Steps**: 1. Create params from repr (only canonical fields) 2. Create new state 3. Copy canonical arrays 4. Rebuild puzzle-specific structures (islands, etc.) 5. Fix up internal structures (pointers, indices) 6. Recompute derived fields (possibles, max values, etc.) 7. Run verification (`puzzle_check`) to set `completed` flag 8. **Clear solver flags** (G_SWEEP, G_WARN, etc.) - these are not part of game state ### 3. Python ctypes Layer: Define Structs **Location**: `rlp/specific_api.py` in `set_api_structures_puzzle()` function **Add at module level** (before the function): ```python class PuzzleIslandRepr(c.Structure): pass class PuzzleStateRepr(c.Structure): pass ``` **Inside `set_api_structures_puzzle()` function**: ```python PuzzleIslandRepr._fields_ = [ ("x", c.c_int), ("y", c.c_int), ("count", c.c_int), # Adjust fields for puzzle ] PuzzleStateRepr._fields_ = [ ("w", c.c_int), ("h", c.c_int), ("maxb", c.c_int), # Adjust for puzzle ("allowloops", c.c_bool), # Adjust for puzzle ("n_islands", c.c_int), ("islands", CT_PTR(PuzzleIslandRepr)), ("grid", CT_UINT_PTR), ("lines", CT_PTR(c.c_ubyte)), ] ``` **Key Points**: - Struct fields must match C struct exactly (order, types) - Use `CT_PTR` for pointers - Use appropriate ctypes types (`c.c_int`, `c.c_bool`, `c.c_ubyte`, etc.) ### 4. Python API Layer: Wire C Function **Location**: `rlp/puzzle.py` in `Puzzle.__init__()` method **Add near other puzzle-specific function wrappers**: ```python if self.puzzle_name == "puzzle_name": # ... existing code ... # Add state_from_repr function from rlp import specific_api as specific self._state_from_repr = wrap_function( self._lib, "puzzle_state_from_repr", api.specific.GAMESTATE_PTR, [c.POINTER(specific.PuzzleStateRepr)] ) ``` ### 5. Python API Layer: Implement Load Function **Location**: `rlp/specific_api.py` after `get_puzzle_state_puzzle()` function **Function**: ```python def load_state_dict_puzzle(state_dict: dict, lib: c.PyDLL) -> c.POINTER(GameState): """ Load a puzzle game state from a Python state dict. Args: state_dict: Dictionary containing state information (from get_puzzle_state_puzzle) lib: PyDLL instance for the puzzle library Returns: GameState pointer (caller must free it using game.free_game()) Raises: ValueError: If required fields are missing or have wrong types """ # Validate required fields exist required_fields = ["w", "h", "params", "grid", "lines", "islands"] # Adjust for puzzle for field in required_fields: if field not in state_dict: raise ValueError(f"Missing required field: {field}") # Validate types if not isinstance(state_dict["w"], int) or not isinstance(state_dict["h"], int): raise ValueError("w and h must be integers") if not isinstance(state_dict["grid"], list) or not isinstance(state_dict["lines"], list): raise ValueError("grid and lines must be lists") if not isinstance(state_dict["islands"], list): raise ValueError("islands must be a list") if not isinstance(state_dict["params"], dict): raise ValueError("params must be a dict") # Extract values w = state_dict["w"] h = state_dict["h"] wh = w * h # Validate params (adjust for puzzle) if "maxb" not in state_dict["params"]: raise ValueError("params.maxb is required") if "allowloops" not in state_dict["params"]: raise ValueError("params.allowloops is required") maxb = state_dict["params"]["maxb"] allowloops = bool(state_dict["params"]["allowloops"]) # Validate array lengths if len(state_dict["grid"]) != wh: raise ValueError(f"grid length ({len(state_dict['grid'])}) does not match w*h ({wh})") if len(state_dict["lines"]) != wh: raise ValueError(f"lines length ({len(state_dict['lines'])}) does not match w*h ({wh})") # Validate islands (adjust for puzzle structure) islands_list = state_dict["islands"] n_islands = len(islands_list) for i, island in enumerate(islands_list): if not isinstance(island, dict): raise ValueError(f"islands[{i}] must be a dict") if "x" not in island or "y" not in island or "count" not in island: raise ValueError(f"islands[{i}] must have x, y, and count fields") if not isinstance(island["x"], int) or not isinstance(island["y"], int) or not isinstance(island["count"], int): raise ValueError(f"islands[{i}] x, y, and count must be integers") # Create ctypes arrays grid_arr = (c.c_uint * wh)(*state_dict["grid"]) lines_arr = (c.c_ubyte * wh)(*state_dict["lines"]) # Create islands repr array (extract only needed fields, ignore adj/computed fields) islands_arr = (PuzzleIslandRepr * n_islands)( *[PuzzleIslandRepr( x=island["x"], y=island["y"], count=island["count"] ) for island in islands_list] ) # Build PuzzleStateRepr struct repr_obj = PuzzleStateRepr( w=w, h=h, maxb=maxb, allowloops=allowloops, n_islands=n_islands, islands=islands_arr, grid=grid_arr, lines=lines_arr, ) # Get the function from lib state_from_repr_func = lib.puzzle_state_from_repr state_from_repr_func.restype = GAMESTATE_PTR state_from_repr_func.argtypes = [c.POINTER(PuzzleStateRepr)] # Call C function state_ptr = state_from_repr_func(c.byref(repr_obj)) if not state_ptr: raise ValueError("Failed to create game state from repr") # Return the pointer (caller must free it) return state_ptr ``` ### 6. Python API Layer: Add Puzzle Method **Location**: `rlp/puzzle.py` in `Puzzle` class **Method**: ```python def load_state_dict(self, state_dict: dict) -> api.specific.GAMESTATE_PTR: """ Load a puzzle game state from a Python state dict. Args: state_dict: Dictionary containing state information (from get_puzzle_state) Returns: GameState pointer (caller must free it using game.free_game()) Raises: ValueError: If puzzle doesn't support this or if state dict is invalid """ if self.puzzle_name != "puzzle_name": raise ValueError(f"load_state_dict is only supported for puzzle_name puzzle, not {self.puzzle_name}") if self._state_from_repr is None: raise ValueError("puzzle_state_from_repr function not available") from rlp import specific_api as specific # Call the load function state_ptr = specific.load_state_dict_puzzle(state_dict, self._lib) return state_ptr ``` ## Critical Gotchas and Important Notes ### 1. Memory Management - **Always return a pointer** from C function (not contents) - **Caller must free** the returned state using `game.free_game(state_ptr)` - Use try/finally blocks in Python to ensure cleanup - Pattern: `state_ptr = puzzle.load_state_dict(dict)` → use → `game.free_game(state_ptr)` ### 2. Solver Flags Must Be Cleared - After calling verification (`puzzle_check`), **clear solver flags** (G_SWEEP, G_WARN, etc.) - These flags are set during verification but are NOT part of the game state - If not cleared, round-trip tests will fail with grid value mismatches - Example: `st->grid[i] &= ~(G_SWEEP | G_WARN);` ### 3. Only Include Canonical Fields - **DO include**: w, h, grid, lines, islands (x, y, count), puzzle-specific rules (maxb, allowloops) - **DON'T include**: - Derived fields (wha, possv, possh, maxv, maxh) - recomputed by backend - Generation parameters (islands, expansion, difficulty) - not needed for verification - UI flags (G_MARK, etc.) - Solver state (dsf, tmpdsf, etc.) - Computed adjacency data (adj) - recomputed by `puzzle_find_connections` ### 4. Test Comparison Logic - Use `ignore_fields` in test comparisons to ignore: - Generation parameters (`params.islands`, `params.expansion`, `params.difficulty`) - Allocation details (`n_islands_alloc`) - Solver flags in grid values (mask out G_SWEEP, G_WARN when comparing) - Compare grid values with flags masked: `val1 & ~(0x1000 | 0x0080)` ### 5. Pointer Dereferencing - When accessing state fields: use `.contents` (e.g., `state_ptr.contents.completed`) - When passing to functions expecting GameState: use `.contents` - When freeing: pass the pointer directly (e.g., `game.free_game(state_ptr)`) ### 6. Validation is Critical - Validate all required fields exist - Validate types (int, list, dict) - Validate array lengths match w*h - Validate island structures have required fields - Raise clear ValueError messages for debugging ### 7. Function Naming Convention - C function: `puzzle_name_state_from_repr` (e.g., `bridges_state_from_repr`) - Python function: `load_state_dict_puzzle_name` (e.g., `load_state_dict_bridges`) - Struct names: `PuzzleNameIslandRepr`, `PuzzleNameStateRepr` ### 8. Recompilation Required - After modifying C code, must recompile: `cmake --build rlp/lib --target libpuzzle_name` - Python changes don't require recompilation (unless ctypes structs change) ## Testing Strategy ### Essential Tests 1. **test_load_problem_state**: Load initial problem state, verify it matches 2. **test_load_solution_state**: Load solved state, verify `completed=True` 3. **test_round_trip_problem**: Problem state → dict → load → dict → compare 4. **test_round_trip_solution**: Solution state → dict → load → dict → compare 5. **test_multiple_sizes**: Test with different puzzle sizes 6. **test_state_verification**: Verify `map_check()` results match 7. **test_swap_states_between_instances**: Swap states between two puzzle instances 8. **test_validation_errors**: Test invalid state dicts raise appropriate exceptions ### Test Pattern ```python def test_example(): puzzle = rp.Puzzle('puzzle_name', arg='5x5de', headless=True) puzzle.new_game() # Get state dict original_dict = puzzle.get_puzzle_state() # Load state dict me = puzzle.fe.contents.me.contents game = me.ourgame.contents free_game_func = game.free_game loaded_state_ptr = puzzle.load_state_dict(original_dict) try: # Use loaded state helper = getattr(puzzle, "_get_puzzle_state_helper", None) loaded_dict = specific.get_puzzle_state_dict( puzzle.puzzle_name, loaded_state_ptr.contents, helper ) # Compare (with ignore_fields) ignore_fields = { 'params.difficulty', 'params.islands', # If applicable 'params.expansion', # If applicable 'n_islands_alloc', # If applicable } differences = deep_compare_dicts(original_dict, loaded_dict, ignore_fields=ignore_fields) assert len(differences) == 0, f"State dicts differ: {differences}" finally: # Always free if loaded_state_ptr: free_game_func(loaded_state_ptr) ``` ## Files Modified (Bridges Example) 1. **puzzles/bridges.c**: - Added `bridges_island_repr` and `bridges_state_repr` structs - Implemented `bridges_state_from_repr()` function 2. **rlp/specific_api.py**: - Added `BridgesIslandRepr` and `BridgesStateRepr` ctypes structures - Implemented `load_state_dict_bridges()` function 3. **rlp/puzzle.py**: - Wired `bridges_state_from_repr` in `__init__()` - Added `load_state_dict()` method 4. **test_load_state_dict.py**: - Created comprehensive test suite ## Checklist for New Puzzle Implementation - [ ] Define C repr structs (only canonical fields) - [ ] Implement C reconstruction function - [ ] Create params from repr - [ ] Create new state - [ ] Copy canonical arrays - [ ] Rebuild puzzle-specific structures - [ ] Fix up internal structures - [ ] Recompute derived fields - [ ] Run verification - [ ] Clear solver flags - [ ] Define Python ctypes structs (match C exactly) - [ ] Wire C function in puzzle.py - [ ] Implement Python load function with validation - [ ] Add Puzzle.load_state_dict() method - [ ] Create comprehensive tests - [ ] Recompile library - [ ] Run tests and verify all pass ## Common Pitfalls 1. **Forgetting to clear solver flags** → Grid value mismatches in tests 2. **Including non-canonical fields** → Unnecessary complexity, potential bugs 3. **Not validating input** → Runtime crashes with unclear errors 4. **Memory leaks** → Not freeing loaded states in tests 5. **Pointer vs contents confusion** → AttributeError or incorrect behavior 6. **Struct field order mismatch** → Silent corruption or crashes 7. **Not recompiling after C changes** → Old code runs, tests fail mysteriously ## Success Criteria - All tests pass - Round-trip tests show no differences (except ignored fields) - State swapping works correctly - Memory is properly managed (no leaks) - Validation catches invalid inputs - ASCII strings match after loading