sudokyst is a Typst package for rendering Sudoku boards with optional candidate hints and highlight overlays.
It also includes a helper for computing legal candidate values for a cell from the current board state.
Rendering approach
The board is rendered as a native Typst table with:
- fixed-width columns and fixed-height rows so the board stays square
- thicker strokes on 3x3 block boundaries
- optional fills for highlighted rows, columns, 3x3 boxes, overlapping intersections, and explicit cells
- optional per-cell hint rendering as a nested
3 x 3Typstgrid
This keeps the package fully Typst-native and easy to style without generating SVG manually.
Example
#import "@preview/sudokyst:0.1.0": sudoku
#let board = (
(8, 0, 0, 0, 0, 0, 0, 0, 2),
(0, 4, 0, 7, 0, 1, 0, 5, 0),
(0, 0, 0, 0, 8, 0, 0, 0, 0),
(0, 7, 0, 4, 0, 5, 0, 2, 0),
(0, 0, 8, 0, 2, 0, 6, 0, 0),
(0, 2, 0, 6, 0, 8, 0, 1, 0),
(0, 0, 0, 0, 5, 0, 0, 0, 0),
(0, 9, 0, 8, 0, 4, 0, 7, 0),
(6, 0, 0, 0, 0, 0, 0, 0, 4),
)
#let hints = (
((), (1, 3, 5), (1, 3, 5, 7), (), (), (), (), (3, 4, 6, 9), ()),
((2, 3, 9), (), (2, 3, 6, 9), (), (3, 6, 9), (), (3, 8, 9), (), (3, 6, 8, 9)),
((1, 2, 3, 5, 7, 9), (1, 3, 5, 6), (1, 2, 3, 5, 6, 7, 9), (2, 3, 5, 9), (), (2, 3, 6, 9), (1, 3, 4, 7, 9), (3, 4, 6, 9), (1, 3, 6, 7, 9)),
((1, 3, 9), (), (1, 3, 6, 9), (), (3, 9), (), (3, 8, 9), (), (3, 8, 9)),
((1, 3, 4, 5, 9), (1, 3, 5), (), (1, 3, 5, 9), (), (3, 7, 9), (), (3, 4, 9), (3, 5, 7, 9)),
((1, 3, 4, 5, 9), (), (3, 4, 5, 9), (), (3, 7, 9), (), (3, 4, 5, 7, 9), (), (3, 5, 7, 9)),
((1, 2, 3, 4, 7), (1, 3, 8), (1, 2, 3, 4, 7), (1, 2, 3, 9), (), (2, 3, 6, 9), (1, 2, 3, 8, 9), (3, 6, 8, 9), (1, 3, 6, 8, 9)),
((1, 2, 3, 5), (), (1, 2, 3, 5), (), (1, 3, 6), (), (1, 2, 3, 5, 8), (), (1, 3, 5, 6, 8)),
((), (1, 3, 5, 8), (1, 2, 3, 5, 7), (1, 2, 3, 9), (1, 3, 7, 9), (2, 3, 7, 9), (1, 2, 3, 5, 8, 9), (3, 8, 9), ()),
)
#sudoku(
board: board,
hints: hints,
show-hints: true,
highlighted-rows: (2, 5),
highlighted-columns: (4,),
highlighted-boxes: (5,),
highlighted-cells: ((5, 5),),
)
Highlight selections are 1-based:
highlighted-rows: (1,)highlights the first rowhighlighted-columns: (9,)highlights the ninth columnhighlighted-boxes: (5,)highlights the center3 x 3boxhighlighted-cells: ((5, 5),)highlights the center cell
Boxes are numbered left-to-right and top-to-bottom:
(1, 2, 3)for the top band(4, 5, 6)for the middle band(7, 8, 9)for the bottom band
Empty cells should be represented with 0 or none.
Main options
board: a9 x 9array of valueshints: a9 x 9array whose cells contain candidate arrays like(1, 3, 9)show-hints: show the nested3 x 3candidate grid in empty cellshighlighted-rows,highlighted-columns,highlighted-boxes,highlighted-cells: optional 1-based highlight selectionscell-size: side length of each Sudoku cellthin-strokeandblock-stroke: cell border stylingvalue-colorandhint-color: text colorsrow-highlight-fill,column-highlight-fill,box-highlight-fill,overlap-highlight-fill,cell-highlight-fill: highlight colorsvalue-text-sizeandhint-text-size: optional text size overrides
Helper functions
available-values(board, row, column): returns the legal values for the given cell as an array like(1, 2, 4). Rows and columns are 1-based. If the cell is already filled, the function returns().valid-move(board, row, column, value): returnstruewhenvalueis a legal1..9placement for the given empty 1-based cell on the current board, otherwisefalse.first-single-position(board): returns the 1-based position(row, column)of the first cell, scanned top-to-bottom and left-to-right, whose available-values list has length1. Returnsnoneif there is no such cell.first-single-move(board): returns((row, column), value)for the first cell, scanned top-to-bottom and left-to-right, whose available-values list has length1. Returnsnoneif there is no such cell.propagate-step(board): applies onefirst-single-move(board)if available. Returnsnoneif there is no forced single, otherwise a record withboard,position,value, andmove.propagate(board): repeatedly appliesfirst-single-move(board)until no forced single remains. Returns a record withboard,positions, andmoves, wherepositionsis the ordered list of filled 1-based cells andmovesis the ordered list of((row, column), value)pairs.solve(board): solves the Sudoku with repeated propagation plus recursive backtracking. Returns a record withsolved,board,boards, andmove_groups.boardsis the ordered trace of board states after each propagation and each unforced guess, across the search tree until the first solution.move_groupsaligns withboards; a propagation contributes multiple moves, while an unforced guess contributes a one-element array. Very hard puzzles may still exceed Typst’s maximum function-call depth.generate-hints(board, positions): returns a full9 x 9hints array forsudoku(...).positionsis a list of 1-based(row, column)pairs. Selected empty cells receive computed candidates; all other cells receive().generate-hints-all(board): returns a full9 x 9hints array for every empty cell on the board. Filled cells receive().set-cell(board, row, column, value): returns a new board with the given 1-based cell updated.valuemay be0,none, or an integer from1to9.
Example:
#import "@preview/sudokyst:0.1.0": available-values, valid-move, first-single-position, first-single-move, propagate-step, propagate, solve, generate-hints, generate-hints-all, set-cell
#let candidates = available-values(board, 1, 3)
#let ok = valid-move(board, 1, 3, 4)
#let single = first-single-position(board)
#let single-move = first-single-move(board)
#let step = propagate-step(board)
#let propagation = propagate(board)
#let solution = solve(board)
#let hints = generate-hints(board, ((1, 3), (1, 4), (2, 2)))
#let all-hints = generate-hints-all(board)
#let next-board = set-cell(board, 1, 3, 4)