Files
tiennm99 629b513c91 feat(games): move /wordle and /loldle to the web games and add /loldledaily
/wordle and /loldle now send the wordle and loldle BotFather games in an
unlimited mode: each player gets their own word or champion, saved after
every guess, with a New game button. Cards the bot did not record as
unlimited, including the daily cards, play the daily puzzle. The helper
commands keep working on the player's round.

/loldledaily mirrors /wordledaily: one champion per day at 07:00 ICT,
subscribe and unsubscribe, the morning recap and the live group results.

The daily and unlimited machinery is shared in util/guessgame, the old
in-chat wordle module is removed, and a one-time startup migration carries
each player's stats and unfinished round over without changing old data.
2026-10-10 09:29:48 +07:00

67 lines
1.9 KiB
Go

package loldle
import (
"context"
"errors"
"fmt"
"github.com/tiennm99/tiennm99bot/internal/storage"
)
// MaxGuesses is the default unlimited round length and the fixed daily one.
// /loldle_setmax overrides the unlimited length per chat, up to
// MaxGuessesCap.
const (
MaxGuesses = 8
MaxGuessesCap = 10
)
// roundConfig stores a chat's unlimited round length. Stored only when
// /loldle_setmax has been run; the absence of this record means "use
// default". The subject is the chat id in a group and the user id in a
// private chat.
type roundConfig struct {
MaxGuesses int `json:"maxGuesses" bson:"maxGuesses"`
}
// ConfigStore is the loldle module's typed config store.
type ConfigStore = storage.DocStore[roundConfig]
func configKey(subject string) string { return "config:" + subject }
func validMaxGuesses(n int) bool {
return n >= 1 && n <= MaxGuessesCap
}
func normalizeMaxGuesses(n int) int {
if validMaxGuesses(n) {
return n
}
return MaxGuesses
}
// getMaxGuesses returns the effective round length: the per-subject override
// if set and in range, otherwise MaxGuesses. Out-of-range values are
// silently ignored — better to serve the default than 500 the user.
func getMaxGuesses(ctx context.Context, cfg ConfigStore, subject string) (int, error) {
c, _, err := cfg.Get(ctx, configKey(subject))
if err != nil {
if errors.Is(err, storage.ErrNotFound) {
return MaxGuesses, nil
}
return 0, fmt.Errorf("loldle getMaxGuesses: %w", err)
}
return normalizeMaxGuesses(c.MaxGuesses), nil
}
// setMaxGuesses validates and persists the per-subject override.
func setMaxGuesses(ctx context.Context, cfg ConfigStore, subject string, n int) error {
if !validMaxGuesses(n) {
return fmt.Errorf("loldle: maxGuesses must be in [1, %d], got %d", MaxGuessesCap, n)
}
if err := cfg.Put(ctx, configKey(subject), roundConfig{MaxGuesses: n}); err != nil {
return fmt.Errorf("loldle setMaxGuesses: %w", err)
}
return nil
}