package hangman import ( "bufio" _ "embed" "errors" "math/rand/v2" "strings" "sync" ) // The bank. gogobee kept its phrases in a file it read at boot out of a path in // an env var, which meant the game was one missing file away from not existing. // Embedding it means the casino cannot start without its phrases, which is the // correct relationship between a game and the thing it is about. // //go:embed phrases.txt var phrasesTxt string // ErrNoPhrases means the bank has nothing at this length. It can only happen if // someone edits phrases.txt down past a tier, and it is a programming error // rather than anything a player did — but it's an error, not a panic, because a // casino that won't boot is worse than one game being shut. var ErrNoPhrases = errors.New("hangman: no phrases in that tier") var ( shelvesOnce sync.Once shelves map[string][]string // tier slug -> the phrases that fit it ) // load sorts the bank onto one shelf per tier, once. Comments and blank lines // are dropped, and so is anything too short to be a game — the tiers' own Min // is the floor. func load() { shelves = make(map[string][]string, len(Tiers)) sc := bufio.NewScanner(strings.NewReader(phrasesTxt)) for sc.Scan() { line := strings.TrimSpace(sc.Text()) if line == "" || strings.HasPrefix(line, "#") { continue } n := len([]rune(line)) for _, t := range Tiers { if n >= t.Min && n <= t.Max { shelves[t.Slug] = append(shelves[t.Slug], line) break } } } } // Shelf is how many phrases a tier has. Exists so a test can assert the bank // hasn't been edited out from under a tier. func Shelf(slug string) int { shelvesOnce.Do(load) return len(shelves[slug]) } // drawPhrase picks one phrase from a tier's shelf. The rng is threaded, never // the package global, so a game replays exactly from the seed in its audit row. func drawPhrase(t Tier, rng *rand.Rand) (string, error) { shelvesOnce.Do(load) shelf := shelves[t.Slug] if len(shelf) == 0 { return "", ErrNoPhrases } return shelf[rng.IntN(len(shelf))], nil }