# Galaxies Load State Dict Implementation Plan This document outlines the plan for implementing `load_state_dict` functionality for the Galaxies puzzle, following the template from `LOAD_STATE_DICT_IMPLEMENTATION_GUIDE.md`. ## Clarifying Questions Before proceeding with implementation, we need to clarify the following: ### 1. Canonical Fields Identification **Question**: Which fields from `get_puzzle_state_galaxies()` are truly canonical vs derived? Current `get_puzzle_state_galaxies()` returns: - `w`, `h` - **Canonical** (from params) - `sx`, `sy` - **Derived** (computed as `(w*2)+1`, `(h*2)+1`) - `grid` - **Canonical** (array of space structs with flags, dotx, doty, nassoc) - `completed` - **Derived** (computed by `check_complete()`) - `used_solve` - **UI state** (not canonical) - `ndots` - **Derived** (computed by `game_update_dots()`) - `dots_indices` - **Derived** (computed from grid by scanning for F_DOT flags) - `cdiff` - **Derived** (difficulty of current puzzle, not needed for reconstruction) **Proposed canonical fields for repr struct**: - `w`, `h` - dimensions - `grid` - array of space structs (only canonical fields: x, y, type, flags, dotx, doty, nassoc) **Questions**: - Should we include `sx`, `sy` in the repr, or always compute them from `w`, `h`? compute from w,h - Should we store `dots_indices` or reconstruct the `dots` array from grid by calling `game_update_dots()`? - Which flags in the `space.flags` field are canonical vs solver/UI flags? (F_DOT, F_EDGE_SET, F_TILE_ASSOC are canonical; F_MARK, F_REACHABLE, F_SCRATCH, F_MULTIPLE, F_DOT_HOLD, F_GOOD, F_DOT_BLACK are likely not) ### 2. Space Struct Fields **Question**: Which fields in the `space` struct are canonical? The `space` struct has: - `x`, `y` - position (can be recomputed from index) - `type` - s_tile, s_edge, s_vertex (can be recomputed from x, y position) - `flags` - bitfield (needs careful filtering) - `dotx`, `doty` - if F_TILE_ASSOC is set (canonical) - `nassoc` - if F_DOT is set (canonical) **Proposed approach**: - Store only: `flags` (filtered), `dotx`, `doty`, `nassoc` - Recompute `x`, `y`, `type` during reconstruction ### 3. Solver Flags to Clear **Question**: Which flags need to be cleared after verification? From the code, solver flags that should be cleared: - `F_MARK` (0x10) - scratch flag - `F_REACHABLE` (0x20) - `F_SCRATCH` (0x40) - `F_MULTIPLE` (0x80) - `F_DOT_HOLD` (0x100) - `F_GOOD` (0x200) **Canonical flags to preserve**: - `F_DOT` (0x1) - `F_EDGE_SET` (0x2) - `F_TILE_ASSOC` (0x4) - `F_DOT_BLACK` (0x8) - UI flag, but might be needed? ### 4. Reconstruction Steps **Question**: What internal structures need to be rebuilt? From analyzing the code: 1. `dots` array - needs to be rebuilt from grid using `game_update_dots()` 2. `ndots` - computed by `game_update_dots()` 3. `completed` - computed by `check_complete()` 4. `sx`, `sy` - computed from `w`, `h` in `blank_game()` **Proposed reconstruction order**: 1. Create params from repr (w, h, diff=0) 2. Create blank state using `blank_game()` or similar 3. Copy grid array (filtering flags) 4. Call `game_update_dots()` to rebuild dots array 5. Call `check_complete()` to set `completed` flag 6. Clear solver flags from grid ### 5. Validation Requirements **Question**: What validation is needed in the Python layer? - Validate `w`, `h` are integers and within reasonable bounds - Validate `grid` is a list with length `sx * sy` where `sx = (w*2)+1`, `sy = (h*2)+1` - Validate each space in grid has required fields (flags, dotx, doty, nassoc) - Validate `dotx`, `doty` are within bounds if `F_TILE_ASSOC` is set - Validate `nassoc` is non-negative if `F_DOT` is set ## Implementation Plan ### Phase 1: C Layer - Repr Structs **File**: `puzzles/galaxies.c` **Location**: Before `game_state` struct (around line 178) ```c /* Repr structs for Python state dict loading */ typedef struct galaxies_space_repr { unsigned int flags; /* Only canonical flags: F_DOT, F_EDGE_SET, F_TILE_ASSOC */ int dotx, doty; /* If flags & F_TILE_ASSOC */ int nassoc; /* If flags & F_DOT */ } galaxies_space_repr; typedef struct galaxies_state_repr { int w; int h; const galaxies_space_repr *grid; /* Length: (w*2+1) * (h*2+1) */ } galaxies_state_repr; ``` **Key Decisions**: - Only store canonical flags (F_DOT, F_EDGE_SET, F_TILE_ASSOC) - Store dotx, doty, nassoc as they are part of the game state - Do NOT store x, y, type (recomputed from position) - Do NOT store sx, sy (recomputed from w, h) ### Phase 2: C Layer - Reconstruction Function **File**: `puzzles/galaxies.c` **Location**: After `load_game()` function (around line 1757) **Function**: `galaxies_state_from_repr()` **Implementation steps**: 1. Validate input (check r != NULL) 2. Create params: `w = r->w`, `h = r->h`, `diff = 0` 3. Create blank state using `blank_game(w, h)` 4. Compute `sx = (w*2)+1`, `sy = (h*2)+1`, `sz = sx * sy` 5. Copy grid array: - For each space in repr->grid: - Set `flags` (only canonical flags) - Set `dotx`, `doty` if `F_TILE_ASSOC` is set - Set `nassoc` if `F_DOT` is set - Preserve `x`, `y`, `type` from blank_game 6. Call `game_update_dots(state)` to rebuild dots array 7. Call `check_complete(state, NULL, NULL)` to set `completed` flag 8. Clear solver flags from grid: ```c for (i = 0; i < sz; i++) { state->grid[i].flags &= ~(F_MARK | F_REACHABLE | F_SCRATCH | F_MULTIPLE | F_DOT_HOLD | F_GOOD); } ``` 9. Set `used_solve = false`, `cdiff = -1` 10. Return state pointer ### Phase 3: Python ctypes Layer - Struct Definitions **File**: `rlp/specific_api.py` **Location**: In `set_api_structures_puzzle()` function, add before the function: ```python class GalaxiesSpaceRepr(c.Structure): pass class GalaxiesStateRepr(c.Structure): pass ``` **Inside `set_api_structures_puzzle()` function**: ```python if puzzle_name == "galaxies": GalaxiesSpaceRepr._fields_ = [ ("flags", c.c_uint), ("dotx", c.c_int), ("doty", c.c_int), ("nassoc", c.c_int), ] GalaxiesStateRepr._fields_ = [ ("w", c.c_int), ("h", c.c_int), ("grid", CT_PTR(GalaxiesSpaceRepr)), ] ``` ### Phase 4: Python API Layer - Wire C Function **File**: `rlp/puzzle.py` **Location**: In `Puzzle.__init__()` method, add after undead section (around line 188): ```python elif self.puzzle_name == "galaxies": # Add state_from_repr function from rlp import specific_api as specific self._state_from_repr = wrap_function( self._lib, "galaxies_state_from_repr", api.specific.GAMESTATE_PTR, [c.POINTER(specific.GalaxiesStateRepr)] ) else: self._text_parse = None self._state_from_repr = None ``` ### Phase 5: Python API Layer - Load Function **File**: `rlp/specific_api.py` **Location**: After `load_state_dict_undead()` function (around line 3460) **Function**: `load_state_dict_galaxies()` **Implementation**: 1. Validate required fields: `w`, `h`, `grid` 2. Validate types: `w`, `h` are ints, `grid` is a list 3. Compute `sx = (w*2)+1`, `sy = (h*2)+1`, `sz = sx * sy` 4. Validate `grid` length matches `sz` 5. Validate each space in grid: - Has `flags`, `dotx`, `doty`, `nassoc` fields - `flags` is an integer - `dotx`, `doty`, `nassoc` are integers - If `F_TILE_ASSOC` is set, validate `dotx`, `doty` are within bounds - If `F_DOT` is set, validate `nassoc >= 0` 6. Create ctypes arrays: - `grid_arr = (GalaxiesSpaceRepr * sz)(...)` 7. Build `GalaxiesStateRepr` struct 8. Call C function `galaxies_state_from_repr()` 9. Return state pointer ### Phase 6: Python API Layer - Add Puzzle Method **File**: `rlp/puzzle.py` **Location**: In `Puzzle` class, add `load_state_dict()` method (around line 530) **Update existing method** to handle galaxies: ```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 not in ("bridges", "undead", "galaxies"): raise ValueError(f"load_state_dict is only supported for bridges, undead, and galaxies puzzles, 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 if self.puzzle_name == "galaxies": state_ptr = specific.load_state_dict_galaxies(state_dict, self._lib) elif self.puzzle_name == "undead": state_ptr = specific.load_state_dict_undead(state_dict, self._lib) elif self.puzzle_name == "bridges": state_ptr = specific.load_state_dict_bridges(state_dict, self._lib) return state_ptr ``` ### Phase 7: Testing **File**: `test_load_state_dict_galaxies.py` (new file) **Test cases**: 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 (5x5, 7x7, 10x10) 6. `test_state_verification` - Verify `check_complete()` 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 9. `test_dots_reconstruction` - Verify dots array is correctly rebuilt 10. `test_solver_flags_cleared` - Verify solver flags are cleared after loading **Ignore fields for comparison**: - `sx`, `sy` (computed from w, h) - `ndots` (computed from grid) - `dots_indices` (computed from grid) - `completed` (computed by check_complete, but should match) - `used_solve` (UI state) - `cdiff` (derived) ## Open Questions Requiring Answers 1. **Should `F_DOT_BLACK` be considered canonical?** It's a UI flag but might affect puzzle state representation. 2. **Should we validate that `dots_indices` in the state dict matches the actual dots found in grid?** This could catch inconsistencies. 3. **How should we handle the `dots` array pointer comparison?** The pointers will be different after reconstruction, but the contents should match. 4. **Should `sx`, `sy` be included in the repr for validation purposes, even though they're derived?** 5. **What are the bounds for `dotx`, `doty`?** Should they be validated against `sx`, `sy` or `w`, `h`? ## Implementation Checklist - [ ] Define C repr structs (`galaxies_space_repr`, `galaxies_state_repr`) - [ ] Implement C reconstruction function (`galaxies_state_from_repr`) - [ ] Create params from repr - [ ] Create blank state - [ ] Copy grid array (filtering flags) - [ ] Rebuild dots array (`game_update_dots`) - [ ] Run verification (`check_complete`) - [ ] Clear solver flags - [ ] Define Python ctypes structs (match C exactly) - [ ] Wire C function in `puzzle.py` - [ ] Implement Python load function with validation - [ ] Update `Puzzle.load_state_dict()` method - [ ] Create comprehensive tests - [ ] Recompile library (`cmake --build rlp/lib --target libgalaxies`) - [ ] Run tests and verify all pass ## Files to Modify 1. **puzzles/galaxies.c**: - Add repr structs - Implement `galaxies_state_from_repr()` function 2. **rlp/specific_api.py**: - Add `GalaxiesSpaceRepr` and `GalaxiesStateRepr` ctypes structures - Implement `load_state_dict_galaxies()` function 3. **rlp/puzzle.py**: - Wire `galaxies_state_from_repr` in `__init__()` - Update `load_state_dict()` method to handle galaxies 4. **test_load_state_dict_galaxies.py** (new): - Create comprehensive test suite ## Next Steps 1. **Answer clarifying questions** above 2. **Review and approve** this plan 3. **Implement Phase 1-2** (C layer) 4. **Test C layer** with simple test cases 5. **Implement Phase 3-6** (Python layer) 6. **Create and run tests** (Phase 7) 7. **Fix any issues** found during testing 8. **Document** any deviations from the plan