testspace / submodules /flowfree /solve_generated.py
nilshoehing's picture
Upload TopoBench Space app
67acd34 verified
Raw
History Blame Contribute Delete
8.96 kB
#!/usr/bin/env python3
import argparse
import os
import platform
import re
import subprocess
from pathlib import Path
from typing import Optional, Tuple
import sys
RAW_HEADER_RE = re.compile(r'^\s*#\s*----\s*raw\s*puzzle\s*block', re.IGNORECASE)
def detect_solver_bin_name() -> str:
return "flowfree.exe" if platform.system().lower().startswith("win") else "flowfree"
def needs_rebuild(src: Path, bin_: Path) -> bool:
if not bin_.exists():
return True
try:
return src.stat().st_mtime > bin_.stat().st_mtime
except FileNotFoundError:
return True
def build_solver(src: Path, bin_: Path) -> None:
print(f"[build] compiling {src.name} -> {bin_.name}")
if platform.system().lower().startswith("win"):
# Prefer gcc from MSYS2/MinGW if available
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 extract_puzzle_block(text: str) -> Optional[str]:
"""
Returns the exact text to feed the solver in solver mode:
"<colors_n> <cols_n> <rows_n>\n"
"<pair line 1>\n"
...
"<pair line colors_n>\n"
"""
lines = text.splitlines()
n = len(lines)
i = 0
while i < n:
if RAW_HEADER_RE.search(lines[i]):
i += 1
# Skip blank lines after the header
while i < n and not lines[i].strip():
i += 1
if i >= n:
return None
# Expect "<colors_n> <cols_n> <rows_n>"
header_match = re.match(r'^\s*(\d+)\s+(\d+)\s+(\d+)\s*$', lines[i])
if not header_match:
return None
colors_n = int(header_match.group(1))
cols_n = int(header_match.group(2))
rows_n = int(header_match.group(3))
i += 1
# Collect the next colors_n non-empty lines that look like coordinate pairs
pairs = []
while i < n and len(pairs) < colors_n:
line = lines[i].strip()
if line:
# Accept anything that starts with '(' as a pair line
if line.startswith("("):
pairs.append(line)
else:
# If we encounter something else before collecting enough pairs, abort
return None
i += 1
if len(pairs) != colors_n:
return None
puzzle = f"{colors_n} {cols_n} {rows_n}\n" + "\n".join(pairs) + "\n"
return puzzle
i += 1
return None
def already_has_solution(text: str) -> bool:
return "# ---- solution (appended by solve_generated.py) ----" in text
def run_solver(bin_path: Path, puzzle_text: str) -> Tuple[str, str, int]:
"""Return (stdout, stderr, returncode)."""
try:
proc = subprocess.run(
[str(bin_path)],
input=puzzle_text,
text=True,
capture_output=True,
check=False,
)
return proc.stdout, proc.stderr, proc.returncode
except FileNotFoundError:
raise SystemExit(f"Solver binary not found: {bin_path}")
def convert_with_freeflow(raw_block: str, converter_path='./convert_freeflow.py') -> str:
"""
Pipe the raw block to convert_freeflow.py and capture the human-readable grid.
"""
proc = subprocess.run(
[sys.executable, converter_path],
input=raw_block.encode("utf-8"),
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
check=False,
)
if proc.returncode != 0:
# If conversion fails, include stderr for debugging in the saved file
pretty = f'"""CONVERSION FAILED (exit {proc.returncode})"""\n{proc.stderr.decode("utf-8", "replace")}'
else:
pretty = proc.stdout.decode("utf-8", "replace").strip()
return pretty
# def process_file(path: Path, solver_bin: Path, dry_run: bool=False) -> str:
# content = path.read_text(encoding="utf-8")
# if already_has_solution(content):
# return f"[skip] {path.name}: solution already appended"
# puzzle = extract_puzzle_block(content)
# if not puzzle:
# return f"[warn] {path.name}: could not find a valid raw puzzle block"
# out, err, rc = run_solver(solver_bin, puzzle)
# if rc != 0:
# return f"[fail] {path.name}: solver exit code {rc}\nstderr:\n{err}"
# append_block = (
# "\n# ---- solution (appended by solve_generated.py) ----\n"
# "## Solver input:\n"
# + puzzle
# + "\n## Solver output:\n"
# + out.strip()
# + ("\n\n## Solver stderr:\n" + err.strip() if err.strip() else "")
# + "\n"
# )
# print(append_block) # For visibility in dry-run mode
# asd
# if dry_run:
# return f"[dry-run] {path.name}: would append solution"
# else:
# with path.open("a", encoding="utf-8") as f:
# f.write(append_block)
# return f"[ok] {path.name}: solution appended"
def process_file(path: Path, solver_bin: Path, converter_path='./convert_freeflow.py', dry_run: bool=False) -> str:
content = path.read_text(encoding="utf-8")
if already_has_solution(content):
return f"[skip] {path.name}: solution already appended"
puzzle = extract_puzzle_block(content)
if not puzzle:
return f"[warn] {path.name}: could not find a valid raw puzzle block"
out, err, rc = run_solver(solver_bin, puzzle)
if rc != 0:
return f"[fail] {path.name}: solver exit code {rc}\nstderr:\n{err}"
# Make a pretty (human-readable) grid of the solver output
# Note: we pass solver stdout (the solution paths) to the converter.
# print('puzzle')
header = puzzle.splitlines()[0]
# print(puzzle)
# print('soln')
out = header + out
# print(out)
# asd
pretty_solution = convert_with_freeflow(out, str(converter_path))
append_block = (
"\n# ---- solution (appended by solve_generated.py) ----\n"
"## Human-readable solution:\n"
f"{pretty_solution}\n"
"\n## Raw solver output:\n"
f"{out.strip()}\n"
+ (f"\n\n## Solver stderr:\n{err.strip()}\n" if err.strip() else "\n")
+ "## Solver input (raw puzzle block):\n"
f"{puzzle}"
)
# print('append_block')
# print(append_block) # For visibility in dry-run mode
# asd
if dry_run:
# Show what we would append (useful for checking formatting)
print(append_block)
return f"[dry-run] {path.name}: would append solution"
else:
with path.open("a", encoding="utf-8") as f:
f.write(append_block)
return f"[ok] {path.name}: solution appended"
def main():
ap = argparse.ArgumentParser(description="Extract raw puzzle blocks, run solver, append solutions to files in generated/")
ap.add_argument("--generated-dir", default="generated", help="Directory with input files (default: generated)")
ap.add_argument("--solver-src", default="flowfree.c", help="C source file for the solver (default: flowfree.c)")
ap.add_argument("--solver-bin", default=None, help="Solver binary name/path (default: auto: flowfree or flowfree.exe)")
ap.add_argument("--dry-run", action="store_true", help="Show what would happen without writing")
args = ap.parse_args()
gen_dir = Path(args.generated_dir)
if not gen_dir.is_dir():
raise SystemExit(f"Directory not found: {gen_dir}")
solver_src = Path(args.solver_src)
solver_bin = Path(args.solver_bin) if args.solver_bin else Path(detect_solver_bin_name())
# # Build solver if needed
# if solver_src.exists() and needs_rebuild(solver_src, solver_bin):
# build_solver(solver_src, solver_bin)
# elif not solver_bin.exists():
# # If no src to build but also no binary, stop
# raise SystemExit(f"Solver binary not found: {solver_bin}. Put it next to this script or provide --solver-bin.")
# Process all regular files in generated/
results = []
for p in sorted(gen_dir.iterdir(), reverse=True):
# temp_path = '/notebooks/multimodal_cot/FlowFree/generated/5x5_3c_1_3b79ecca.txt'
# p = Path(temp_path)
if p.is_file():
try:
results.append(process_file(p, solver_bin, dry_run=args.dry_run))
except Exception as e:
results.append(f"[error] {p.name}: {e}")
# break
print("\n".join(results))
if __name__ == "__main__":
main()