#!/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: " \n" "\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 " " 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()