Built with millirec

Every game on digitizer.fun is played by writing — sudoku, crosswords, wordle, hangman, chess, quizzes — and every letter and digit is read in the browser by the same recognizers you can put on your own page. Here is what each game uses, with the code.

npm install millirec

Capital letters in boxes

The word games share one writing surface: a box per letter, all read by one copy of char-cnn (98.5% right on held-out capital letters). Scramble passes only, so a box is read as one of the letters its word can hold. Wordle and hangman never say what they read: they use the three readings and their probabilities, so an unclear letter counts for less.

import { MillirecCharCnn } from 'millirec/char-cnn'

// the 'accurate' option
const rec = await MillirecCharCnn.create()
rec.attach(box)   // or recognize() your own strokes

// [{ char: 'R', prob: 0.91 }, …]
const top3 = await rec.recognize(strokes)
// one of the letters this box can hold
const fit = await rec.recognize(strokes,
  { only: 'CRANE' })

Digits, only the ones the puzzle allows

Sudoku and KenKen read digits with char-lite's shipped digits set (99.2% right on held-out digits). A 6×6 grid only takes 1–6, so any reading outside the grid's range is dropped: a misread 7 comes back as unread, not as a wrong number in the square.

import { MillirecCharLite, loadSet } from 'millirec'

const training = await loadSet('digits')
const rec =
  await MillirecCharLite.create({ training })

const allowed = new Set('123456')   // a 6×6 sudoku
const read = async (strokes) =>
  ((await rec.recognize(strokes)) || [])
    .filter((r) => allowed.has(r.char))

Signs the player invents

In emoji code and sudoku's emoji mode, the player draws a sign for each emoji, and that emoji becomes a class name: char-lite learns any string from a couple of drawings. Shipped default signs make every emoji playable before anything is drawn; the player's own take over as they add them.

// one class per emoji, from the player's drawings
const sample = (d) =>
  MillirecCharLite.strokesToSample(d)
const training = Object.fromEntries(signs.map(
  ({ emoji, drawings }) =>
    [emoji, drawings.map(sample)]))

const rec =
  await MillirecCharLite.create({ training })
// [{ char: '🍀', prob: … }, …]
await rec.recognize(strokes)

Learning one player's hand

After a game, the player can keep the letters and digits they wrote as samples. Letters refit char-cnn's last layer on them, anchored to the shipped model, so a few samples nudge a letter and many take it over. Digits are added to the digits set, and replace the shipped ones once there are three.

// letters: refit the CNN on this hand
await letters.personalize(
  { A: [a1, a2, a3], R: [r1, r2] })

// digits: their own 7s replace the shipped
const mine = MillirecCharLite.withSamples(digitsSet,
  { 7: [s1, s2, s3] }, { replace: true })
const digits =
  await MillirecCharLite.create({ training: mine })

Whole words in one box

The quizzes and the chess board use the handwriting input: letters written one on top of another in a single box, read against a word list (97.8% of words right on held-out capital letters). A quiz hands it the answers. Chess rebuilds the list from the legal moves after every move, so "Nf3" only reads as a move that can be played; the chess position builder reads one piece letter per square with char-lite.

<script src="https://cdn.jsdelivr.net/npm/millirec@0.1/dist/overlap/millirec-overlap.min.js"></script>

const box = await MillirecOverlap.create(el,
  { set: 'capitals', words: quiz.answers })
// chess: only what can be written now
box.setWords(legalMoves(position))

Your own

All of this is the millirec package — MIT licensed, no dependencies, nothing sent anywhere. Start with the ready-made capital letters, make a recognizer for your own symbols, or try the handwriting input.