testspace / submodules /rlp /ASCII_PARSER_IMPLEMENTATION_GUIDE_new.md
nilshoehing's picture
Upload TopoBench Space app
67acd34 verified
|
Raw
History Blame Contribute Delete
17 kB
# ASCII Parser Implementation Guide for Bridges Puzzles
This document provides a comprehensive guide to implementing ASCII parsers for puzzle games, using the Bridges puzzle as a reference implementation. This guide can be used to create similar parsers for other puzzles (undead, loopy, etc.).
## Table of Contents
1. [Overview](#overview)
2. [Architecture](#architecture)
3. [Implementation Structure](#implementation-structure)
4. [Key Components](#key-components)
5. [Testing Strategy](#testing-strategy)
6. [Gotchas and Lessons Learned](#gotchas-and-lessons-learned)
7. [Integration Points](#integration-points)
8. [Step-by-Step Implementation Checklist](#step-by-step-implementation-checklist)
---
## Overview
### Purpose
The ASCII parser converts ASCII text representations of puzzle states into Python dictionaries that match the format produced by `get_puzzle_state_<puzzle_name>()` functions. This enables:
- **Verification**: Check if an ASCII state (from LLM or other sources) is solved
- **State Loading**: Load arbitrary puzzle states from ASCII text
- **Round-trip Testing**: Verify ASCII → state dict → load → format → ASCII works correctly
- **Integration**: Use ASCII states with existing `load_state_dict` functionality
### Pipeline
The complete pipeline follows this flow:
```
ASCII Text → Parse (Python) → State Dict → Load (C) → Game State → Verify/Format
```
1. **Parse ASCII** (Python): Convert ASCII text to state dictionary
2. **Load State Dict** (C): Use existing `load_state_dict_<puzzle>()` function
3. **Verify/Format** (C): Check `completed` flag or format back to ASCII
---
## Architecture
### Three-Layer Design
1. **Python Parser Layer** (`rlp/ascii_parser.py`)
- Pure Python implementation
- Parses ASCII text into state dictionaries
- No C dependencies for parsing logic
2. **State Dict Format Layer** (`rlp/specific_api.py`)
- Must match `get_puzzle_state_<puzzle>()` format exactly
- Defines the canonical state representation
3. **C Loading Layer** (existing `load_state_dict_<puzzle>()`)
- Reconstructs C game state from state dict
- Computes derived fields (possibles, max arrays, etc.)
- Validates and checks if solved
### Key Design Principle
**The parser only needs to produce the minimal canonical fields required for reconstruction.** The C code will:
- Recompute derived arrays (possibles, max arrays, etc.)
- Rebuild internal structures (island adjacencies, etc.)
- Validate the state and set `completed`/`solved` flags
---
## Implementation Structure
### File Organization
```
rlp/
ascii_parser.py # Main parser module (puzzle-specific functions)
specific_api.py # State dict format definitions
puzzle.py # Puzzle class (load_state_dict method)
verifier.py # Verification script using new pipeline
test_ascii_parser.py # Parser-specific tests
test_verifier.py # Integration tests
```
### Parser Function Signature
```python
def parse_ascii_<puzzle_name>(ascii_text: str) -> dict:
"""
Parse ASCII text representation of a <puzzle_name> puzzle and return a state dict.
Args:
ascii_text: The ASCII representation of the puzzle state
Returns:
dict: State dictionary matching get_puzzle_state_<puzzle_name> format
Raises:
ValueError: If the ASCII text is invalid or empty
"""
```
### State Dict Format
The parser must produce a dictionary that **exactly matches** the format from `get_puzzle_state_<puzzle_name>()`. For bridges, this includes:
```python
{
"w": int, # Width
"h": int, # Height
"completed": bool, # Will be computed by C code (set to False)
"solved": bool, # Will be computed by C code (set to False)
"grid": List[int], # Grid flags array (w*h elements)
"lines": List[int], # Lines array (w*h elements)
"islands": List[dict], # Island structures
"n_islands": int, # Number of islands
"n_islands_alloc": int, # Allocation size (same as n_islands for parsed)
"params": { # Minimal params (only for verification/printing)
"w": int,
"h": int,
"maxb": int, # Default: 2 for bridges
"allowloops": bool, # Default: True for bridges
},
# Derived arrays - initialized to zeros, computed by C code
"wha": List[int], # w*h elements
"possv": List[int], # w*h elements
"possh": List[int], # w*h elements
"maxv": List[int], # w*h elements
"maxh": List[int], # w*h elements
}
```
---
## Key Components
### 1. ASCII Format Understanding
**For Bridges:**
- Islands: `'0'-'9'` (count 0-9) or `'A'-'G'` (count 10-16)
- Vertical bridges: `'|'` (single), `'"'` (double)
- Horizontal bridges: `'-'` (single), `'='` (double)
- Empty cells: `'.'`
**Key Insight**: Study the C `game_text_format()` function to understand the exact ASCII format. This is the inverse operation.
### 2. Dimension Inference
```python
# First pass: infer dimensions
lines = ascii_text.strip().split('\n')
h = len(lines)
max_w = max(len(line.rstrip()) for line in lines)
w = max_w
```
**Important**: Handle variable line lengths gracefully. Shorter lines are treated as empty cells.
### 3. Grid Flags
Define constants matching the C definitions:
```python
# Grid flags matching <puzzle>.c definitions
G_ISLAND = 0x0001
G_LINEV = 0x0002 # contains a vertical line
G_LINEH = 0x0004 # contains a horizontal line
```
**Critical**: These must match the C `#define` values exactly.
### 4. State Dict Construction
```python
# Initialize arrays
wh = w * h
grid = [0] * wh
lines_array = [0] * wh
islands = []
# Parse each cell
for y, line in enumerate(lines):
for x in range(w):
c = stripped[x] if x < len(stripped) else None
idx = y * w + x
if c is island_char:
grid[idx] |= G_ISLAND
islands.append({"x": x, "y": y, "count": count})
elif c is line_char:
grid[idx] |= G_LINE_FLAG
lines_array[idx] = line_count # 1 or 2
# Empty cells remain 0
```
### 5. Minimal Params
Only include params needed for verification/printing:
```python
"params": {
"w": w,
"h": h,
# Only include fields needed for load_state_dict
# Omit generation params: islands, expansion, difficulty
}
```
---
## Testing Strategy
### 1. Unit Tests (Parser-Specific)
**File**: `test_ascii_parser.py`
Test cases:
- **Problem states** (initial puzzle, no solution)
- **Solution states** (complete solution with all elements)
- **Edge cases**: Empty states, single cell, maximum size
- **Special characters**: Letter islands (A-G), double bridges
- **Round-trip**: ASCII → parse → load → format → ASCII (must match)
### 2. Integration Tests (Verifier)
**File**: `test_verifier.py`
Test cases:
- **CSV predictions**: Verify problems return `solved=False`, solutions return `solved=True`
- **State comparison**: Compare two similar ASCII states
- **Large-scale testing**: Test all problems/solutions from dataset
### 3. Round-Trip Testing
The most important test pattern:
```python
# 1. Get ASCII from a puzzle state
ascii_original = game.text_format(state).decode('utf-8')
# 2. Parse with Python parser
state_dict = parse_ascii_bridges(ascii_original)
# 3. Load into C
loaded_state_ptr = puzzle.load_state_dict(state_dict)
# 4. Format back to ASCII
ascii_loaded = game.text_format(loaded_state_ptr).decode('utf-8')
# 5. Compare (should match exactly)
assert ascii_original.strip() == ascii_loaded.strip()
```
### 4. Structural Validity (Optional but Recommended)
For puzzles with connectivity requirements (like bridges), add a structural validity check:
```python
def check_<puzzle>_structural_validity(ascii_text: str) -> bool:
"""
Check structural validity before parsing.
Validates:
- Lines are contiguous (no breaks)
- Clues haven't been modified
- Basic sanity checks
"""
```
This catches common errors early (broken lines, modified clues, etc.).
---
## Gotchas and Lessons Learned
### 1. Grid Flags Must Match C Exactly
**Problem**: Grid flag values must match C `#define` values exactly.
**Solution**: Copy the exact hex values from the C source file.
```python
# From bridges.c:
#define G_ISLAND 0x0001
#define G_LINEV 0x0002
#define G_LINEH 0x0004
# In Python:
G_ISLAND = 0x0001 # Must match exactly
G_LINEV = 0x0002
G_LINEH = 0x0004
```
### 2. Derived Arrays Can Be Zeros
**Problem**: Don't try to compute derived arrays (possibles, max arrays) in Python.
**Solution**: Initialize to zeros. The C code will recompute them:
```python
# Derived arrays - initialized to zeros, will be computed by C code
"wha": [0] * wh,
"possv": [0] * wh,
"possh": [0] * wh,
"maxv": [0] * wh,
"maxh": [0] * wh,
```
### 3. Only Include Canonical Fields
**Problem**: Including non-canonical fields (generation params, solver flags) causes issues.
**Solution**: Only include fields needed for reconstruction. Omit:
- Generation params: `islands`, `expansion`, `difficulty`
- Solver flags: `G_SWEEP`, `G_WARN` (these are computed by C)
- Allocation details: `n_islands_alloc` can be set to `n_islands` for parsed states
### 4. Handle Variable Line Lengths
**Problem**: ASCII text may have lines of different lengths.
**Solution**: Use maximum width, treat shorter lines as having empty cells:
```python
max_w = max(len(line.rstrip()) for line in lines)
# For cells beyond line length, treat as empty (already 0)
```
### 5. Memory Management
**Problem**: Must free loaded states to avoid memory leaks.
**Solution**: Always use try/finally:
```python
loaded_state_ptr = puzzle.load_state_dict(state_dict)
try:
# Use the state
is_solved = loaded_state_ptr.contents.completed
finally:
if loaded_state_ptr:
free_game_func(loaded_state_ptr)
```
### 6. State Dict Format Must Match Exactly
**Problem**: Even small differences in state dict format cause failures.
**Solution**:
- Copy the exact structure from `get_puzzle_state_<puzzle>()`
- Use the same field names and types
- Test with round-trip verification
### 7. Letter Islands (A-G)
**Problem**: Islands can have counts 10-16 represented as letters.
**Solution**: Handle both digits and letters:
```python
if c >= '0' and c <= '9':
count = ord(c) - ord('0')
elif c >= 'A' and c <= 'G':
count = (ord(c) - ord('A')) + 10
```
### 8. Double Bridges
**Problem**: Double bridges use different characters (`"` for vertical, `=` for horizontal).
**Solution**: Check for both single and double bridge characters:
```python
elif c == '|':
lines_array[idx] = 1
elif c == '"':
lines_array[idx] = 2
elif c == '-':
lines_array[idx] = 1
elif c == '=':
lines_array[idx] = 2
```
### 9. Empty Cells vs Missing Cells
**Problem**: Need to distinguish between empty cells (`.`) and cells beyond line length.
**Solution**: Both are treated the same (grid=0, lines=0), but handle bounds checking:
```python
if x >= len(stripped):
continue # Beyond line length, already initialized to 0
elif c == '.':
pass # Empty cell, already initialized to 0
```
### 10. Testing with Real Data
**Problem**: Unit tests may not catch all edge cases.
**Solution**: Test with real dataset:
- Parse all problems and solutions from CSV
- Verify round-trip works for all
- Check that problems are unsolved and solutions are solved
---
## Integration Points
### 1. Verifier Script
The verifier uses the parser in a complete pipeline:
```python
def verify_ascii_state(puzzle, ascii_text: str) -> str:
# 1. Optional: Check structural validity
if not check_bridges_structural_validity(ascii_text):
return "NOT SOLVED"
# 2. Parse ASCII
state_dict = parse_ascii_bridges(ascii_text)
# 3. Load state dict
loaded_state_ptr = puzzle.load_state_dict(state_dict)
# 4. Check completed flag
is_solved = loaded_state_ptr.contents.completed
return "SOLVED" if is_solved else "NOT SOLVED"
```
### 2. Load State Dict Function
The parser output must be compatible with existing `load_state_dict_<puzzle>()`:
```python
# In rlp/specific_api.py
def load_state_dict_bridges(state_dict: dict, lib: c.PyDLL) -> c.POINTER(GameState):
# Validates required fields
# Creates C structures
# Calls bridges_state_from_repr()
```
### 3. Puzzle Class
The Puzzle class provides the interface:
```python
# In rlp/puzzle.py
puzzle = Puzzle('bridges', arg='5x5de', headless=True)
puzzle.new_game()
state_dict = parse_ascii_bridges(ascii_text)
loaded_state = puzzle.load_state_dict(state_dict)
```
---
## Step-by-Step Implementation Checklist
### Phase 1: Research and Understanding
- [ ] Study the C `game_text_format()` function to understand ASCII format
- [ ] Review `get_puzzle_state_<puzzle>()` to understand state dict format
- [ ] Identify all ASCII characters and their meanings
- [ ] Identify grid flags and their values from C source
- [ ] Understand the puzzle's state structure
### Phase 2: Parser Implementation
- [ ] Create `rlp/ascii_parser.py` (or add to existing file)
- [ ] Define grid flag constants matching C
- [ ] Implement dimension inference
- [ ] Implement cell-by-cell parsing
- [ ] Build grid array with correct flags
- [ ] Build lines/connections array
- [ ] Extract puzzle-specific structures (islands, etc.)
- [ ] Construct state dict matching `get_puzzle_state_<puzzle>()` format
- [ ] Add error handling and validation
### Phase 3: Structural Validity (Optional)
- [ ] Implement structural validity checker
- [ ] Validate connectivity (if applicable)
- [ ] Validate clues haven't been modified
- [ ] Add sanity checks
### Phase 4: Testing
- [ ] Create `test_ascii_parser.py`
- [ ] Test problem states (unsolved)
- [ ] Test solution states (solved)
- [ ] Test edge cases (empty, single cell, max size)
- [ ] Test special characters/features
- [ ] Implement round-trip tests
- [ ] Test with real dataset (CSV)
### Phase 5: Integration
- [ ] Update `verifier.py` to use new parser
- [ ] Create/update `test_verifier.py`
- [ ] Test CSV predictions (problems vs solutions)
- [ ] Verify memory management (no leaks)
### Phase 6: Documentation
- [ ] Document ASCII format
- [ ] Document parser function
- [ ] Document state dict format
- [ ] Add examples and usage
---
## Example: Bridges Parser Structure
```python
# rlp/ascii_parser.py
# 1. Grid flags (must match C)
G_ISLAND = 0x0001
G_LINEV = 0x0002
G_LINEH = 0x0004
# 2. Structural validity (optional)
def check_bridges_structural_validity(ascii_text: str) -> bool:
# Validates line continuity, clue integrity, etc.
pass
# 3. Main parser
def parse_ascii_bridges(ascii_text: str) -> dict:
# Dimension inference
lines = ascii_text.strip().split('\n')
h = len(lines)
w = max(len(line.rstrip()) for line in lines)
# Initialize arrays
wh = w * h
grid = [0] * wh
lines_array = [0] * wh
islands = []
# Parse cells
for y, line in enumerate(lines):
for x in range(w):
c = line[x] if x < len(line.rstrip()) else None
idx = y * w + x
if c in '0-9A-G': # Island
grid[idx] |= G_ISLAND
islands.append({"x": x, "y": y, "count": ...})
elif c in '|"': # Vertical line
grid[idx] |= G_LINEV
lines_array[idx] = 2 if c == '"' else 1
elif c in '-=': # Horizontal line
grid[idx] |= G_LINEH
lines_array[idx] = 2 if c == '=' else 1
# Build state dict
return {
"w": w,
"h": h,
"grid": grid,
"lines": lines_array,
"islands": islands,
# ... rest of fields
}
```
---
## Key Takeaways
1. **Study the C code first**: Understand `game_text_format()` and state structure
2. **Match formats exactly**: State dict must match `get_puzzle_state_<puzzle>()` exactly
3. **Minimal canonical fields**: Only include what's needed for reconstruction
4. **Let C compute derived fields**: Initialize derived arrays to zeros
5. **Round-trip testing is critical**: ASCII → parse → load → format → ASCII must match
6. **Handle edge cases**: Variable line lengths, special characters, empty states
7. **Memory management**: Always free loaded states
8. **Test with real data**: Use actual problems and solutions from datasets
---
## References
- Bridges parser: `rlp/ascii_parser.py`
- State dict format: `rlp/specific_api.py` (lines 2576-2585)
- C text format: `puzzles/bridges.c` (lines 234-266)
- C state structure: `puzzles/bridges.c` (lines 177-189)
- Load state dict: `rlp/specific_api.py` (lines 2588-2686)
- Tests: `test_ascii_parser.py`, `test_verifier.py`
- Verifier: `verifier.py`