Files
tiennm99 6f30d13b2d feat(noitu): add the noitu HTML5 Telegram game
Serve the BotFather game noitu from the bot's HTTP server under
/games/noitu when GAME_BASE_URL is set. /noitu sends the game, Play
answers with a signed, expiring link, and the server validates every
word against the embedded noitu dictionary (CC BY-SA 4.0), runs the
bot opponent and turn timer, and reports the score with setGameScore.

Modules can now register game-short-name callbacks and HTTP routes.
2026-10-09 14:32:57 +07:00

47 lines
1.8 KiB
Go

// Package dict is the nối từ word list: the embedded Wiktionary-derived
// corpus, the normalization player input goes through, the spelling variants
// a lookup accepts, and the word graph the engine and the bot opponent walk.
//
// Ported from tiennm99/noitu: Normalize from server/internal/vietnamese,
// the variant generator from server/cmd/build-dictionary/aliases.go, and the
// store from server/internal/dictionary (minus SQLite — the corpus is parsed
// into maps once, in memory).
package dict
import (
"errors"
"strings"
"golang.org/x/text/unicode/norm"
)
// MinSyllables is the shortest word the game accepts. Nối từ is played with
// compound words; a single syllable is not a valid move.
const MinSyllables = 2
// ErrEmpty is returned when input contains no usable characters.
var ErrEmpty = errors.New("dict: empty input")
// Normalize canonicalizes raw player or corpus input and splits it into
// syllables: NFC composition (so a base letter plus combining marks equals the
// precomposed form), lowercase, and whitespace collapsed to single spaces.
// Tone marks are kept exactly as typed; alternative tone placements are
// handled by Store.Resolve, not here.
func Normalize(raw string) (string, []string, error) {
composed := norm.NFC.String(raw)
// Composed again after lowering: case-folding can enable a composition that
// only exists for the lowercase form, so the lowered string may no longer
// be NFC.
lowered := norm.NFC.String(strings.ToLower(composed))
syllables := strings.Fields(lowered)
if len(syllables) == 0 {
return "", nil, ErrEmpty
}
return strings.Join(syllables, " "), syllables, nil
}
// HasEnoughSyllables reports whether a word is long enough to be a legal move.
func HasEnoughSyllables(syllables []string) bool {
return len(syllables) >= MinSyllables
}