Spaces:
Sleeping
Sleeping
| #!/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() | |