nilshoehing commited on
Commit
96e3e70
·
verified ·
1 Parent(s): 59c2724

Fix puzzle UI and verifier normalization

Browse files
space_app/board_utils.py ADDED
@@ -0,0 +1,120 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from typing import Iterable
4
+
5
+
6
+ def split_lines(board: str) -> list[str]:
7
+ return board.replace("\r", "").split("\n")
8
+
9
+
10
+ def pad_lines(lines: Iterable[str], width: int) -> list[str]:
11
+ return [line.ljust(width) for line in lines]
12
+
13
+
14
+ def join_grid(grid: list[list[str]]) -> str:
15
+ return "\n".join("".join(row) for row in grid)
16
+
17
+
18
+ def _parse_loopy(problem_ascii: str) -> tuple[list[str], list[tuple[int, int]], list[tuple[int, int]]]:
19
+ lines = split_lines(problem_ascii)
20
+ rows = max(0, (len(lines) - 3) // 2)
21
+ cols = max(0, ((len(lines[0]) if lines else 0) - 3) // 2)
22
+ horizontal_edges: list[tuple[int, int]] = []
23
+ vertical_edges: list[tuple[int, int]] = []
24
+
25
+ for row in range(rows + 1):
26
+ for col in range(cols):
27
+ horizontal_edges.append((1 + 2 * row, 2 + 2 * col))
28
+
29
+ for row in range(rows):
30
+ for col in range(cols + 1):
31
+ vertical_edges.append((2 + 2 * row, 1 + 2 * col))
32
+
33
+ return lines, horizontal_edges, vertical_edges
34
+
35
+
36
+ def normalize_loopy_board(problem_ascii: str, board_ascii: str) -> str:
37
+ puzzle_lines, horizontal_edges, vertical_edges = _parse_loopy(problem_ascii)
38
+ width = len(puzzle_lines[0]) if puzzle_lines else 0
39
+ grid = [list(line) for line in pad_lines(split_lines(board_ascii), width)]
40
+ if not grid:
41
+ grid = [list(line) for line in pad_lines(puzzle_lines, width)]
42
+
43
+ for row, col in horizontal_edges:
44
+ current = grid[row][col]
45
+ if current != "-":
46
+ grid[row][col] = "x"
47
+
48
+ for row, col in vertical_edges:
49
+ current = grid[row][col]
50
+ if current != "|":
51
+ grid[row][col] = "x"
52
+
53
+ return join_grid(grid)
54
+
55
+
56
+ def _parse_pattern_rows(board_ascii: str) -> tuple[list[str], list[str], list[str]]:
57
+ lines = split_lines(board_ascii)
58
+ clue_lines: list[str] = []
59
+ content_lines: list[str] = []
60
+ border_lines: list[str] = []
61
+
62
+ grid_started = False
63
+ for line in lines:
64
+ if "|" in line:
65
+ grid_started = True
66
+ content_lines.append(line)
67
+ elif "+" in line and "-" in line:
68
+ if grid_started:
69
+ border_lines.append(line)
70
+ else:
71
+ clue_lines.append(line)
72
+ else:
73
+ clue_lines.append(line)
74
+
75
+ return clue_lines, content_lines, border_lines
76
+
77
+
78
+ def normalize_pattern_board(board_ascii: str) -> str:
79
+ clue_lines, content_lines, border_lines = _parse_pattern_rows(board_ascii)
80
+ if not content_lines:
81
+ return board_ascii
82
+
83
+ normalized: list[str] = [*clue_lines]
84
+ border_iter = iter(border_lines)
85
+
86
+ if border_lines:
87
+ normalized.append(next(border_iter))
88
+
89
+ for content in content_lines:
90
+ pieces = content.split("|")
91
+ if len(pieces) < 3:
92
+ normalized.append(content)
93
+ continue
94
+ next_pieces = [pieces[0]]
95
+ for cell in pieces[1:-1]:
96
+ next_pieces.append(".." if cell == " " else cell)
97
+ next_pieces.append(pieces[-1])
98
+ normalized.append("|".join(next_pieces))
99
+ try:
100
+ normalized.append(next(border_iter))
101
+ except StopIteration:
102
+ pass
103
+
104
+ return "\n".join(normalized)
105
+
106
+
107
+ def normalize_board_for_display(*, puzzle_type: str, problem_ascii: str, board_ascii: str) -> str:
108
+ if puzzle_type == "loopy":
109
+ return normalize_loopy_board(problem_ascii, board_ascii)
110
+ if puzzle_type == "pattern":
111
+ return normalize_pattern_board(board_ascii)
112
+ return board_ascii
113
+
114
+
115
+ def normalize_board_for_submission(*, puzzle_type: str, problem_ascii: str, board_ascii: str) -> str:
116
+ if puzzle_type == "loopy":
117
+ return normalize_loopy_board(problem_ascii, board_ascii)
118
+ if puzzle_type == "pattern":
119
+ return normalize_pattern_board(board_ascii)
120
+ return board_ascii
space_app/main.py CHANGED
@@ -8,6 +8,7 @@ from fastapi import Depends, FastAPI, Header, HTTPException, Query
8
  from fastapi.responses import FileResponse
9
  from fastapi.staticfiles import StaticFiles
10
 
 
11
  from .config import Settings
12
  from .dataset import DatasetStore
13
  from .db import SessionStore
@@ -28,6 +29,11 @@ def _session_to_response(
28
  problem_ascii: str,
29
  image_base64: str | None,
30
  ) -> SessionResponse:
 
 
 
 
 
31
  return SessionResponse(
32
  session_id=str(session["id"]),
33
  engine=str(session["engine"]),
@@ -39,7 +45,7 @@ def _session_to_response(
39
  started_at=session["started_at"],
40
  payload=SessionPayload(
41
  problem_ascii=problem_ascii,
42
- current_board_ascii=str(session["submitted_artifact"] or problem_ascii),
43
  image_base64=image_base64,
44
  ),
45
  )
@@ -161,16 +167,21 @@ def create_app(
161
  if not session["started_at"]:
162
  raise HTTPException(status_code=409, detail="Session is not ready yet.")
163
  row = current_dataset_store.get_row(str(session["puzzle_filename"]))
164
- verification = current_verifier.verify(
165
  puzzle_type=str(session["puzzle_type"]),
166
  problem_ascii=row.problem,
167
  board_ascii=request.board_ascii,
 
 
 
 
 
168
  args=str(session["args"]),
169
  )
170
  updated = current_session_store.record_submission(
171
  session_id=session_id,
172
  solved=bool(verification.get("correct")),
173
- submitted_artifact=request.board_ascii,
174
  verification_payload=verification,
175
  )
176
  return SubmitResponse(
 
8
  from fastapi.responses import FileResponse
9
  from fastapi.staticfiles import StaticFiles
10
 
11
+ from .board_utils import normalize_board_for_display, normalize_board_for_submission
12
  from .config import Settings
13
  from .dataset import DatasetStore
14
  from .db import SessionStore
 
29
  problem_ascii: str,
30
  image_base64: str | None,
31
  ) -> SessionResponse:
32
+ current_board_ascii = normalize_board_for_display(
33
+ puzzle_type=str(session["puzzle_type"]),
34
+ problem_ascii=problem_ascii,
35
+ board_ascii=str(session["submitted_artifact"] or problem_ascii),
36
+ )
37
  return SessionResponse(
38
  session_id=str(session["id"]),
39
  engine=str(session["engine"]),
 
45
  started_at=session["started_at"],
46
  payload=SessionPayload(
47
  problem_ascii=problem_ascii,
48
+ current_board_ascii=current_board_ascii,
49
  image_base64=image_base64,
50
  ),
51
  )
 
167
  if not session["started_at"]:
168
  raise HTTPException(status_code=409, detail="Session is not ready yet.")
169
  row = current_dataset_store.get_row(str(session["puzzle_filename"]))
170
+ normalized_board_ascii = normalize_board_for_submission(
171
  puzzle_type=str(session["puzzle_type"]),
172
  problem_ascii=row.problem,
173
  board_ascii=request.board_ascii,
174
+ )
175
+ verification = current_verifier.verify(
176
+ puzzle_type=str(session["puzzle_type"]),
177
+ problem_ascii=row.problem,
178
+ board_ascii=normalized_board_ascii,
179
  args=str(session["args"]),
180
  )
181
  updated = current_session_store.record_submission(
182
  session_id=session_id,
183
  solved=bool(verification.get("correct")),
184
+ submitted_artifact=normalized_board_ascii,
185
  verification_payload=verification,
186
  )
187
  return SubmitResponse(
space_app/verification.py CHANGED
@@ -1,5 +1,6 @@
1
  from __future__ import annotations
2
 
 
3
  import sys
4
  from pathlib import Path
5
  from typing import Any
@@ -10,11 +11,56 @@ EVALS_SRC_DIR = ROOT_DIR / "evals" / "src"
10
  if str(EVALS_SRC_DIR) not in sys.path:
11
  sys.path.insert(0, str(EVALS_SRC_DIR))
12
 
13
- from evals_verifier import verify_flow_free, verify_with_rlp # type: ignore
 
 
 
 
14
  from puzzles import PUZZLES # type: ignore
15
 
16
 
17
  class VerificationService:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
18
  def verify(
19
  self,
20
  *,
@@ -26,10 +72,14 @@ class VerificationService:
26
  spec = PUZZLES[puzzle_type]
27
  if spec.verifier_type == "flow_free":
28
  return verify_flow_free(problem_ascii, board_ascii)
 
 
 
 
 
29
  return verify_with_rlp(
30
  puzzle_type=spec.verifier_type,
31
  problem_ascii=problem_ascii,
32
  extracted_board=board_ascii,
33
  args=args,
34
  )
35
-
 
1
  from __future__ import annotations
2
 
3
+ import subprocess
4
  import sys
5
  from pathlib import Path
6
  from typing import Any
 
11
  if str(EVALS_SRC_DIR) not in sys.path:
12
  sys.path.insert(0, str(EVALS_SRC_DIR))
13
 
14
+ from evals_verifier import ( # type: ignore
15
+ check_undead_structural_validity,
16
+ verify_flow_free,
17
+ verify_with_rlp,
18
+ )
19
  from puzzles import PUZZLES # type: ignore
20
 
21
 
22
  class VerificationService:
23
+ def _verify_undead_with_timeout(
24
+ self,
25
+ *,
26
+ board_ascii: str,
27
+ args: str,
28
+ ) -> dict[str, Any]:
29
+ result = {
30
+ "board_exists": bool(board_ascii),
31
+ "board_valid": False,
32
+ "board_modified": False,
33
+ "correct": False,
34
+ }
35
+ if not board_ascii:
36
+ return result
37
+
38
+ result["board_valid"] = bool(check_undead_structural_validity(board_ascii))
39
+ if not result["board_valid"]:
40
+ result["board_modified"] = True
41
+ return result
42
+
43
+ verifier_path = ROOT_DIR / "submodules" / "rlp" / "verifier.py"
44
+ command = [sys.executable, str(verifier_path), "undead", "--arg", args]
45
+ try:
46
+ completed = subprocess.run(
47
+ command,
48
+ input=board_ascii,
49
+ text=True,
50
+ capture_output=True,
51
+ cwd=ROOT_DIR,
52
+ timeout=15,
53
+ check=False,
54
+ )
55
+ except subprocess.TimeoutExpired:
56
+ result["error"] = "Undead verification timed out."
57
+ return result
58
+
59
+ result["correct"] = completed.returncode == 0 and "SOLVED" in completed.stdout
60
+ if completed.stderr.strip():
61
+ result["stderr"] = completed.stderr.strip()[:500]
62
+ return result
63
+
64
  def verify(
65
  self,
66
  *,
 
72
  spec = PUZZLES[puzzle_type]
73
  if spec.verifier_type == "flow_free":
74
  return verify_flow_free(problem_ascii, board_ascii)
75
+ if spec.verifier_type == "undead":
76
+ return self._verify_undead_with_timeout(
77
+ board_ascii=board_ascii,
78
+ args=args,
79
+ )
80
  return verify_with_rlp(
81
  puzzle_type=spec.verifier_type,
82
  problem_ascii=problem_ascii,
83
  extracted_board=board_ascii,
84
  args=args,
85
  )