Skip to content

Python API

solve()

The main entry point for solving puzzles programmatically.

from larsdoku_zs import solve

Signature

solve(puzzle, max_level=99, no_oracle=False, detail=False, gf2_extended=False)

Parameters

Parameter Type Default Description
puzzle str required 81-char puzzle string (0 or . for empty)
max_level int 99 Max technique level (1-7)
no_oracle bool False Pure logic only — stop if stalled
detail bool False Capture rich detail (candidates, explanations)
gf2_extended bool False Use GF(2) Extended with probing + conjugates

Returns

A dictionary with the following keys:

{
    'success': bool,           # True if puzzle fully solved
    'stalled': bool,           # True if engine stalled (with no_oracle=True)
    'board': str,              # Current board state (81 chars)
    'solution': str,           # Backtrack solution for verification
    'n_steps': int,            # Number of placement steps
    'steps': list,             # List of step dictionaries
    'technique_counts': dict,  # {technique_name: count}
    'empty_remaining': int,    # Unsolved cells (0 if success)
    'rounds': int,             # Solver rounds
    'elim_events': list,       # Elimination events (if detail=True)
}

Step Dictionary

Each entry in steps looks like:

{
    'step': 42,                # step number
    'pos': 35,                 # cell position (0-80)
    'digit': 7,                # placed digit
    'technique': 'crossHatch', # technique used
    'cell': 'R4C9',            # human-readable cell name
    'round': 5,                # which round this happened in
}

With detail=True, steps also include:

{
    'cands_before': [3, 7, 9],  # candidates before placement
    'explanation': '...',        # human-readable explanation
}

Examples

Basic solve:

from larsdoku_zs import solve

result = solve("4...3.......6..8..........1....5..9..8....6...7.2........1.27..5.3....4.9........")
print(f"Solved in {result['n_steps']} steps")

Pure logic with technique analysis:

result = solve(puzzle, no_oracle=True)

if result['success']:
    for tech, count in sorted(result['technique_counts'].items(), key=lambda x: -x[1]):
        print(f"  {tech}: {count}")
else:
    print(f"Stalled with {result['empty_remaining']} cells remaining")

Detailed solve with elimination tracking:

result = solve(puzzle, detail=True, no_oracle=True)

for step in result['steps']:
    print(f"  #{step['step']:3d}  {step['cell']}={step['digit']}  [{step['technique']}]")
    if 'explanation' in step:
        print(f"         {step['explanation']}")

Batch processing:

from larsdoku_zs import solve
from larsdoku_zs.puzzles import TOP1465

pure = 0
for puzzle in TOP1465:
    result = solve(puzzle, no_oracle=True)
    if result['success']:
        pure += 1

print(f"Pure logic: {pure}/{len(TOP1465)} ({100*pure/len(TOP1465):.1f}%)")

Puzzle Collections

from larsdoku_zs.puzzles import FAMOUS_10, EXPERT_669, TOP1465

FAMOUS_10

List of tuples: (name, author, year, puzzle_string)

from larsdoku_zs.puzzles import FAMOUS_10

for name, author, year, puzzle in FAMOUS_10:
    print(f"{name} by {author} ({year})")

EXPERT_669

List of 81-character puzzle strings. 669 expert-level puzzles, box-shuffled.

from larsdoku_zs.puzzles import EXPERT_669
print(f"{len(EXPERT_669)} puzzles")

TOP1465

List of 81-character puzzle strings (using . for empty cells). The canonical Stertenbrink/dukuso benchmark.

from larsdoku_zs.puzzles import TOP1465
print(f"{len(TOP1465)} puzzles")

Web API (--serve)

When running larsdoku --serve, the engine exposes a REST API at POST /api/solve.

Solve Request

{
  "puzzle": "800000000003600000070090200...",
  "autotrust": true,
  "level": 7,
  "no_oracle": false,
  "gf2": false,
  "gf2x": false,
  "preset": "expert",
  "only": null,
  "exclude": null,
  "cell": null,
  "path": false
}

All fields except puzzle are optional.

Field Type Default Description
puzzle str required 81-char puzzle string
autotrust bool true Trust backtrack solution (enables DeepResonance)
level int 99 Max technique level (1-7)
no_oracle bool false Pure logic only
gf2 bool false Enable GF(2) Block Lanczos
gf2x bool false Enable GF(2) Extended (implies gf2)
preset str null "expert" or "wsrf"
only str null Comma-separated technique list
exclude str null Comma-separated exclusion list
cell str null Cell query mode (e.g., "R3C5")
path bool false Include technique path (with cell)

Solve Response

{
  "success": true,
  "steps": [
    {"step": 1, "pos": 14, "digit": 3, "technique": "crossHatch", "cell": "R2C6", "round": 1}
  ],
  "technique_counts": {"crossHatch": 42, "nakedSingle": 9},
  "elapsed_ms": 18.3,
  "solution": "812753649943682175...",
  "stalled": false,
  "empty_remaining": 0,
  "n_steps": 55,
  "elim_events": [
    {"round": 1, "technique": "SimpleColoring", "detail": "Simple Coloring: 5 eliminations", "count": 5}
  ]
}

Cell Query Response

When cell is provided, the API calls query_cell() instead:

{
  "cell": "R3C5",
  "answer": 9,
  "technique": "nakedSingle",
  "step": 48,
  "reachable": true,
  "candidates": [2, 9],
  "path": [{"step": 1, "pos": 14, "digit": 3, "technique": "crossHatch", "cell": "R2C6", "round": 1}],
  "elim_events": [...],
  "solve_status": "solved",
  "path_technique_counts": {"crossHatch": 22, "nakedSingle": 14},
  "elapsed_ms": 526.2,
  "message": "R3C5 = 9 via nakedSingle (step 48)"
}

Board Class

The Board class provides a higher-level API with zone intelligence, SIRO predictions, and cascade analysis.

from larsdoku_zs import Board

Board.solve_cascade()

Cascade solver: classifies each step as bottleneck (L3+ technique) or cascade (L1-L2). Shows how the puzzle avalanches from a few hard moves.

b = Board("980700600700000090006050000...")
result = b.solve_cascade()

print(f"Bottleneck depth: {result['bottleneck_depth']}")
print(f"Cascade placements: {result['cascade_count']}")
print(f"Ratio: 1:{result['cascade_count'] // max(1, result['bottleneck_depth'])}")

for m in result['bottleneck_moves']:
    print(f"  {m['cell']}={m['digit']} via {m['technique']}")

On the 50 hardest puzzles, average bottleneck depth is 2.8 — just 3 hard moves, everything else cascades through singles.

Board.siro_solve()

Hybrid solver: techniques crack bottlenecks, SIRO predicts the rest.

b = Board("980700600700000090006050000...")
result = b.siro_solve()

print(f"Predictions: {result['correct']}/{result['total_predictions']} ({result['accuracy']:.1%})")
print(f"Propagated: {result['propagated']} cells")

Board.scandalous_exocet()

Post-solve validated Exocet scan. Solves with pure logic first, then checks if any Exocet patterns on the original board are valid.

b = Board("980700600750000040003080070...")
results = b.scandalous_exocet(preset='larstech')

for r in results:
    print(f"{r['detail']}")
    print(f"Valid: {r['valid']}")

Board.forge_permute()

Constellation Forge: generate unique puzzles via digit permutation. Takes a unique puzzle, permutes digits 1-9, returns up to 362,880 unique puzzles.

puzzles = Board.forge_permute(
    "000060010000300007000001300007000080020400006100005900003050060800009500040200091",
    count=10
)
for p in puzzles:
    print(p)

Every output puzzle is guaranteed unique. The mask structure is preserved — only digit labels change.

Board.siro_predict()

Run SIRO cross-digit prediction on the current board state.

b = Board("980700600...")
preds = b.siro_predict()
for pos, info in preds.items():
    print(f"R{pos//9+1}C{pos%9+1}: predict {info['digit']} (confidence {info['confidence']})")

Board.compute_zones()

Compute WSRF zone likely map for the current board.

b = Board("980700600...")
zones = b.compute_zones()

Engine Access

For advanced usage, you can access the bitwise engine directly:

from larsdoku_zs.engine import BitBoard, propagate_l1l2, solve_backtrack

# Create a board
bb = BitBoard.from_string("4...3.......6..8..........1....")

# Run L1+L2 propagation
placements = propagate_l1l2(bb)
print(f"L1+L2 placed {len(placements)} digits, {bb.empty} remaining")

# Get backtrack solution (for verification)
solution = solve_backtrack("4...3.......6..8..........1....")

Warning

The engine API is lower-level and may change between versions. Use solve() for stable usage.