Skip to content

Getting Started

Installation

pip install larsdoku-zs

Requirements

  • Python >= 3.9
  • NumPy >= 1.22
  • Numba >= 0.56 (provides JIT compilation for the hot path)

Your First Solve

IPython / Interactive Session

from larsdoku_zs import Board

# Solve a forum hardest puzzle (SE 11.0+)
b = Board("98.7..6..75..4......3..8.7.8....9.5..3.2..1.....4....6.7...4.3....8..4......1...2")
print(b)

result = b.solve()
print(f"Solved: {result['success']}")
print(f"Steps:  {result['n_steps']}")

# See which advanced techniques were used
L1 = {'crossHatch', 'nakedSingle', 'fullHouse', 'lastRemaining'}
for tech, count in sorted(result['technique_counts'].items(), key=lambda x: -x[1]):
    if tech not in L1:
        print(f"  {tech}: {count}")

Output:

Solved: True
Steps:  58
  DeepResonance: 2
  SimpleColoring: 2
  ALS_XZ: 7
  XCycle: 1
  XWing: 2

Solve Without DeepResonance (FPF Path)

from larsdoku_zs.solver import solve_selective

# Same puzzle, different path — FPF takes over
result = solve_selective(
    "98.7..6..75..4......3..8.7.8....9.5..3.2..1.....4....6.7...4.3....8..4......1...2".replace('.','0'),
    exclude_techniques={'DeepResonance'}
)
print(f"Solved: {result['success']}")  # True — FPF handles it
# FPF fires once, then standard techniques cascade to completion

GF(2) Linear Algebra

from larsdoku_zs import Board

b = Board("98.7..6..75..4......3..8.7.8....9.5..3.2..1.....4....6.7...4.3....8..4......1...2")
p, e, dof = b.gf2()
print(f"Placements: {len(p)}, Eliminations: {len(e)}, DoF: {dof}")

# With residual info — which digits are fully determined by algebra?
from larsdoku_zs.gf2_xor import detect_gf2_xor
from larsdoku_zs.engine import BitBoard
bb = BitBoard.from_string(b.puzzle)
p, e, dof, free_cells, skip_digits = detect_gf2_xor(bb, return_residual=True)
det = [d+1 for d in range(9) if skip_digits & (1 << d)]
print(f"Digits fully determined by GF(2): {det}")

Batch Solve Forum Hardest

from larsdoku_zs.solver import solve_selective

with open('puzzles5_forum_hardest_1905_11+.txt') as f:
    puzzles = [l.strip() for l in f if len(l.strip()) == 81]

solved = 0
for p in puzzles:
    r = solve_selective(p.replace('.', '0'))
    if r['success']:
        solved += 1

print(f"{solved}/{len(puzzles)} solved ({solved/len(puzzles)*100:.1f}%)")
# 48765/48765 solved (100.0%)

Command Line

# Basic solve with board output
larsdoku "4...3.......6..8..........1....5..9..8....6...7.2........1.27..5.3....4.9........" --board

# Watch the engine think step-by-step
larsdoku "100007090030020008009600500005300900010080002600004000300000010040000007007000300" --steps

# Detailed round-by-round trace with candidates
larsdoku "100000002090400050006000700050903000000070000000850040700000600030009080002000001" --detail --board

Puzzle Format

Puzzles are 81 characters long, representing the board row by row, left to right, top to bottom.

  • Use 0 or . for empty cells
  • Digits 1-9 for given clues
Row 1: positions  1-9
Row 2: positions 10-18
...
Row 9: positions 73-81

Both formats are equivalent:

003000600900700010080005020600010900200807003004090005020500060010003002005000300
..3...6..9..7...1..8...5.2.6...1.9..2..8.7..3..4.9...5.2.5...6..1...3..2..5...3..

Try the Famous Puzzles

from larsdoku_zs import solve
from larsdoku_zs.puzzles import FAMOUS_10

for name, author, year, puzzle in FAMOUS_10:
    result = solve(puzzle, no_oracle=True)
    status = "SOLVED" if result['success'] else "STALLED"
    print(f"{name:30s} ({author}, {year})  {status}  {result['n_steps']} steps")

Run Benchmarks

# All collections
larsdoku-bench

# Just the Top1465
larsdoku-bench --collection top1465

# Quick test (first 50)
larsdoku-bench --collection top1465 --limit 50