feat(wordledaily): add the daily Wordle HTML5 game

/wordledaily sends a BotFather game card whose page plays one shared word
per day, changing at 07:00 ICT. The server checks guesses, saves each
player's board after every guess and keeps NYT-style stats. A win is
reported with setGameScore, and in a group one live results message shows
everyone's colour grids.

/wordledaily_subscribe and /wordledaily_unsubscribe opt a chat or topic in
to the 07:00 push: yesterday's recap, opening with the answer and the
group's win streak, then today's card.

The HTML5 game plumbing that noitu used is now shared in util/htmlgame, and
the wordle word list and scoring live in wordle/wordlist.
This commit is contained in:
tiennm99 committed 2026-10-09 17:20:02 +07:00
1 parent 871b4a1042
commit c0231ceadf
56 files changed
+8013 -306

No files matched your search

+4
View File
@@ -107,6 +107,10 @@ func TestCommandDiscovery_AllPublicCommandsHaveSafeMetadata(t *testing.T) {
"tth": "[target...]",
"wheelofnames": "<option,...>",
"wordle": "[word]",
"wordledaily": "",
"wordledaily_subscribe": "",
"wordledaily_unsubscribe": "",
}
menu := botCommandMenu(reg)
+2
View File
@@ -42,6 +42,7 @@ import (
"github.com/tiennm99/tiennm99bot/internal/modules/util"
"github.com/tiennm99/tiennm99bot/internal/modules/weather"
"github.com/tiennm99/tiennm99bot/internal/modules/wordle"
"github.com/tiennm99/tiennm99bot/internal/modules/wordledaily"
"github.com/tiennm99/tiennm99bot/internal/server"
"github.com/tiennm99/tiennm99bot/internal/storage"
"github.com/tiennm99/tiennm99bot/internal/systemstate"
@@ -101,6 +102,7 @@ func factories() map[string]modules.Factory {
"amlich": amlich.New,
"monkeyd": monkeyd.New,
"wordle": wordle.New,
wordledaily.ShortName: wordledaily.New,
noitu.ShortName: noitu.New,
"loldle": loldle.New,
lol.CollectionName: lol.New,
+24
View File
@@ -122,6 +122,30 @@ func TestFactoriesRegistersNoituDisabledByDefault(t *testing.T) {
}
}
// wordledaily is in the catalog and, with GAME_BASE_URL unset, registers its
// commands and game but no public HTTP route and no daily push.
func TestFactoriesRegistersWordleDailyDisabledByDefault(t *testing.T) {
t.Setenv("GAME_BASE_URL", "")
t.Setenv("WORDLEDAILY_GAME_SECRET", "")
reg, err := modules.Build([]string{"wordledaily"}, factories(), storage.NewMemoryProvider(), modules.BuildOptions{})
if err != nil {
t.Fatalf("Build wordledaily: %v", err)
}
for _, name := range []string{"wordledaily", "wordledaily_subscribe", "wordledaily_unsubscribe"} {
if _, ok := reg.AllCommands[name]; !ok {
t.Fatalf("missing command %s", name)
}
}
if routes := serverRoutes(reg); len(routes) != 0 {
t.Fatalf("disabled game exposes routes: %+v", routes)
}
for _, m := range reg.Modules {
if len(m.Crons) != 0 {
t.Fatalf("disabled game registers crons: %+v", m.Crons)
}
}
}
// An empty MODULES loads every module, so a command name that collides with an
// existing module surfaces here as a test failure rather than as a startup
// crash on deploy.