File size: 6,828 Bytes
67acd34
 
30865cf
f227b96
96e3e70
67acd34
 
 
 
 
 
 
 
 
 
96e3e70
b266f7c
 
96e3e70
 
 
67acd34
 
 
b266f7c
 
 
 
 
 
 
 
 
f227b96
30865cf
 
 
 
 
f227b96
 
67acd34
b266f7c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f227b96
 
 
 
 
96e3e70
 
f227b96
b266f7c
96e3e70
 
 
 
 
 
 
 
 
 
 
 
f227b96
b266f7c
f227b96
96e3e70
 
 
 
 
f227b96
 
b266f7c
 
 
 
 
 
 
 
96e3e70
 
 
 
 
 
 
f227b96
96e3e70
f227b96
96e3e70
 
f227b96
96e3e70
 
 
 
 
 
 
30865cf
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
67acd34
 
 
 
 
 
 
 
 
 
30865cf
 
 
f227b96
 
 
b266f7c
96e3e70
 
 
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
from __future__ import annotations

import json
import os
import subprocess
import sys
from pathlib import Path
from typing import Any


ROOT_DIR = Path(__file__).resolve().parents[1]
EVALS_SRC_DIR = ROOT_DIR / "evals" / "src"
if str(EVALS_SRC_DIR) not in sys.path:
    sys.path.insert(0, str(EVALS_SRC_DIR))

from evals_verifier import (  # type: ignore
    check_bridges_structural_validity,
    check_galaxies_structural_validity,
    check_undead_structural_validity,
    verify_with_rlp,
)
from puzzles import PUZZLES  # type: ignore


# Puzzle families whose "solved" check runs in the native RLP C library (loaded
# in-process via ctypes). That library can segfault or hang when its puzzle
# objects are reused across many requests in a long-lived server process, which
# crashes the web worker and surfaces as an opaque HTTP 500 (and, sometimes, a
# hung request). We run every native check in a short-lived subprocess so a
# native crash/hang kills only the child and we return a clean "not solved"
# result instead of taking down the worker. (flow_free already shells out to its
# own compiled solver binary, so it is naturally isolated.)
SUBPROCESS_RLP_PUZZLES = {"undead", "loopy", "bridges", "galaxies", "pattern"}
RLP_VERIFIER_TIMEOUT_SECONDS = 20
# flow_free shells out to a compiled C solver with no internal timeout and can
# raise SystemExit on a missing compiler/binary; isolate it too so it can never
# hang or crash the worker. Enumerating all solutions can be slower than the
# RLP solved-check, so it gets a more generous budget.
FLOW_FREE_TIMEOUT_SECONDS = 30


class VerificationService:
    def _structural_validity(
        self, *, puzzle_type: str, board_ascii: str, problem_ascii: str
    ) -> bool:
        # Pure-Python structural pre-checks are safe to run in-process and also
        # cover the "start board was modified" case for bridges/galaxies. Puzzle
        # families without such a check are treated as structurally valid here;
        # the subprocess solved-check is the source of truth for correctness.
        if puzzle_type == "bridges":
            return bool(
                check_bridges_structural_validity(board_ascii, problem_ascii=problem_ascii)
            )
        if puzzle_type == "galaxies":
            return bool(
                check_galaxies_structural_validity(board_ascii, problem_ascii=problem_ascii)
            )
        if puzzle_type == "undead":
            return bool(check_undead_structural_validity(board_ascii))
        return True

    def _verify_rlp_with_subprocess(
        self,
        *,
        puzzle_type: str,
        problem_ascii: str,
        board_ascii: str,
        args: str,
    ) -> dict[str, Any]:
        result = {
            "board_exists": bool(board_ascii),
            "board_valid": False,
            "board_modified": False,
            "correct": False,
        }
        if not board_ascii:
            return result

        result["board_valid"] = self._structural_validity(
            puzzle_type=puzzle_type, board_ascii=board_ascii, problem_ascii=problem_ascii
        )
        if not result["board_valid"]:
            result["board_modified"] = True
            return result

        verifier_path = ROOT_DIR / "submodules" / "rlp" / "verifier.py"
        command = [sys.executable, str(verifier_path), puzzle_type, "--arg", args]
        # The RLP libraries pull in pygame, which noisily probes audio/video on
        # import; force the dummy drivers (and silence warnings) so a headless
        # verifier run does not pollute the captured stderr.
        env = {
            **os.environ,
            "SDL_AUDIODRIVER": "dummy",
            "SDL_VIDEODRIVER": "dummy",
            "PYTHONWARNINGS": "ignore",
        }
        try:
            completed = subprocess.run(
                command,
                input=board_ascii,
                text=True,
                capture_output=True,
                cwd=ROOT_DIR,
                timeout=RLP_VERIFIER_TIMEOUT_SECONDS,
                check=False,
                env=env,
            )
        except subprocess.TimeoutExpired:
            result["error"] = f"{puzzle_type.capitalize()} verification timed out."
            return result

        result["correct"] = completed.returncode == 0 and "SOLVED" in completed.stdout
        if completed.stderr.strip():
            result["stderr"] = completed.stderr.strip()[:500]
        return result

    def _verify_flow_free_with_subprocess(
        self, *, problem_ascii: str, board_ascii: str
    ) -> dict[str, Any]:
        result = {
            "board_exists": bool(board_ascii),
            "board_valid": False,
            "board_modified": False,
            "correct": False,
        }
        if not board_ascii:
            return result

        runner_path = Path(__file__).resolve().parent / "flow_free_runner.py"
        payload = json.dumps({"problem_ascii": problem_ascii, "board_ascii": board_ascii})
        env = {**os.environ, "PYTHONWARNINGS": "ignore"}
        try:
            completed = subprocess.run(
                [sys.executable, str(runner_path)],
                input=payload,
                text=True,
                capture_output=True,
                cwd=ROOT_DIR,
                timeout=FLOW_FREE_TIMEOUT_SECONDS,
                check=False,
                env=env,
            )
        except subprocess.TimeoutExpired:
            result["error"] = "Flow_free verification timed out."
            return result

        if completed.returncode == 0 and completed.stdout.strip():
            try:
                parsed = json.loads(completed.stdout)
                if isinstance(parsed, dict):
                    return parsed
            except json.JSONDecodeError:
                pass
        result["error"] = "Flow_free verification failed."
        if completed.stderr.strip():
            result["stderr"] = completed.stderr.strip()[:500]
        return result

    def verify(
        self,
        *,
        puzzle_type: str,
        problem_ascii: str,
        board_ascii: str,
        args: str,
    ) -> dict[str, Any]:
        spec = PUZZLES[puzzle_type]
        if spec.verifier_type == "flow_free":
            return self._verify_flow_free_with_subprocess(
                problem_ascii=problem_ascii, board_ascii=board_ascii
            )
        if spec.verifier_type in SUBPROCESS_RLP_PUZZLES:
            return self._verify_rlp_with_subprocess(
                puzzle_type=spec.verifier_type,
                problem_ascii=problem_ascii,
                board_ascii=board_ascii,
                args=args,
            )
        return verify_with_rlp(
            puzzle_type=spec.verifier_type,
            problem_ascii=problem_ascii,
            extracted_board=board_ascii,
            args=args,
        )