testspace / submodules /rlp /verifier.py
nilshoehing's picture
Upload TopoBench Space app
67acd34 verified
Raw
History Blame Contribute Delete
15 kB
#!/usr/bin/env python3
"""
Simple script to verify if a puzzle ASCII state is solved.
Supports: bridges, undead, galaxies, pattern, loopy
Usage:
# From stdin
python verifier.py bridges < ascii_state.txt
python verifier.py undead < ascii_state.txt
# From command line argument
python verifier.py bridges "3|.|2\n..."
python verifier.py undead "G: 3 V: 1 Z: 6\n\n 2 3 1 1 \n..."
python verifier.py galaxies "+-+-+\n|o o|\n+-+-+\n|o o|\n+-+-+\n"
python verifier.py loopy " x x x - x \nx x0x |3| x\n..."
# With escape sequences
echo "3|.|2\n..." | python verifier.py bridges
echo "G: 3 V: 1 Z: 6\n\n..." | python verifier.py undead
echo "+-+-+\n|o o|\n..." | python verifier.py galaxies
"""
import sys
import argparse
import warnings
from rlp.puzzle import Puzzle
from rlp.ascii_parser import parse_ascii_bridges, check_bridges_structural_validity, parse_ascii_undead, check_undead_structural_validity, parse_ascii_galaxies, check_galaxies_structural_validity, parse_ascii_pattern, parse_ascii_loopy
def verify_ascii_state(puzzle, ascii_text: str, problem_ascii: str = None) -> str:
"""
Verify if an ASCII puzzle state is solved.
Args:
puzzle: Puzzle instance (must be initialized with new_game())
ascii_text: ASCII representation of puzzle state
problem_ascii: Optional ASCII representation of the original problem state.
When provided, checks that pre-filled cells haven't been modified.
For bridges: checks islands haven't moved/changed.
For galaxies: checks dots haven't moved/removed.
Returns:
str: "SOLVED" if the state is solved, "NOT SOLVED" otherwise
Raises:
Exception: If verification fails
"""
puzzle_type = puzzle.puzzle_name
try:
if puzzle_type == "bridges":
# Warn if problem_ascii not provided
if problem_ascii is None:
warnings.warn(
"verify_ascii_state called for bridges without problem_ascii. "
"Startboard modification check will be skipped.",
UserWarning
)
# First check structural validity (includes island modification check if problem_ascii provided)
if not check_bridges_structural_validity(str(ascii_text), problem_ascii=problem_ascii):
# Structurally invalid (broken lines, modified clues, etc.)
return "NOT SOLVED"
# Parse ASCII with Python parser
state_dict = parse_ascii_bridges(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 (completed flag)
is_solved = loaded_state_ptr.contents.completed
if is_solved:
return "SOLVED"
else:
return "NOT SOLVED"
finally:
# Free the loaded state
if loaded_state_ptr:
free_game_func(loaded_state_ptr)
elif puzzle_type == "undead":
# First check structural validity
if not check_undead_structural_validity(str(ascii_text)):
# Structurally invalid (missing header, no grid, invalid format, etc.)
return "NOT SOLVED"
# Use new pipeline: parse → load → check
# Parse ASCII with Python parser
state_dict = parse_ascii_undead(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 (undead only has 'solved' field, not 'completed')
is_solved = loaded_state_ptr.contents.solved
if is_solved:
return "SOLVED"
else:
return "NOT SOLVED"
finally:
# Free the loaded state
if loaded_state_ptr:
free_game_func(loaded_state_ptr)
elif puzzle_type == "galaxies":
# Warn if problem_ascii not provided
if problem_ascii is None:
warnings.warn(
"verify_ascii_state called for galaxies without problem_ascii. "
"Startboard modification check will be skipped.",
UserWarning
)
# First check structural validity (includes dot modification check if problem_ascii provided)
if not check_galaxies_structural_validity(str(ascii_text), problem_ascii=problem_ascii):
# Structurally invalid (invalid dimensions, dots modified, etc.)
return "NOT SOLVED"
# Use new pipeline: parse → load → check
# Parse ASCII with Python parser
state_dict = parse_ascii_galaxies(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 (galaxies uses 'completed' field)
is_solved = loaded_state_ptr.contents.completed
if is_solved:
return "SOLVED"
else:
return "NOT SOLVED"
finally:
# Free the loaded state
if loaded_state_ptr:
free_game_func(loaded_state_ptr)
elif puzzle_type == "pattern":
# Use new pipeline: parse → load → check
# Parse ASCII with Python parser
state_dict = parse_ascii_pattern(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 (pattern uses 'completed' field)
is_solved = loaded_state_ptr.contents.completed
if is_solved:
return "SOLVED"
else:
return "NOT SOLVED"
finally:
# Free the loaded state
if loaded_state_ptr:
free_game_func(loaded_state_ptr)
elif puzzle_type == "loopy":
# Use new pipeline: parse → load → check
# IMPORTANT: Early dimension check to prevent segfaults
# Creating temp puzzle instances for each unique dimension causes segfaults
# after ~20 instances. Validate dimensions BEFORE calling parse_ascii_loopy
# to avoid creating temp puzzles for invalid/malformed ASCII.
expected_dimensions = {(5, 5), (7, 7), (10, 10)} # easy, medium, hard
# Pre-check dimensions without creating any puzzle instances
try:
ascii_str = str(ascii_text)
if ascii_str.endswith('\n'):
lines = ascii_str[:-1].split('\n')
else:
lines = ascii_str.split('\n')
content_lines = [line for line in lines if line.strip() or len(line) > 0]
H = len(content_lines) if len(content_lines) > 0 else len(lines)
max_W_content = max((len(line) for line in lines), default=0)
if max_W_content == 0:
return "NOT SOLVED"
W = max_W_content + 1
if (H - 1) % 2 != 0:
H += 1
# Check valid dimension format
if (W - 2) % 2 != 0 or (H - 1) % 2 != 0:
return "NOT SOLVED"
w = (W - 2) // 2
h = (H - 1) // 2
if w < 1 or h < 1:
return "NOT SOLVED"
# Reject unexpected dimensions to prevent temp puzzle creation
if (w, h) not in expected_dimensions:
return "NOT SOLVED"
except Exception:
return "NOT SOLVED"
try:
# Parse ASCII with Python parser, passing existing puzzle instance to avoid creating temp ones
state_dict = parse_ascii_loopy(str(ascii_text), grid_type=0, puzzle_instance=puzzle)
except ValueError as e:
# Check if this is a dimension validation error (model mistake in response format)
error_msg = str(e)
if any(keyword in error_msg for keyword in [
"Invalid canvas width",
"Invalid canvas height",
"Invalid dimensions",
"Dimension mismatch"
]):
# Model made a mistake in the response format - treat as NOT SOLVED
return "NOT SOLVED"
else:
# Other ValueError - might indicate a bug, re-raise
raise
# 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 (loopy uses 'solved' field)
is_solved = loaded_state_ptr.contents.solved
if is_solved:
return "SOLVED"
else:
return "NOT SOLVED"
finally:
# Free the loaded state
if loaded_state_ptr:
free_game_func(loaded_state_ptr)
else:
# For other puzzles, fall back to old method
is_solved = puzzle.check_ascii_solved(str(ascii_text))
if is_solved:
return "SOLVED"
else:
return "NOT SOLVED"
except ValueError as e:
# Check if this is a dimension validation error from parsing (model mistake)
error_msg = str(e)
if any(keyword in error_msg for keyword in [
"Invalid canvas width",
"Invalid canvas height",
"Invalid dimensions",
"Dimension mismatch",
"Invalid grid width",
"Invalid grid height"
]):
# Model made a mistake in the response format - treat as NOT SOLVED
return "NOT SOLVED"
else:
# Other ValueError - might indicate a bug, re-raise
raise
except Exception as e:
# Other unexpected errors - re-raise with context
raise Exception(f"Failed to check puzzle state: {e}") from e
def main():
"""Read ASCII state and check if solved."""
parser = argparse.ArgumentParser(
description='Verify if a puzzle ASCII state is solved',
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
python verifier.py bridges "3|.|2\\n..."
python verifier.py undead "G: 3 V: 1 Z: 6\\n\\n 2 3 1 1 \\n..."
python verifier.py galaxies "+-+-+\\n|o o|\\n..."
echo "..." | python verifier.py bridges
"""
)
parser.add_argument(
'puzzle_type',
choices=['bridges', 'undead', 'galaxies', 'pattern', 'loopy'],
help='Type of puzzle to verify'
)
parser.add_argument(
'ascii_text',
nargs='?',
help='ASCII representation of puzzle state (if not provided, read from stdin)'
)
parser.add_argument(
'--arg',
default=None,
help='Puzzle initialization argument (e.g., "5x5deL" for bridges, "4x4" for undead)'
)
parser.add_argument(
'--problem',
default=None,
help='Original problem ASCII state (for checking startboard modifications)'
)
args = parser.parse_args()
# Read ASCII input
if args.ascii_text:
# ASCII state provided as command line argument
ascii_text = args.ascii_text
# Convert escape sequences like \n to actual newlines
# This handles cases where user passes "text\nmore" from command line
ascii_text = ascii_text.encode().decode('unicode_escape')
else:
# Read from stdin
ascii_text = sys.stdin.read()
if not ascii_text.strip():
print("Error: No ASCII state provided", file=sys.stderr)
sys.exit(1)
# Determine puzzle initialization argument if not provided
if args.arg is None:
if args.puzzle_type == "bridges":
args.arg = '5x5deL' # Default for bridges
elif args.puzzle_type == "undead":
args.arg = '4x4' # Default for undead
elif args.puzzle_type == "galaxies":
args.arg = '4x4' # Default for galaxies
elif args.puzzle_type == "pattern":
args.arg = '5x5' # Default for pattern
elif args.puzzle_type == "loopy":
args.arg = '5x5t0' # Default for loopy (5x5 square grid)
# Create puzzle instance
puzzle = Puzzle(args.puzzle_type, arg=args.arg, headless=True)
puzzle.new_game() # Initialize the game structure
try:
# Process problem ASCII if provided
problem_ascii = None
if args.problem:
problem_ascii = args.problem.encode().decode('unicode_escape')
# Verify the ASCII state
result = verify_ascii_state(puzzle, ascii_text, problem_ascii=problem_ascii)
print(result)
sys.exit(0 if result == "SOLVED" else 1)
except Exception as e:
print(f"Error: {e}", file=sys.stderr)
import traceback
traceback.print_exc()
sys.exit(1)
finally:
# Clean up puzzle instance
import gc
gc.collect()
del puzzle
gc.collect()
if __name__ == "__main__":
main()