testspace / submodules /rlp /GALAXIES_LOAD_STATE_DICT_PLAN.md
nilshoehing's picture
Upload TopoBench Space app
67acd34 verified
|
Raw
History Blame Contribute Delete
12.5 kB

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)

/* 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:
    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:

class GalaxiesSpaceRepr(c.Structure):
    pass

class GalaxiesStateRepr(c.Structure):
    pass

Inside set_api_structures_puzzle() function:

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):

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:

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