testspace / submodules /rlp /ASCII_PARSER_IMPLEMENTATION_TEMPLATE.md
nilshoehing's picture
Upload TopoBench Space app
67acd34 verified
|
Raw
History Blame Contribute Delete
22.5 kB
# ASCII Parser Implementation Template
This template provides step-by-step instructions for implementing ASCII parsers for puzzle games. Use this guide to create parsers for new puzzle types following the established pattern from bridges and undead implementations.
## Table of Contents
1. [Overview](#overview)
2. [Prerequisites](#prerequisites)
3. [Step-by-Step Implementation](#step-by-step-implementation)
4. [Code Structure](#code-structure)
5. [Testing Strategy](#testing-strategy)
6. [Integration Checklist](#integration-checklist)
7. [Common Patterns and Examples](#common-patterns-and-examples)
8. [Troubleshooting](#troubleshooting)
---
## 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
```
ASCII Text → Structural Validation → Parse (Python) → State Dict → Load (C) → Game State → Verify/Format
```
1. **Structural Validation** (Python): Quick check if ASCII looks valid
2. **Parse ASCII** (Python): Convert ASCII text to state dictionary
3. **Load State Dict** (C): Use existing `load_state_dict_<puzzle>()` function
4. **Verify/Format** (C): Check `completed`/`solved` flag or format back to ASCII
---
## Prerequisites
Before starting, you need:
1. **Access to C source code**: `puzzles/<puzzle_name>.c`
2. **Understanding of ASCII format**: Study `game_text_format()` function in C
3. **Understanding of state dict format**: Review `get_puzzle_state_<puzzle_name>()` in `rlp/specific_api.py`
4. **Understanding of load function**: Review `load_state_dict_<puzzle_name>()` in `rlp/specific_api.py`
5. **Example ASCII states**: Have sample problem and solution states to test with
---
## Step-by-Step Implementation
### Phase 1: Research and Understanding
#### Step 1.1: Study the C ASCII Format Function
**Location**: `puzzles/<puzzle_name>.c`
**Function**: `game_text_format()`
**What to look for**:
- How the function formats the puzzle state to ASCII
- What characters represent different cell types
- How dimensions are represented
- What the header/prefix looks like (if any)
- How multi-line structures are formatted
**Example questions to answer**:
- What characters represent different cell states?
- Are there special characters for different values?
- How are dimensions encoded?
- Is there a header line (like "G: X V: Y Z: Z" for undead)?
- How are edge clues/borders represented?
**Document your findings**:
```python
# ASCII Format for <puzzle_name>:
# - Header: <description>
# - Cell types:
# - <char1>: <meaning>
# - <char2>: <meaning>
# - Grid structure: <description>
```
#### Step 1.2: Study the State Dict Format
**Location**: `rlp/specific_api.py`
**Function**: `get_puzzle_state_<puzzle_name>()`
**What to look for**:
- Required top-level fields
- Nested structures (like `common` for undead)
- Array types and lengths
- Field names and types
- Which fields are canonical (required) vs derived (computed by C)
**Key insight**: Only include canonical fields in the parser output. Derived fields should be initialized to zeros/False and computed by C code.
**Document the structure**:
```python
# State dict format for <puzzle_name>:
{
"field1": <type>, # Required
"field2": <type>, # Required
"nested": { # If applicable
"subfield1": <type>,
},
"derived_array": [0] * size, # Will be computed by C
}
```
#### Step 1.3: Study Cell Type Constants
**Location**: `puzzles/<puzzle_name>.c`
**What to look for**:
- `#define` statements for grid flags
- `enum` definitions for cell types
- Any constants used to represent cell states
**Example**:
```c
// From undead.c:
enum {
CELL_EMPTY,
CELL_MIRROR_L,
CELL_MIRROR_R,
CELL_GHOST,
CELL_VAMPIRE,
CELL_ZOMBIE,
};
```
**Action**: Copy these exact values to Python constants in `rlp/ascii_parser.py`
#### Step 1.4: Study the Load Function
**Location**: `rlp/specific_api.py`
**Function**: `load_state_dict_<puzzle_name>()`
**What to look for**:
- Required fields validation
- Array length requirements
- Type requirements
- Which fields are optional vs required
**This tells you**: What fields your parser MUST produce for the state dict to load successfully.
---
### Phase 2: Structural Validity Checker (Optional but Recommended)
#### Step 2.1: Implement `check_<puzzle_name>_structural_validity()`
**Purpose**: Quick validation before parsing to catch obviously invalid inputs.
**Location**: `rlp/ascii_parser.py`
**What to check**:
- [ ] Has required header/prefix (if applicable)
- [ ] Has grid structure present
- [ ] Grid has minimum dimensions
- [ ] Not empty text
- [ ] Not just long unstructured text
- [ ] Basic format sanity checks
**Template**:
```python
def check_<puzzle_name>_structural_validity(ascii_text: str) -> bool:
"""
Check structural validity of a <puzzle_name> ASCII state.
Validates:
1. <Check 1>
2. <Check 2>
3. <Check 3>
Args:
ascii_text: The ASCII representation of the puzzle state
Returns:
bool: True if structurally valid, False otherwise
"""
if not ascii_text or not ascii_text.strip():
return False
lines = ascii_text.strip().split('\n')
if len(lines) == 0:
return False
# Check for required header/prefix
# <implementation>
# Check for grid structure
# <implementation>
# Check minimum dimensions
# <implementation>
# Check for invalid patterns (long text, etc.)
# <implementation>
return True
```
**Testing**: Create test cases with valid and invalid inputs.
---
### Phase 3: Parser Implementation
#### Step 3.1: Add Constants
**Location**: `rlp/ascii_parser.py`
Add cell type constants matching C definitions:
```python
# Cell types matching <puzzle_name>.c enum/defines
CONSTANT_NAME_1 = <value>
CONSTANT_NAME_2 = <value>
# ... etc
```
**Important**: Values must match C exactly (copy hex values, enum values, etc.)
#### Step 3.2: Implement Dimension Inference
**Pattern**:
```python
# First pass: infer dimensions
lines = ascii_text.strip().split('\n')
h = len(lines)
if h == 0:
raise ValueError("ASCII text must contain at least one line")
# Find maximum width (handle variable line lengths)
max_w = 0
for line in lines:
stripped = line.rstrip()
w = len(stripped)
if w > max_w:
max_w = w
if max_w == 0:
raise ValueError("ASCII text must contain at least one non-whitespace character")
w = max_w
```
**Variations**:
- Some puzzles have fixed-width cells (e.g., undead: 2 characters per cell)
- Some puzzles have headers to skip
- Some puzzles have border cells to account for
#### Step 3.3: Implement Cell-by-Cell Parsing
**Pattern**:
```python
# Initialize arrays
wh = w * h # or (w+2)*(h+2) if including border
grid = [0] * wh
other_array = [0] * wh
structures = [] # For puzzle-specific structures (islands, etc.)
# Parse each cell
for y, line in enumerate(lines):
stripped = line.rstrip()
for x in range(w):
if x >= len(stripped):
# Line is shorter, treat as empty
continue
c = stripped[x] # or cell_str = stripped[x*2:(x+1)*2] for fixed-width
idx = y * w + x # or y * grid_w + x if including border
# Parse based on character/cell
if c == '<char1>':
grid[idx] = CONSTANT_1
# ... set other fields
elif c == '<char2>':
grid[idx] = CONSTANT_2
# ... set other fields
# ... etc
```
**Key considerations**:
- Handle variable line lengths gracefully
- Map ASCII characters to cell type constants
- Build puzzle-specific structures (islands, monsters, etc.)
- Track indices correctly (especially if grid includes border)
#### Step 3.4: Build State Dict
**Pattern**:
```python
state_dict = {
# Required top-level fields
"field1": value1,
"field2": value2,
# Nested structures (if applicable)
"nested": {
"params": {
"w": w,
"h": h,
# Only include canonical params, omit generation params
},
"array1": array1,
"array2": array2,
},
# Derived arrays - initialized to zeros, computed by C code
"derived_array": [0] * size,
# Status flags - will be computed by C code
"completed": False,
"solved": False,
}
```
**Important**:
- Only include canonical fields (required for `load_state_dict`)
- Initialize derived arrays to zeros
- Set `completed`/`solved` to False (C will compute)
- Omit generation params (islands, expansion, difficulty, etc.)
#### Step 3.5: Add Error Handling
**Pattern**:
```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
"""
if not ascii_text or not ascii_text.strip():
raise ValueError("ASCII text cannot be empty")
# ... parsing logic ...
# Validate dimensions
if w < 1 or h < 1:
raise ValueError(f"Invalid grid dimensions: w={w}, h={h}")
# ... return state_dict ...
```
---
### Phase 4: Testing
#### Step 4.1: Create Test File
**Location**: `test_ascii_parser_<puzzle_name>.py`
**Template structure**:
```python
"""
Test script for <puzzle_name> ASCII parser round-trip functionality.
"""
import sys
import os
import pandas as pd
from rlp import puzzle as rp
from rlp.ascii_parser import parse_ascii_<puzzle_name>, check_<puzzle_name>_structural_validity
def test_parse_example():
"""Test parsing with a known example."""
# <example ASCII>
# ... test logic ...
def test_structural_validity():
"""Test structural validity checker."""
# Valid examples
# Invalid examples
# ... test logic ...
def test_round_trip():
"""Test round-trip: ASCII → parse → load → format → ASCII."""
# ... test logic ...
def test_csv_predictions_round_trip():
"""Test round-trip for all problems and solutions in CSV."""
# ... test logic ...
if __name__ == "__main__":
# Run all tests
# ... test runner ...
```
#### Step 4.2: Test Cases to Include
1. **Basic parsing test**: Known valid ASCII state
2. **Structural validity test**: Valid and invalid examples
3. **Round-trip test**: Generate puzzle → get ASCII → parse → load → format → compare
4. **Edge cases**: Empty states, single cell, maximum size, special characters
5. **CSV round-trip**: Test all problems/solutions from dataset
#### Step 4.3: Round-Trip Test Pattern
**Critical test**:
```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_<puzzle_name>(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.contents).decode('utf-8')
# 5. Compare (should match exactly)
assert ascii_original.strip() == ascii_loaded.strip()
```
---
### Phase 5: Integration
#### Step 5.1: Update Verifier
**Location**: `verifier.py`
**Changes needed**:
1. Import parser function: `from rlp.ascii_parser import parse_ascii_<puzzle_name>, check_<puzzle_name>_structural_validity`
2. Add puzzle type case in `verify_ascii_state()`:
```python
elif puzzle_type == "<puzzle_name>":
# First check structural validity
if not check_<puzzle_name>_structural_validity(str(ascii_text)):
return "NOT SOLVED"
# Parse ASCII with Python parser
state_dict = parse_ascii_<puzzle_name>(str(ascii_text))
# Load state dict
loaded_state_ptr = puzzle.load_state_dict(state_dict)
# Get free_game function
me = puzzle.fe.contents.me.contents
game = me.ourgame.contents
free_game_func = game.free_game
try:
# Check if solved (use correct field: completed or solved)
is_solved = loaded_state_ptr.contents.<field> # completed or solved
if is_solved:
return "SOLVED"
else:
return "NOT SOLVED"
finally:
if loaded_state_ptr:
free_game_func(loaded_state_ptr)
```
**Important**: Check which field the puzzle uses (`completed` for bridges, `solved` for undead)
#### Step 5.2: Update Test Verifier
**Location**: `test_verifier.py`
**Changes needed**:
1. Add puzzle type parameter to test functions
2. Add puzzle-specific configuration (difficulty args, puzzle name in CSV)
3. Add puzzle type to command-line argument choices
#### Step 5.3: Update Evaluate Predictions (if applicable)
**Location**: `evaluate-predictions.py`
**Changes needed**:
1. Add puzzle type to command-line arguments
2. Add puzzle-specific configuration
3. Update puzzle name filtering
---
## Code Structure
### File: `rlp/ascii_parser.py`
**Structure**:
```python
# Constants for puzzle 1
CONSTANT_1 = value1
CONSTANT_2 = value2
# Structural validity for puzzle 1
def check_puzzle1_structural_validity(ascii_text: str) -> bool:
...
# Parser for puzzle 1
def parse_ascii_puzzle1(ascii_text: str) -> dict:
...
# Constants for puzzle 2
CONSTANT_3 = value3
# Structural validity for puzzle 2
def check_puzzle2_structural_validity(ascii_text: str) -> bool:
...
# Parser for puzzle 2
def parse_ascii_puzzle2(ascii_text: str) -> dict:
...
```
### Function Signature Template
```python
def parse_ascii_<puzzle_name>(ascii_text: str) -> dict:
"""
Parse ASCII text representation of a <puzzle_name> puzzle and return a state dict.
ASCII Format:
- <Description of format>
- <Cell types and meanings>
- <Special structures>
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
"""
# Implementation
```
---
## Testing Strategy
### Unit Tests (Parser-Specific)
**File**: `test_ascii_parser_<puzzle_name>.py`
**Test categories**:
1. **Basic parsing**: Valid ASCII states parse correctly
2. **Structural validity**: Valid vs invalid inputs
3. **Edge cases**: Empty, single cell, max size, special characters
4. **Round-trip**: ASCII → parse → load → format → ASCII (must match)
5. **CSV integration**: Test with real dataset
### Integration Tests (Verifier)
**File**: `test_verifier.py`
**Test categories**:
1. **CSV predictions**: Problems return `solved=False`, solutions return `solved=True`
2. **State comparison**: Compare two similar ASCII states
3. **Large-scale testing**: Test all problems/solutions from dataset
---
## Integration Checklist
### Parser Implementation
- [ ] Constants defined matching C values
- [ ] Structural validity checker implemented (optional but recommended)
- [ ] Parser function implemented
- [ ] State dict format matches `get_puzzle_state_<puzzle_name>()` exactly
- [ ] Error handling added
- [ ] Documentation added
### Testing
- [ ] Unit tests created (`test_ascii_parser_<puzzle_name>.py`)
- [ ] Structural validity tests pass
- [ ] Round-trip tests pass
- [ ] Edge case tests pass
- [ ] CSV round-trip tests pass (if applicable)
### Integration
- [ ] Verifier updated (`verifier.py`)
- [ ] Test verifier updated (`test_verifier.py`)
- [ ] Evaluate predictions updated (if applicable)
- [ ] All tests pass
---
## Common Patterns and Examples
### Pattern 1: Simple Grid (like bridges)
```python
# Dimension inference
lines = ascii_text.strip().split('\n')
h = len(lines)
w = max(len(line.rstrip()) for line in lines)
# Cell-by-cell parsing
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
# Parse character
```
### Pattern 2: Grid with Border (like undead)
```python
# Grid is (w+2) x (h+2) including border
grid_w = max_cells # from parsing
grid_h = len(grid_lines)
w = grid_w - 2
h = grid_h - 2
# Cell-by-cell parsing with border
for y, line in enumerate(grid_lines):
for x in range(grid_w):
cell_str = line[x*2:(x+1)*2] # 2-character cells
idx = y * grid_w + x
# Parse cell
```
### Pattern 3: Header + Grid
```python
# Parse header
first_line = lines[0].strip()
# Extract header information
# Find grid start
grid_start = 1
if len(lines) > 1 and lines[1].strip() == '':
grid_start = 2
grid_lines = lines[grid_start:]
# Parse grid
```
### Pattern 4: Helper Functions
```python
def helper_function_for_puzzle(x, y, w, h):
"""
Helper function ported from C.
Args:
x, y: Coordinates
w, h: Dimensions
Returns:
<return type>
"""
# Ported logic from C
pass
```
---
## Troubleshooting
### Common Issues and Solutions
#### Issue 1: State Dict Format Mismatch
**Symptom**: `load_state_dict()` raises ValueError about missing fields
**Solution**:
- Compare your state dict with output from `get_puzzle_state_<puzzle_name>()`
- Ensure all required fields are present
- Check field names match exactly (case-sensitive)
- Verify array lengths match requirements
#### Issue 2: Round-Trip Mismatch
**Symptom**: ASCII after round-trip doesn't match original
**Solution**:
- Check dimension inference is correct
- Verify cell parsing maps characters correctly
- Ensure border cells are handled correctly (if applicable)
- Check if whitespace differences matter (use `.strip()` for comparison)
#### Issue 3: Grid Flags Don't Match
**Symptom**: Parsed state doesn't behave correctly
**Solution**:
- Verify constants match C `#define` values exactly
- Check hex values are correct (0x0001 vs 0x001, etc.)
- Ensure bitwise operations are correct
#### Issue 4: Derived Arrays Issues
**Symptom**: Errors about array access or computation
**Solution**:
- Initialize derived arrays to zeros (don't try to compute in Python)
- Let C code recompute them
- Ensure array lengths are correct
#### Issue 5: Memory Leaks
**Symptom**: Memory usage grows over time
**Solution**:
- Always use try/finally to free loaded states
- Call `free_game_func(loaded_state_ptr)` in finally block
- Don't forget to free states in test loops
#### Issue 6: Structural Validity Too Strict/Loose
**Symptom**: Valid states rejected or invalid states accepted
**Solution**:
- Adjust validation criteria
- Test with edge cases
- Balance between catching errors and being lenient
---
## Quick Reference
### Key Files
- **Parser implementation**: `rlp/ascii_parser.py`
- **State dict format**: `rlp/specific_api.py` (`get_puzzle_state_<puzzle_name>()`)
- **Load function**: `rlp/specific_api.py` (`load_state_dict_<puzzle_name>()`)
- **C ASCII format**: `puzzles/<puzzle_name>.c` (`game_text_format()`)
- **C state structure**: `puzzles/<puzzle_name>.c` (struct definitions)
- **Verifier**: `verifier.py`
- **Tests**: `test_ascii_parser_<puzzle_name>.py`, `test_verifier.py`
### Key Functions
- `parse_ascii_<puzzle_name>(ascii_text: str) -> dict`: Main parser
- `check_<puzzle_name>_structural_validity(ascii_text: str) -> bool`: Quick validation
- `verify_ascii_state(puzzle, ascii_text: str) -> str`: Verifier integration
- `puzzle.load_state_dict(state_dict: dict)`: Load state into C
### Key Principles
1. **Match formats exactly**: State dict must match `get_puzzle_state_<puzzle_name>()` exactly
2. **Minimal canonical fields**: Only include what's needed for reconstruction
3. **Let C compute derived fields**: Initialize derived arrays to zeros
4. **Round-trip testing is critical**: ASCII → parse → load → format → ASCII must match
5. **Handle edge cases**: Variable line lengths, special characters, empty states
6. **Memory management**: Always free loaded states
7. **Test with real data**: Use actual problems and solutions from datasets
---
## Example: Complete Implementation Checklist
For a new puzzle called "example":
### Phase 1: Research
- [ ] Study `puzzles/example.c` `game_text_format()` function
- [ ] Review `get_puzzle_state_example()` in `rlp/specific_api.py`
- [ ] Review `load_state_dict_example()` in `rlp/specific_api.py`
- [ ] Identify all ASCII characters and their meanings
- [ ] Identify cell type constants from C source
- [ ] Understand the puzzle's state structure
### Phase 2: Implementation
- [ ] Add constants to `rlp/ascii_parser.py`
- [ ] Implement `check_example_structural_validity()` (optional)
- [ ] Implement `parse_ascii_example()` function
- [ ] Test with known examples
### Phase 3: Testing
- [ ] Create `test_ascii_parser_example.py`
- [ ] Test structural validity (if implemented)
- [ ] Test basic parsing
- [ ] Test round-trip
- [ ] Test edge cases
- [ ] Test with CSV (if applicable)
### Phase 4: Integration
- [ ] Update `verifier.py` to use new parser
- [ ] Update `test_verifier.py` to test example puzzle
- [ ] Update `evaluate-predictions.py` (if applicable)
- [ ] Verify all tests pass
---
## References
- Bridges parser: `rlp/ascii_parser.py` (`parse_ascii_bridges`, `check_bridges_structural_validity`)
- Undead parser: `rlp/ascii_parser.py` (`parse_ascii_undead`, `check_undead_structural_validity`)
- Original guide: `ASCII_PARSER_IMPLEMENTATION_GUIDE_new.md`
- State dict format: `rlp/specific_api.py`
- C text format: `puzzles/<puzzle_name>.c` (`game_text_format()`)
- Tests: `test_ascii_parser.py`, `test_ascii_parser_undead.py`, `test_verifier.py`
---
## Notes
- This template is based on successful implementations for bridges and undead puzzles
- Adapt patterns to your specific puzzle's requirements
- When in doubt, refer to existing implementations (bridges, undead) as examples
- Test thoroughly with real data before considering implementation complete
- Round-trip testing is the most important validation