Spaces:
Sleeping
Sleeping
File size: 15,161 Bytes
67acd34 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 | #!/usr/bin/env python3
"""
verifier.py
Verify if a given ASCII board state is a valid solution to a FlowFree puzzle.
Uses the modified solver (flowfree_all_solutions.c) to find all solutions and
compares the given solution against them.
"""
import argparse
import platform
import re
import subprocess
import sys
from collections import defaultdict
from itertools import chain
from pathlib import Path
from typing import Dict, List, Optional, Tuple
# Reuse patterns from convert_freeflow.py
HEADER_RE = re.compile(r"^\s*(\d+)\s+(\d+)\s+(\d+)\s*$")
COORD_RE = re.compile(r"\(\s*(\d+)\s*,\s*(\d+)\s*\)")
SOLUTIONS_RE = re.compile(r"^\s*Solutions\s+(\d+)\s*$", re.IGNORECASE)
def letter_labels(n):
"""Generate letter labels for colors (A, B, C, ..., Z, a, b, ..., z, AA, AB, ...)."""
base = [chr(i) for i in range(ord('A'), ord('Z')+1)]
extra = [chr(i) for i in range(ord('a'), ord('z')+1)]
if n <= len(base) + len(extra):
return list(chain(base, extra))[:n]
labels = list(chain(base, extra))
i = 0
while len(labels) < n:
for ch in base:
labels.append(base[i % len(base)] + ch)
if len(labels) == n:
break
i += 1
return labels
def build_all_solutions_solver(src: Path, bin_: Path) -> None:
"""Compile the modified solver that outputs all solutions."""
print(f"[build] compiling {src.name} -> {bin_.name}")
if platform.system().lower().startswith("win"):
cmd = ["gcc", "-std=c11", "-O2", "-Wall", "-Wextra", "-o", str(bin_), str(src)]
else:
cmd = ["gcc", "-std=c11", "-O2", "-Wall", "-Wextra", "-o", str(bin_), str(src)]
try:
subprocess.run(cmd, check=True)
except FileNotFoundError as e:
raise SystemExit(
"Error: C compiler not found (gcc). Install gcc or build the solver manually."
) from e
except subprocess.CalledProcessError as e:
raise SystemExit(f"Build failed with exit code {e.returncode}")
def parse_ascii_board(ascii_str: str) -> Tuple[List[List[str]], int, int]:
"""
Parse ASCII board string to 2D grid.
Returns: (grid as 2D list, cols, rows)
"""
lines = ascii_str.strip().split('\n')
grid = []
for line in lines:
line = line.strip()
if line:
grid.append(list(line))
if not grid:
raise ValueError("Empty board")
rows = len(grid)
cols = len(grid[0]) if rows > 0 else 0
# Validate all rows have same length
for i, row in enumerate(grid):
if len(row) != cols:
raise ValueError(f"Row {i} has length {len(row)}, expected {cols}")
return grid, cols, rows
def extract_endpoints(grid: List[List[str]], cols: int, rows: int) -> Dict[str, List[Tuple[int, int]]]:
"""
Find all endpoint pairs for each color.
Endpoints are cells that appear exactly once or twice per color (the two endpoints).
Returns: dict mapping color labels to list of (x, y) coordinates (col, row, zero-based).
"""
color_positions = defaultdict(list)
# Find all positions for each color
for y in range(rows):
for x in range(cols):
cell = grid[y][x]
if cell != '.' and cell.strip():
color_positions[cell].append((x, y))
endpoints = {}
for color, positions in color_positions.items():
if len(positions) == 2:
# Exactly 2 cells - these are the endpoints
endpoints[color] = positions
elif len(positions) == 1:
# Only one cell found - might be a single endpoint visible
# In problem boards, we expect exactly 2 endpoints per color
endpoints[color] = positions
else:
# More than 2 cells - this is a solution board, not a problem board
# For problem boards, we should only have endpoints
raise ValueError(f"Color '{color}' appears {len(positions)} times, expected 2 for problem board")
return endpoints
def convert_to_solver_input(endpoints: Dict[str, List[Tuple[int, int]]], cols: int, rows: int) -> str:
"""
Convert endpoints to solver input format.
Format:
<num_colors> <cols> <rows>
(x1, y1) (x2, y2)
...
"""
# Sort colors to ensure consistent ordering
sorted_colors = sorted(endpoints.keys())
num_colors = len(sorted_colors)
lines = [f"{num_colors} {cols} {rows}"]
for color in sorted_colors:
positions = endpoints[color]
if len(positions) == 2:
(x1, y1), (x2, y2) = positions
lines.append(f"({x1}, {y1}) ({x2}, {y2})")
elif len(positions) == 1:
# Only one endpoint found - this shouldn't happen in valid puzzles
# But handle it gracefully
(x1, y1) = positions[0]
lines.append(f"({x1}, {y1}) ({x1}, {y1})") # Duplicate as fallback
else:
raise ValueError(f"Color '{color}' has {len(positions)} endpoints, expected 2")
return "\n".join(lines) + "\n"
def run_solver(solver_bin: str, puzzle_input: str) -> Tuple[str, str, int]:
"""
Run the modified C solver with puzzle input.
Returns: (stdout, stderr, returncode)
"""
# Resolve path to absolute
solver_path = Path(solver_bin).resolve()
if not solver_path.exists():
raise SystemExit(f"Solver binary not found: {solver_path}")
try:
proc = subprocess.run(
[str(solver_path)],
input=puzzle_input,
text=True,
capture_output=True,
check=False,
)
return proc.stdout, proc.stderr, proc.returncode
except FileNotFoundError:
raise SystemExit(f"Solver binary not found: {solver_path}")
def parse_all_solutions(solver_output: str, cols: int, rows: int) -> List[List[List[str]]]:
"""
Parse solver output to extract ALL solution blocks.
Each solution block contains paths: (x1, y1) (x2, y2) ... (xn, yn) per color.
Solutions are separated by empty lines.
Returns: List of 2D grids (one per solution found).
"""
lines = solver_output.splitlines()
solutions = []
i = 0
# Find the number of colors from the first solution block
# We'll infer it from the first complete solution we find
num_colors = None
while i < len(lines):
# Skip status lines
if (SOLUTIONS_RE.match(lines[i]) or
"Touching" in lines[i] or
"Nodes" in lines[i] or
"Attempts" in lines[i] or
HEADER_RE.match(lines[i])): # Skip header lines if present
i += 1
continue
# Look for a solution block starting at an empty line or coordinate line
# Solutions start with an empty line (from puts("") in the solver)
# or directly with coordinate lines
if not lines[i].strip():
i += 1
# Next lines should be coordinate lines for a solution
# Collect coordinate lines for one solution
paths = []
j = i
while j < len(lines):
line = lines[j].strip()
# Stop at status lines
if (SOLUTIONS_RE.match(lines[j]) or
"Touching" in lines[j] or
"Nodes" in lines[j] or
"Attempts" in lines[j]):
break
# Stop at empty line if we already have paths (next solution)
if not line:
if paths:
break
j += 1
continue
# Try to parse coordinates
coords = COORD_RE.findall(line)
if coords:
path = [(int(x), int(y)) for x, y in coords]
paths.append(path)
j += 1
else:
# Non-coordinate line - might be end of solution or start of next
if paths:
break
j += 1
# If we found paths, it's a solution
if paths:
# Infer num_colors from first solution
if num_colors is None:
num_colors = len(paths)
# Only process if we have the expected number of colors
if len(paths) == num_colors:
try:
labels = letter_labels(num_colors)
grid = build_grid(cols, rows)
grid = place_paths_on_grid(cols, rows, paths, labels)
solutions.append(grid)
except Exception as e:
# Skip invalid solutions
pass
i = j
else:
i += 1
return solutions
def build_grid(cols: int, rows: int) -> List[List[str]]:
"""Build an empty grid."""
return [['.' for _ in range(cols)] for _ in range(rows)]
def place_paths_on_grid(cols: int, rows: int, paths: List[List[Tuple[int, int]]], labels: List[str]) -> List[List[str]]:
"""
Fill the grid with labels along paths.
Reused from convert_freeflow.py logic.
"""
grid = build_grid(cols, rows)
for label, path in zip(labels, paths):
for (x, y) in path:
if not (0 <= x < cols and 0 <= y < rows):
raise ValueError(f"Coordinate {(x,y)} out of bounds for {cols}x{rows}")
if grid[y][x] != '.' and grid[y][x] != label:
raise ValueError(f"Cell {(x,y)} already occupied by '{grid[y][x]}' (overlap).")
grid[y][x] = label
return grid
def convert_solution_to_ascii(grid: List[List[str]]) -> str:
"""Convert 2D grid back to ASCII string format."""
return '\n'.join(''.join(row) for row in grid)
def normalize_ascii_board(ascii_str: str) -> str:
"""Normalize ASCII board for comparison (handle whitespace, newlines)."""
lines = [line.strip() for line in ascii_str.strip().split('\n') if line.strip()]
return '\n'.join(lines)
def verify_solution(
problem_ascii: str,
solution_ascii: str,
solver_src: str = "./flowfree_all_solutions.c",
solver_bin: str = "./flowfree_all_solutions",
print_solutions: bool = False
) -> Tuple[bool, List[List[List[str]]]]:
"""
Main verification function.
Returns: (is_valid, all_solutions) where:
- is_valid: True if solution matches any valid solution, False otherwise
- all_solutions: List of all valid solutions found by the solver (as 2D grids)
"""
solver_src_path = Path(solver_src)
solver_bin_path = Path(solver_bin)
# Build solver if needed
if not solver_bin_path.exists():
if not solver_src_path.exists():
raise SystemExit(f"Solver source not found: {solver_src_path}")
build_all_solutions_solver(solver_src_path, solver_bin_path)
# Parse problem board
try:
problem_grid, cols, rows = parse_ascii_board(problem_ascii)
except Exception as e:
print(f"Error parsing problem board: {e}", file=sys.stderr)
return False, []
# Extract endpoints
try:
endpoints = extract_endpoints(problem_grid, cols, rows)
except Exception as e:
print(f"Error extracting endpoints: {e}", file=sys.stderr)
return False, []
# Convert to solver input
try:
puzzle_input = convert_to_solver_input(endpoints, cols, rows)
except Exception as e:
print(f"Error converting to solver input: {e}", file=sys.stderr)
return False, []
# Run solver
try:
stdout, stderr, returncode = run_solver(str(solver_bin_path), puzzle_input)
if returncode != 0:
print(f"Solver failed with exit code {returncode}", file=sys.stderr)
if stderr:
print(f"Stderr: {stderr}", file=sys.stderr)
return False, []
except Exception as e:
print(f"Error running solver: {e}", file=sys.stderr)
return False, []
# Parse all solutions
try:
solutions = parse_all_solutions(stdout, cols, rows)
except Exception as e:
print(f"Error parsing solutions: {e}", file=sys.stderr)
return False, []
if not solutions:
print("No solutions found by solver", file=sys.stderr)
return False, []
# Print all solutions if requested
if print_solutions:
print(f"\nFound {len(solutions)} solution(s):", file=sys.stderr)
for i, sol_grid in enumerate(solutions, 1):
sol_ascii = convert_solution_to_ascii(sol_grid)
print(f"\nSolution {i}:", file=sys.stderr)
print(sol_ascii, file=sys.stderr)
# Normalize the given solution
normalized_solution = normalize_ascii_board(solution_ascii)
# Compare against all solutions
for sol_grid in solutions:
sol_ascii = convert_solution_to_ascii(sol_grid)
normalized_sol = normalize_ascii_board(sol_ascii)
if normalized_solution == normalized_sol:
return True, solutions
return False, solutions
def main():
ap = argparse.ArgumentParser(
description="Verify if an ASCII board state is a valid solution to a FlowFree puzzle",
epilog="""
Example usage:
python verifier.py \\
--problem "BBBBBBBB\\nBFFFFFFB\\nFFGGGGFB\\nFGGAAGFB\\nFGAAGGBB\\nEGGGGDBC\\nEGGGDDBC\\nEEEEEBBC" \\
--solution "BBBBBBBB\\nBFFFFFFB\\nFFGGGGFB\\nFGGAAGFB\\nFGAAGGBB\\nEGGGGDBC\\nEGGGDDBC\\nEEEEEBBC"
"""
)
ap.add_argument("--problem", required=True, help="Problem board as ASCII string (with \\n for newlines)")
ap.add_argument("--solution", required=True, help="Solution board as ASCII string (with \\n for newlines)")
ap.add_argument("--solver-src", default="./flowfree_all_solutions.c", help="Path to solver source")
ap.add_argument("--solver-bin", default="./flowfree_all_solutions", help="Path to solver binary")
ap.add_argument("--print-solutions", action="store_true", help="Print all solutions found by the solver")
args = ap.parse_args()
# Convert \n escape sequences to actual newlines
problem = args.problem.replace('\\n', '\n')
solution = args.solution.replace('\\n', '\n')
is_valid, all_solutions = verify_solution(problem, solution, args.solver_src, args.solver_bin, args.print_solutions)
if is_valid:
print("VALID: Solution matches a valid solution found by the solver")
sys.exit(0)
else:
print("INVALID: Solution does not match any valid solution")
if args.print_solutions and all_solutions:
print(f"\nNote: The solver found {len(all_solutions)} valid solution(s) (printed above).", file=sys.stderr)
sys.exit(1)
# Example usage as a Python function:
#
# problem = '''BBBBBBBB
# BFFFFFFB
# FFGGGGFB
# FGGAAGFB
# FGAAGGBB
# EGGGGDBC
# EGGGDDBC
# EEEEEBBC'''
#
# solution = '''BBBBBBBB
# BFFFFFFB
# FFGGGGFB
# FGGAAGFB
# FGAAGGBB
# EGGGGDBC
# EGGGDDBC
# EEEEEBBC'''
#
# is_valid = verify_solution(problem, solution)
# print(f"Solution is valid: {is_valid}")
if __name__ == "__main__":
main()
|