Spaces:
Sleeping
Pattern Puzzle Load State Dict Implementation Plan
This document provides a detailed implementation plan for adding load_state_dict functionality to the Pattern puzzle, following the template from Bridges puzzle implementation.
Overview
The load_state_dict feature for Pattern puzzle will enable:
- 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
Pattern Puzzle Structure
Canonical Fields (Must Include in Repr)
w,h- grid dimensionsrowdata- flattened array of clue values (size:rowsize * (w + h))rowlen- array of clue lengths for each row/column (size:w + h)immutable- boolean array marking which cells are immutable clues (size:w * h)grid- player's current marks (size:w * h, values: GRID_UNKNOWN=2, GRID_FULL=1, GRID_EMPTY=0)
Non-Canonical / Derived Fields (Recompute)
rowsize-max(w, h)- recomputerefcount- internal, set to 1fontsize- recompute from rowdata (FS_LARGE if all clues < 10, else FS_SMALL)completed- recompute viacompute_rowdatacomparison (likeexecute_movedoes)cheated- set to false
Implementation Steps
1. C Layer: Define Repr Structs
Location: puzzles/pattern.c - Add before the game_state struct (around line 60)
/* Repr structs for Python state dict loading */
typedef struct pattern_state_repr {
int w;
int h;
int rowsize; /* max(w, h) - for array sizing */
int n_rowcol; /* w + h - number of rows + columns */
const int *rowdata; /* flattened, size rowsize * n_rowcol */
const int *rowlen; /* size n_rowcol */
const bool *immutable; /* size w * h */
const unsigned char *grid; /* size w * h */
} pattern_state_repr;
Key Points:
- No separate "island" struct needed (unlike Bridges)
rowdatais flattened:rowdata[rowsize * i + j]for row/columni, cluejrowlen[i]gives the number of clues for row/columnin_rowcol = w + h(firstwentries are columns, nexthentries are rows)
2. C Layer: Implement Reconstruction Function
Location: puzzles/pattern.c - Add after existing functions (after new_game, around line 1045)
Function signature:
game_state *pattern_state_from_repr(const pattern_state_repr *r)
Implementation:
game_state *pattern_state_from_repr(const pattern_state_repr *r)
{
game_params params;
game_state *st;
game_state_common *common;
int wh, wph, rowsize, i, j;
if (!r) {
return NULL;
}
/* Validate dimensions */
if (r->w <= 0 || r->h <= 0) {
return NULL;
}
if (r->rowsize != max(r->w, r->h)) {
return NULL; /* rowsize must match max(w, h) */
}
if (r->n_rowcol != r->w + r->h) {
return NULL; /* n_rowcol must match w + h */
}
/* Fill params from repr - only canonical fields */
params.w = r->w;
params.h = r->h;
wh = r->w * r->h;
wph = r->w + r->h;
rowsize = max(r->w, r->h);
/* Allocate game_state */
st = snew(game_state);
if (!st) {
return NULL;
}
/* Allocate and initialize game_state_common */
common = snew(game_state_common);
if (!common) {
sfree(st);
return NULL;
}
common->w = r->w;
common->h = r->h;
common->rowsize = rowsize;
common->refcount = 1; /* Set to 1 for new state */
/* Allocate arrays */
st->grid = snewn(wh, unsigned char);
common->rowdata = snewn(rowsize * wph, int);
common->rowlen = snewn(wph, int);
common->immutable = snewn(wh, bool);
if (!st->grid || !common->rowdata || !common->rowlen || !common->immutable) {
/* Cleanup on failure */
if (st->grid) sfree(st->grid);
if (common->rowdata) sfree(common->rowdata);
if (common->rowlen) sfree(common->rowlen);
if (common->immutable) sfree(common->immutable);
sfree(common);
sfree(st);
return NULL;
}
st->common = common;
/* Copy canonical arrays */
if (r->grid) {
for (i = 0; i < wh; i++) {
st->grid[i] = r->grid[i];
}
} else {
memset(st->grid, GRID_UNKNOWN, wh);
}
if (r->rowdata) {
for (i = 0; i < rowsize * wph; i++) {
common->rowdata[i] = r->rowdata[i];
}
} else {
memset(common->rowdata, 0, rowsize * wph * sizeof(int));
}
if (r->rowlen) {
for (i = 0; i < wph; i++) {
common->rowlen[i] = r->rowlen[i];
}
} else {
memset(common->rowlen, 0, wph * sizeof(int));
}
if (r->immutable) {
for (i = 0; i < wh; i++) {
common->immutable[i] = r->immutable[i];
}
} else {
memset(common->immutable, 0, wh * sizeof(bool));
}
/* Recompute fontsize from rowdata */
common->fontsize = FS_LARGE;
for (i = 0; i < r->w; i++) {
for (j = 0; j < common->rowlen[i]; j++) {
if (common->rowdata[rowsize * i + j] >= 10) {
common->fontsize = FS_SMALL;
goto fontsize_done;
}
}
}
fontsize_done:
/* Initialize state flags */
st->completed = false;
st->cheated = false;
/* Verify state: recompute completed flag using same logic as execute_move */
if (st->grid && common->rowdata && common->rowlen) {
int *rowdata_check = snewn(rowsize, int);
bool all_match = true;
/* Check columns (first w entries) */
for (i = 0; i < r->w && all_match; i++) {
int len = compute_rowdata(rowdata_check, st->grid + i,
r->h, r->w);
if (len != common->rowlen[i] ||
memcmp(common->rowdata + rowsize * i,
rowdata_check, len * sizeof(int)) != 0) {
all_match = false;
break;
}
}
/* Check rows (next h entries) */
if (all_match) {
for (i = 0; i < r->h && all_match; i++) {
int len = compute_rowdata(rowdata_check,
st->grid + i * r->w,
r->w, 1);
int row_idx = r->w + i;
if (len != common->rowlen[row_idx] ||
memcmp(common->rowdata + rowsize * row_idx,
rowdata_check, len * sizeof(int)) != 0) {
all_match = false;
break;
}
}
}
st->completed = all_match;
sfree(rowdata_check);
}
return st;
}
Critical Steps:
- Validate input dimensions and array sizes
- Allocate
game_stateandgame_state_commonseparately - Copy all canonical arrays:
grid,rowdata,rowlen,immutable - Recompute
fontsizefromrowdata - Recompute
completedusingcompute_rowdatacomparison (same asexecute_move) - Set
refcount = 1andcheated = false
3. Python ctypes Layer: Define Structs
Location: rlp/specific_api.py
Add at module level (around line 128, after other repr structs):
class PatternStateRepr(c.Structure):
pass
Inside set_api_structures_pattern() function (around line 1571, after GameState._fields_):
PatternStateRepr._fields_ = [
("w", c.c_int),
("h", c.c_int),
("rowsize", c.c_int),
("n_rowcol", c.c_int),
("rowdata", CT_INT_PTR),
("rowlen", CT_INT_PTR),
("immutable", CT_BOOL_PTR),
("grid", CT_UCHAR_PTR),
]
Key Points:
- Struct fields must match C struct exactly (order, types)
- Use
CT_INT_PTRforint*pointers - Use
CT_BOOL_PTRforbool*pointers - Use
CT_UCHAR_PTRforunsigned char*pointers
4. Python API Layer: Wire C Function
Location: rlp/puzzle.py in Puzzle.__init__() method (around line 163)
Add in the puzzle-specific section:
elif self.puzzle_name == "pattern":
# Add state_from_repr function
from rlp import specific_api as specific
self._state_from_repr = wrap_function(
self._lib, "pattern_state_from_repr",
api.specific.GAMESTATE_PTR,
[c.POINTER(specific.PatternStateRepr)]
)
self._text_parse = None
5. Python API Layer: Implement Load Function
Location: rlp/specific_api.py after get_puzzle_state_pattern() function (around line 3047)
Function:
def load_state_dict_pattern(state_dict: dict, lib: c.PyDLL) -> c.POINTER(GameState):
"""
Load a Pattern puzzle game state from a Python state dict.
Args:
state_dict: Dictionary containing state information (from get_puzzle_state_pattern)
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 = ["common", "grid", "completed", "cheated"]
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["common"], dict):
raise ValueError("common must be a dict")
if not isinstance(state_dict["grid"], list):
raise ValueError("grid must be a list")
# Extract common fields
common = state_dict["common"]
required_common_fields = ["w", "h", "rowsize", "rowdata", "rowlen", "immutable"]
for field in required_common_fields:
if field not in common:
raise ValueError(f"Missing required common field: {field}")
w = common["w"]
h = common["h"]
rowsize = common["rowsize"]
wh = w * h
wph = w + h
# Validate dimensions
if not isinstance(w, int) or not isinstance(h, int):
raise ValueError("w and h must be integers")
if w <= 0 or h <= 0:
raise ValueError("w and h must be positive")
if rowsize != max(w, h):
raise ValueError(f"rowsize ({rowsize}) must equal max(w, h) ({max(w, h)})")
# Validate array types and lengths
if not isinstance(common["rowdata"], list):
raise ValueError("common.rowdata must be a list")
if not isinstance(common["rowlen"], list):
raise ValueError("common.rowlen must be a list")
if not isinstance(common["immutable"], list):
raise ValueError("common.immutable must be a list")
if not isinstance(state_dict["grid"], list):
raise ValueError("grid must be a list")
# Validate array lengths
expected_rowdata_len = rowsize * wph
if len(common["rowdata"]) != expected_rowdata_len:
raise ValueError(
f"rowdata length ({len(common['rowdata'])}) does not match "
f"rowsize * (w + h) ({expected_rowdata_len})"
)
if len(common["rowlen"]) != wph:
raise ValueError(
f"rowlen length ({len(common['rowlen'])}) does not match w + h ({wph})"
)
if len(common["immutable"]) != wh:
raise ValueError(
f"immutable length ({len(common['immutable'])}) does not match w * h ({wh})"
)
if len(state_dict["grid"]) != wh:
raise ValueError(
f"grid length ({len(state_dict['grid'])}) does not match w * h ({wh})"
)
# Validate array element types
for i, val in enumerate(common["rowdata"]):
if not isinstance(val, int):
raise ValueError(f"rowdata[{i}] must be an integer")
for i, val in enumerate(common["rowlen"]):
if not isinstance(val, int):
raise ValueError(f"rowlen[{i}] must be an integer")
if val < 0 or val > rowsize:
raise ValueError(f"rowlen[{i}] ({val}) must be between 0 and rowsize ({rowsize})")
for i, val in enumerate(common["immutable"]):
if not isinstance(val, bool):
raise ValueError(f"immutable[{i}] must be a boolean")
for i, val in enumerate(state_dict["grid"]):
if not isinstance(val, int):
raise ValueError(f"grid[{i}] must be an integer")
if val not in (0, 1, 2): # GRID_EMPTY, GRID_FULL, GRID_UNKNOWN
raise ValueError(f"grid[{i}] ({val}) must be 0, 1, or 2")
# Create ctypes arrays
rowdata_arr = (c.c_int * (rowsize * wph))(*common["rowdata"])
rowlen_arr = (c.c_int * wph)(*common["rowlen"])
immutable_arr = (c.c_bool * wh)(*common["immutable"])
grid_arr = (c.c_ubyte * wh)(*state_dict["grid"])
# Build PatternStateRepr struct
repr_obj = PatternStateRepr(
w=w,
h=h,
rowsize=rowsize,
n_rowcol=wph,
rowdata=rowdata_arr,
rowlen=rowlen_arr,
immutable=immutable_arr,
grid=grid_arr,
)
# Get the function from lib
state_from_repr_func = lib.pattern_state_from_repr
state_from_repr_func.restype = GAMESTATE_PTR
state_from_repr_func.argtypes = [c.POINTER(PatternStateRepr)]
# 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 (check if method already exists, add if not)
Method (add if not already present):
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 != "pattern":
raise ValueError(f"load_state_dict is only supported for pattern puzzle, not {self.puzzle_name}")
if self._state_from_repr is None:
raise ValueError("pattern_state_from_repr function not available")
from rlp import specific_api as specific
# Call the load function
state_ptr = specific.load_state_dict_pattern(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) - Pattern uses refcounted
game_state_common-free_gamehandles refcounting - Use try/finally blocks in Python to ensure cleanup
- Pattern:
state_ptr = puzzle.load_state_dict(dict)→ use →game.free_game(state_ptr)
2. Rowdata Array Layout
rowdatais flattened:rowdata[rowsize * i + j]where:iis row/column index (0 to w+h-1)- First
wentries (i=0 to w-1) are columns - Next
hentries (i=w to w+h-1) are rows jis clue index within that row/column (0 to rowlen[i]-1)
- When copying, must preserve this flattened layout
3. Only Include Canonical Fields
- DO include: w, h, rowdata, rowlen, immutable, grid
- DON'T include:
rowsize- recompute asmax(w, h)refcount- set to 1fontsize- recompute from rowdatacompleted,cheated- recompute
4. Completion Verification
- Pattern's completion check uses
compute_rowdatato compute actual row/column patterns - Compare computed patterns against stored
rowdataandrowlen - Same logic as
execute_moveinpattern.c(lines 1477-1506) - Must check all columns (first w entries) and all rows (next h entries)
5. Fontsize Recomputation
- Check all column clues (first w entries in rowlen)
- If any clue value >= 10, set
fontsize = FS_SMALL - Otherwise
fontsize = FS_LARGE - See
new_gameinpattern.c(lines 1019-1023)
6. Validation is Critical
- Validate all required fields exist
- Validate types (int, list, dict, bool)
- Validate array lengths match expected sizes
- Validate grid values are 0, 1, or 2
- Validate rowlen values are between 0 and rowsize
- Raise clear ValueError messages for debugging
7. Function Naming Convention
- C function:
pattern_state_from_repr - Python function:
load_state_dict_pattern - Struct name:
PatternStateRepr
8. Recompilation Required
- After modifying C code, must recompile:
cmake --build rlp/lib --target libpattern - Python changes don't require recompilation (unless ctypes structs change)
Testing Strategy
Essential Tests
- test_load_problem_state: Load initial problem state, verify it matches
- test_load_solution_state: Load solved state, verify
completed=True - test_round_trip_problem: Problem state → dict → load → dict → compare
- test_round_trip_solution: Solution state → dict → load → dict → compare
- test_multiple_sizes: Test with different puzzle sizes (5x5, 10x10, 15x15)
- test_state_verification: Verify completion check results match
- test_swap_states_between_instances: Swap states between two puzzle instances
- test_validation_errors: Test invalid state dicts raise appropriate exceptions
- test_immutable_preservation: Verify immutable cells are preserved correctly
- test_clue_arrays: Verify rowdata and rowlen arrays are correctly preserved
Test Pattern
def test_pattern_example():
puzzle = rp.Puzzle('pattern', arg='5x5', 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 = {
'common.refcount', # Internal refcounting
'common.fontsize', # Recomputed from rowdata
}
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 to Modify
puzzles/pattern.c:
- Add
pattern_state_reprstruct definition - Implement
pattern_state_from_repr()function
- Add
rlp/specific_api.py:
- Add
PatternStateReprctypes structure inset_api_structures_pattern() - Implement
load_state_dict_pattern()function
- Add
rlp/puzzle.py:
- Wire
pattern_state_from_reprin__init__() - Add/update
load_state_dict()method to support pattern
- Wire
test_load_state_dict.py (or create new test file):
- Create comprehensive test suite for pattern puzzle
Checklist for Implementation
- Define C repr struct (
pattern_state_repr) with canonical fields - Implement C reconstruction function (
pattern_state_from_repr)- Validate input dimensions and sizes
- Allocate game_state and game_state_common
- Copy canonical arrays (grid, rowdata, rowlen, immutable)
- Recompute fontsize from rowdata
- Recompute completed flag using compute_rowdata comparison
- Set refcount = 1 and cheated = false
- Define Python ctypes struct (
PatternStateRepr) matching C exactly - Wire C function in puzzle.py
__init__() - Implement Python load function (
load_state_dict_pattern) with validation - Add/update
Puzzle.load_state_dict()method - Create comprehensive tests
- Recompile library:
cmake --build rlp/lib --target libpattern - Run tests and verify all pass
Common Pitfalls
- Incorrect rowdata layout → Clues don't match rows/columns
- Forgetting to recompute fontsize → UI display issues
- Wrong completion check logic → States marked complete when they shouldn't be
- Not validating array lengths → Crashes or corruption
- Memory leaks → Not freeing loaded states in tests
- Pointer vs contents confusion → AttributeError or incorrect behavior
- Struct field order mismatch → Silent corruption or crashes
- Not recompiling after C changes → Old code runs, tests fail mysteriously
- Mixing up columns and rows → First w entries are columns, next h are rows
Success Criteria
- All tests pass
- Round-trip tests show no differences (except ignored fields: refcount, fontsize)
- State swapping works correctly
- Memory is properly managed (no leaks)
- Validation catches invalid inputs
- Completion verification works correctly
- Immutable cells are preserved
- Clue arrays (rowdata, rowlen) are correctly preserved