InternBootcamp/internbootcamp/libs/calcudoku/calcudoku_validor.py
2025-05-23 15:27:15 +08:00

207 lines
No EOL
8.3 KiB
Python
Executable file
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import re
from collections import defaultdict
from itertools import permutations
def validate_calcudoku_puzzle(puzzle_spec, solution):
"""
Given:
puzzle_spec: list of strings, each string has n group labels or clues separated by whitespace.
E.g., "J+12 F/4 5 2 E*3 E".
solution: 2D list of ints (n×n), e.g. [[6,4,5,2,1,3], [5,1,4,3,2,6], ...]
Returns: (bool, str)
- bool indicating whether the solution is fully valid or not.
- str is "" if valid, or an explanation of the first error found.
Steps to validate:
1) Parse puzzle spec to find:
- The puzzle size n
- group_label -> list of cells
- group_label -> (op, target), if any
2) Check solution dimension matches puzzle dimension.
3) Check each row, each column for distinct 1..n.
4) For each group, check if the group's constraint is satisfied:
op "+" => sum of group cells == target
op "*" => product of group cells == target
op "-" => for some permutation p of group cells, p[0] - p[1] - ... = target
op "/" => for some permutation p of group cells, p[0] / p[1] / ... = target (within an epsilon for float)
"""
# ---------------------------------------------------------------------------
# Step 1: parse puzzle spec
# ---------------------------------------------------------------------------
n = len(puzzle_spec)
# basic check: each row in puzzle_spec should have n tokens
puzzle_grid = []
for r, line in enumerate(puzzle_spec):
tokens = line.strip().split()
if len(tokens) != n:
return False, f"Row {r} of puzzle_spec has {len(tokens)} cells, expected {n}"
puzzle_grid.append(tokens)
# group_label -> dict with {"cells": [...], "op": None or str, "target": None or int}
groups = defaultdict(lambda: {"cells": [], "op": None, "target": None})
# We'll read each cell's label or clue.
# If it includes operation+target (like "J+12"), we store that in the group info.
for r in range(n):
for c in range(n):
cell_str = puzzle_grid[r][c]
# We assume the puzzle format:
# either "X" (just a label)
# or "X+12", "X-3", "X*60", "X/4", etc.
# or possibly "XY+12" if label has multiple letters
# We'll parse label, operation, and target.
match = re.match(r"^([A-Za-z]+)([+\-\*/])(\d+)$", cell_str)
if match:
g_label = match.group(1)
op = match.group(2)
tgt = int(match.group(3))
groups[g_label]["op"] = op
groups[g_label]["target"] = tgt
groups[g_label]["cells"].append((r, c))
else:
# just a label
g_label = cell_str
groups[g_label]["cells"].append((r, c))
# gather group data in a simpler structure
group_list = []
for g_label, info in groups.items():
op = info["op"]
target = info["target"]
cells = info["cells"]
group_list.append((g_label, op, target, cells))
# ---------------------------------------------------------------------------
# 2) Check solution dimension
# ---------------------------------------------------------------------------
if len(solution) != n:
return False, f"Solution row count {len(solution)} != puzzle dimension {n}"
for r in range(n):
if len(solution[r]) != n:
return False, f"Solution row {r} has {len(solution[r])} cols, expected {n}"
# ---------------------------------------------------------------------------
# 3) Check row & column distinctness
# Each row/col must contain numbers 1..n exactly once
# ---------------------------------------------------------------------------
# Check row distinctness
for r in range(n):
row_vals = solution[r]
if len(set(row_vals)) != n:
return False, f"Row {r} has repeated values: {row_vals}"
# optionally check they are exactly 1..n
for val in row_vals:
if val < 1 or val > n:
return False, f"Row {r} has out-of-range value {val}"
# Check column distinctness
for c in range(n):
col_vals = [solution[r][c] for r in range(n)]
if len(set(col_vals)) != n:
return False, f"Column {c} has repeated values: {col_vals}"
for val in col_vals:
if val < 1 or val > n:
return False, f"Column {c} has out-of-range value {val}"
# ---------------------------------------------------------------------------
# 4) Check group operation constraints
# ---------------------------------------------------------------------------
for g_label, op, target, cells in group_list:
# Gather the solution values for these cells
vals = [solution[r][c] for (r, c) in cells]
# If the group has no op/target (like single-cell group sometimes?),
# then it's typically the puzzle format that it does have an op & target
# on at least one cell. For a single-cell group, one possibility is "A+5" or similar.
# If we truly have no op, we'll just skip or treat it as passing automatically.
if op is None or target is None:
# If the group is single cell, we can interpret that as the target = that cell's value with op = '+'
# or we can just skip. Here, let's skip. Or we can check that if group has 1 cell, the value is that cell.
if len(cells) == 1:
# It's presumably correct.
# If you need a stricter logic, uncomment:
# if vals[0] != target: return False, f"Single-cell group {g_label} mismatch"
pass
continue
if op == '+':
if sum(vals) != target:
return False, f"Group {g_label} sum {sum(vals)} != target {target}"
elif op == '*':
prod = 1
for v in vals:
prod *= v
if prod != target:
return False, f"Group {g_label} product {prod} != target {target}"
elif op == '-':
# We look for any permutation p of vals s.t. p[0] - p[1] - ... = target
found = False
for perm in permutations(vals):
result = perm[0]
for x in perm[1:]:
result -= x
if result == target:
found = True
break
if not found:
return False, f"Group {g_label} cannot satisfy difference = {target} with values {vals}"
elif op == '/':
# We look for any permutation p of vals s.t. p[0] / p[1] / ... = target
found = False
for perm in permutations(vals):
result = float(perm[0])
ok = True
for x in perm[1:]:
# check dividing by zero not possible here if x>0
# but just in case
if x == 0:
ok = False
break
result /= float(x)
# check if close
if ok and abs(result - target) < 1e-9:
found = True
break
if not found:
return False, f"Group {g_label} cannot satisfy ratio = {target} with values {vals}"
else:
return False, f"Group {g_label} has unsupported op {op}"
# If we reach here, everything passed
return True, ""
if __name__ == "__main__":
# Example puzzle (the same puzzle_spec might be from a generator).
puzzle_spec_example = [
"P+4 O+1 T+5 G-2 E+3 R+5",
"A-2 Q/3 Q G F+5 R",
"A A H+2 N+8 N K+4",
"L-1 B*48 B N C+6 S+1",
"L J+10 B I*10 I I",
"J J D+8 D D M+6"
]
# Suppose n=4 here. A= group 1, B= group 2, etc.
# This puzzle_spec is purely an illustrative example, likely incomplete as a real puzzle.
# And an example solution that we want to check:
solution_example = [
[4, 1, 5, 6, 3, 2],
[1 ,2, 6, 4, 5, 3],
[3, 6, 2, 5, 1, 4],
[5, 4, 3, 2, 6, 1],
[6, 3, 4, 1, 2, 5],
[2, 5, 1, 3, 4, 6],
]
is_valid, reason = validate_calcudoku_puzzle(puzzle_spec_example, solution_example)
if is_valid:
print("Solution is valid!")
else:
print("Solution is invalid:", reason)