Commit Graph
71 Commits
Author SHA1 Message Date
tiennm99 b459cc9ee7 refactor(semantle): swap word2sim backend for ConceptNet
ConceptNet provides a free public /relatedness endpoint (returns cosine-like
[-1, 1]) and /c/en/{term} for vocabulary check. No random-word endpoint, so
we ship a curated local target pool in wordlist.js (~250 words) and verify
each pick via the concept endpoint with a fallback to an unverified pick.

Each guess now makes two parallel ConceptNet calls (concept + relatedness)
instead of a single word2sim call. Slightly higher latency but zero hosting
cost and no dependency on the self-hosted word2sim instance.

- api-client.js rewritten; UpstreamError replaces Word2SimError (aliased
  for backwards compat with older imports).
- wordlist.js added (curated target pool + pickFromPool).
- handlers.js: drops RANDOM_FILTERS (no filtering needed; pool is curated).
- index.js: drops WORD2SIM_API_URL env var; ConceptNet base hardcoded.
- wrangler.toml + .dev.vars.example: drop WORD2SIM_API_URL.
- api-client tests rewritten for ConceptNet shape; total tests 336 → 341.
2026-04-22 23:07:54 +07:00
tiennm99 52c50528a7 refactor(semantle): drop /semantle_new; reply on duplicate guesses
Giveup already auto-starts a fresh round on next /semantle, so /semantle_new
was redundant. Duplicate guesses now match loldle's behavior: reply with
"🔁 already guessed" and skip the similarity API call (fast-path dedup
against prior word or canonical, with a post-API fallback for different
inputs that canonicalize to the same token).
2026-04-22 22:20:47 +07:00
tiennm99 e3fcc2e9c8 feat(semantle): add word2vec guessing game module
Telegram commands /semantle, /semantle_new, /semantle_giveup, /semantle_stats.
Round starts with /random pick from hosted word2sim; each guess scored via
/similarity. Unlimited guesses; solve on case-insensitive exact match.

New env var WORD2SIM_API_URL (wrangler.toml, .env.deploy). Includes
module README and 90 unit tests covering api-client, state, format,
render, and handlers.
2026-04-22 22:05:27 +07:00
tiennm99 ba829d2906 fix(loldle): start round timer on first guess, not round end
Previously startFreshGame was called at the tail of every win/lose/giveup
path, stamping startedAt to that moment — so the clock accrued while the
player was away between rounds. Now:

- round-ending paths call clearGame (new helper in state.js), deleting the
  KV record instead of pre-creating the next round
- getOrInitGame lazily creates the next round on the player's next /loldle
  call, with startedAt: null
- the first actual guess inside handleLoldle stamps startedAt = Date.now()

Viewing an empty board gives no hints, so it shouldn't count against the
clock. handleGiveup no longer auto-creates a fresh round and now reports
"No active round" when called with nothing in progress.
2026-04-22 17:00:31 +07:00
tiennm99 cedd9da4ac chore: run biome format across the repo
Broaden `npm run format` / `npm run lint` to biome's full scan (`.`)
instead of a fixed src/tests/scripts list, so root-level files and any
new top-level directories stay formatted. Drop the stale ignore entry
for the deleted champions-data.js.
2026-04-22 14:14:29 +07:00
tiennm99 173d1dbb18 fix(loldle): recover newer champions skipped by the scraper
loldle.net's classic-mode bundle has two record shapes — older champions
carry _id/championId, newer ones (Bel'Veth, K'Sante, Nilah, …) don't.
The regex required those leading fields, silently dropping anyone added
since 2022.

Make _id/championId optional and non-capturing, and drop them from the
output record (the bot never read them anyway). Champion count:
169 → 172; guessing /loldle k'sante, /loldle bel'veth, /loldle nilah
now resolve correctly.
2026-04-22 14:07:11 +07:00
tiennm99 1cb5215a01 refactor(loldle): align display labels with loldle.net grid
Column headers now match loldle.net's classic-mode grid verbatim:
Range → Range type, Region → Region(s), Lane → Position(s),
Year → Release year. The champion row header becomes Champion (was
Name). Data field names already matched; only labels diverged.
2026-04-22 13:54:23 +07:00
tiennm99 57832c3a6c refactor(loldle): trim module to current behavior only
KV payload cleanup:
- drop lastResultAt from stats (never read)
- drop solved/giveup flags from game state (round is immediately
  replaced after finish, making the flags transient noise)
- skip redundant saveGame on winning/giveup/out-of-guesses paths;
  startFreshGame overwrites anyway

Code cleanup:
- delete daily.js + daily.test.js (pickDaily/todayUtc were speculative
  "future use" — only pickRandom was wired in, inlined into handlers)
- drop the dead switch default in compare.js
- trim file preambles across the module

Docs: rewrite README around current behavior with loldle.net as the
sole data source; update scraper header to match the raw schema.
2026-04-22 13:49:38 +07:00
tiennm99 f1ea74b0c5 refactor(loldle): store only championNames in KV, recompute rows on render
Round state now keeps `guesses` as a plain string[] (the names the player
tried) instead of caching full comparison results. The board view
rehydrates rows at display time by re-running compareChampions against
the current target.

Smaller KV payloads, and the rendered board always reflects the live
champions.json — useful if a weekly data refresh lands mid-round.
2026-04-22 13:33:46 +07:00
tiennm99 aa11847acb refactor(loldle): consume loldle.net's raw schema directly
Drop the in-scraper normalization step — champions.json now mirrors the
exact shape emitted by loldle.net's JS bundle. Records use _id,
championId, championName, arrays for positions/species/regions/
range_type, "Male"/"Female"/"Other" gender strings, and a full
YYYY-MM-DD release_date.

Comparison is schema-aware: multi-value keys accept arrays directly,
the year axis parses YYYY out of the ISO date, and exact compares stay
case-insensitive.
2026-04-22 13:29:55 +07:00
tiennm99 b794c25a45 refactor(loldle): import champions.json directly, drop ESM wrapper
Node 24 + wrangler 4.x both accept `import ... with { type: "json" }`,
so the generated champions-data.js wrapper is no longer needed.

Drop scripts/build-loldle-data.js and the build:loldle-data npm script.
Scraper writes champions.json only.
2026-04-22 13:24:24 +07:00
tiennm99 470130195e refactor(loldle): source all champion data from loldle.net
loldle.net's JS bundle ships the complete set of classic-mode axes in
plaintext, so ddragon merging is no longer needed. Scraper now produces
the final schema directly.

Schema changes: drop title, skinCount, image, and genre (ddragon-only).
Replace genre (class tags like Fighter/Mage) with species (Human/Darkin/
Vastayan) — the axis loldle.net actually uses. Promote region to a
multi-value field so multi-region champions compare correctly.

Handlers no longer show "Name — Title" on win/giveup.
2026-04-22 13:19:10 +07:00
tiennm99 8dd4a4617f chore(loldle): refresh champion data from loldle.net
First regeneration via scripts/scrape-loldle-data.js. Corrects
multi-region champions (Aatrox runeterra,shurima; Ambessa
piltover,noxus; Ashe noxus,runeterra), fixes lane/region accuracy,
and canonicalizes region slugs (mount-targon -> targon).
2026-04-22 13:14:23 +07:00
tiennm99 4cb5cf341a feat(loldle): weekly scraper for champion data from loldle.net
Pulls canonical game fields (gender, positions, range_type, regions,
release_date) from loldle.net's JS bundle and merges with ddragon
championFull for display metadata (title, resource, genre, skinCount,
image). Opens a PR weekly via GitHub Actions (Mon 06:00 UTC).
2026-04-22 13:14:12 +07:00
tiennm99 da79408542 feat(loldle): reject duplicate guesses within a round
Prevents two players in a group from wasting a slot on the same champion —
re-guessing a previously tried champion replies with a hint and does not
consume a guess.
2026-04-22 09:35:22 +07:00
tiennm99 d03b1859a5 feat(loldle): sticker reactions + per-attempt flavor + solve time
- Send a random sticker from WIN/LOSE/GIVEUP pools before the text reply
  (errors swallowed so a rotten file_id never blocks the message).
- Win message includes attempt-count flavor ("First try!" / "Sharp!" /
  "Close call!" / "Phew — last one!") and elapsed solve time.
- Lose and giveup messages now also include the champion's title.
- Extract stickers.js and flavor.js so handlers.js stays at ~200 LoC.
2026-04-22 00:00:50 +07:00
tiennm99 06a93c6c60 feat(util): add private /stickerid to capture bot-scoped sticker file_ids
Reply to any sticker with /stickerid to get its file_id and
file_unique_id. Collects IDs for hard-coded sticker pools in other
modules (upcoming loldle win/lose/giveup reactions).

Private visibility keeps the command out of /help and the Telegram
slash menu.
2026-04-21 23:48:27 +07:00
tiennm99 679bf8ec6f fix(loldle): escape <champion> in HTML replies so Telegram accepts them
Literal <champion> inside an HTML parse_mode reply made Telegram reject
the message as an unknown entity, so the victory / out-of-guesses /
giveup text never reached the user — only the silently-started fresh
round was visible on the next /loldle.
2026-04-21 23:12:30 +07:00
tiennm99 7997c8eb5b feat(loldle): monospace aligned board, auto-restart round
- Remove /loldle_new; finished rounds (solve/giveup/out-of-guesses)
  immediately roll into a fresh round.
- Render guesses as an HTML <pre> monospace table with auto-widthed
  label column and a 🎯 Name row (uppercase champion name).
- Year direction uses ⬆️ / ⬇️.
2026-04-21 23:03:49 +07:00
tiennm99andGitHub bd266c0385 Merge pull request #2 from tiennm99/dependabot/npm_and_yarn/multi-4223e67014
build(deps): bump undici and wrangler
2026-04-21 22:14:47 +07:00
tiennm99 73f36901d5 feat(help): append support footer with repo star link 2026-04-21 22:12:33 +07:00
tiennm99andGitHub 76efc52f1d Merge pull request #10 from tiennm99/chore/drop-preview-kv
chore(wrangler): drop preview KV namespace
2026-04-21 14:39:55 +07:00
tiennm99andGitHub 9132ffad98 Merge pull request #11 from tiennm99/fix/trading-kbs-endpoint
fix(trading): swap dead TCBS endpoint for KBS data_day
2026-04-21 14:39:30 +07:00
tiennm99 841960b324 fix(trading): swap dead TCBS endpoint for KBS data_day
The TCBS `apipubaws.tcbs.com.vn` host returns HTTP 404/500 for every
request, so every ticker resolved as "Unknown stock ticker" and /trade_buy
was unusable. Switch price + symbol resolution to the KBS public endpoint
that vnstock currently defaults to (`kbbuddywts.kbsec.com.vn/iis-server/
investment/stocks/{TICKER}/data_day`). KBS needs no auth, returns JSON,
and is Worker-compatible.

- `prices.fetchStockPrice` now queries KBS with a 14-day lookback window
  (covers weekends/holidays) and drops the TCBS-specific ×1000 scaling;
  KBS returns real VND.
- `symbols.resolveSymbol` delegates to `fetchStockPrice` for existence
  checks — empty `data_day` means unknown ticker.
- Update test fetch stubs to match the `kbsec` host and KBS response
  shape (`{ symbol, data_day: [{ c }] }`).
2026-04-21 14:39:05 +07:00
tiennm99 77d456381a fix(trading): swap dead TCBS endpoint for KBS data_day
The TCBS `apipubaws.tcbs.com.vn` host returns HTTP 404/500 for every
request, so every ticker resolved as "Unknown stock ticker" and /trade_buy
was unusable. Switch price + symbol resolution to the KBS public endpoint
that vnstock currently defaults to (`kbbuddywts.kbsec.com.vn/iis-server/
investment/stocks/{TICKER}/data_day`). KBS needs no auth, returns JSON,
and is Worker-compatible.

- `prices.fetchStockPrice` now queries KBS with a 14-day lookback window
  (covers weekends/holidays) and drops the TCBS-specific ×1000 scaling;
  KBS returns real VND.
- `symbols.resolveSymbol` delegates to `fetchStockPrice` for existence
  checks — empty `data_day` means unknown ticker.
- Update test fetch stubs to match the `kbsec` host and KBS response
  shape (`{ symbol, data_day: [{ c }] }`).
2026-04-21 13:33:08 +07:00
tiennm99 522963ab7f chore(wrangler): drop preview KV namespace — production-only project
No preview deploy stage is used, so the preview_id binding (and its
companion create comment) just advertises a namespace that never sees
traffic. Keep wrangler.toml narrowly scoped to what we actually deploy.
2026-04-21 10:49:30 +07:00
tiennm99 3dc015102c fix(lolschedule): group week by league → day instead of day → league
Users expect to scan a single region's upcoming week rather than a daily
fixture board. Reverses the nesting: /lolschedule_week now lists each
major league as a top-level section, then italicised ICT date headers
with that league's matches beneath.
2026-04-21 10:44:35 +07:00
tiennm99 f46cd59e2c refactor(lolschedule): post-subscribe review — docs and stale option
- handlers.js header now reflects fan-out to subscribers, not a single chat.
- README "Time zone" references the correct command names and gains a
  Subscribers section; Files section lists subscribers.js.
- formatEventLine's showLeague option is dead in production (renderToday
  and renderWeek always group under a league header), so drop it and the
  test that covered only the option toggle.
2026-04-21 10:38:59 +07:00
tiennm99 58e8995968 feat(lolschedule): per-chat subscribe/unsubscribe for the daily cron
Replaces the single LOLSCHEDULE_CHAT_ID env var with a KV-backed
subscriber list. New commands /lolschedule_subscribe and
/lolschedule_unsubscribe let each chat opt in/out. The cron now fans
out to every subscriber via Promise.allSettled so one blocked chat
cannot break the others.
2026-04-21 10:38:59 +07:00
tiennm99 3c5bfb6910 feat(lolschedule): group by league, rename commands, add daily push cron
- Commands renamed: /lol_today → /lolschedule_today, /lol_week → /lolschedule_week.
- Today view groups events under a league header per section.
- Week view nests leagues under each ICT day.
- LEAGUE_ORDER gives tier-1 tournaments priority (Worlds / MSI / First Stand,
  then LCK / LPL / LEC / LCS, etc).
- New cron "0 1 * * *" (08:00 ICT) pushes today's major-league schedule to
  LOLSCHEDULE_CHAT_ID via the Telegram Bot API. Skips cleanly when chat id
  or token is missing, or when today has no major-league matches.
2026-04-21 10:38:59 +07:00
tiennm99 cc5eb2d473 refactor(lolschedule): drop stale Leaguepedia references and unused params
Module header and api-client top comment still mentioned Leaguepedia /
MatchSchedule / keying by league filter. fetchSchedulePage also exposed
an unused `leagueId` parameter and returned an unused `olderToken`;
remove both to match the actual usage.
2026-04-21 10:11:49 +07:00
tiennm99 86180ef02b refactor(lolschedule): swap Leaguepedia for lolesports.com esports-api
Leaguepedia's anonymous IP rate limit is too aggressive for a bot even
from CF Worker egress (~1–2 req/min), and authenticated Fandom tokens
don't lift it. Switching to the lolesports.com getSchedule endpoint —
the same data feed powering the official site — removes the limit and
provides richer fields: state (unstarted/inProgress/completed), per-team
result.gameWins and outcome, league metadata, bestOf strategy.

Handlers simplify back to cache-first (120 s fresh / 1 h stale fallback)
with no cron needed. Results are filtered to major leagues (LCK, LPL,
LEC, LCS, worlds, msi, first_stand, LCP, CBLOL, EMEA Masters) to keep
the week view under Telegram's 4096-char message limit.
2026-04-21 10:11:49 +07:00
tiennm99 ef710e1477 fix(lolschedule): structured error logging for cargoquery failures
Swaps the best-effort console.warn for JSON log lines emitted via
console.log so Workers Observability + wrangler tail surface the real
cause (HTTP status, API error info, or non-JSON body) when /lol_today
and /lol_week fall into the error branch.
2026-04-21 09:54:02 +07:00
tiennm99 80b6c1a70e docs(plans): add Leaguepedia API verification reports
Captures the feasibility check and auth-token investigation that led to
the lolschedule module: confirmed endpoint + table + field set, and
documented why caching beats token-based rate-limit mitigation on Fandom.
2026-04-21 09:36:39 +07:00
tiennm99 9287f6629a feat(lolschedule): add LoL esports match schedule module
New module exposing /lol_today and /lol_week commands, backed by the
Leaguepedia Cargo API (MatchSchedule table). Renders scores for
played/live matches and ICT times for scheduled ones. Caches range
queries in KV (60s today, 300s week) with stale-fallback on fetch error.
2026-04-21 09:36:39 +07:00
tiennm99 141d4cf764 fix(wordle): drop column alignment — use NYT share format (word above colors)
Cross-client column alignment between the marker row and a letter row is
unreliable in Telegram:
- <pre> monospace doesn't enforce equal width for emoji
- fullwidth Latin (U+FF21..FF3A) falls back to base Latin on mobile fonts
- squared-letter emoji (U+1F130/1F170..) render at different intrinsic
  widths than color-square emoji (U+1F7E8/1F7E9/2B1C) on many clients

Instead, render each guess as the word on one line followed by the
colored marker row — the standard NYT Wordle share format:

  CRANE
  🟩🟨🟩🟩

The association between a letter and its color is visually unambiguous
without depending on character-column alignment. Also drops the HTML
parse_mode requirement — replies are plain text again.
2026-04-20 22:52:40 +07:00
tiennm99 c86227faf9 fix(wordle): render letters as emoji-class characters for exact alignment
Previous fullwidth-Latin approach (U+FF21..FF3A) failed on mobile
Telegram clients because their monospace fonts don't ship fullwidth
glyphs — the codepoints fall back to base Latin at 1 cell, making the
letter row half as wide as the marker row.

Switch to Negative Squared Latin Capital Letters (U+1F170..1F189,
🅰🅱🅲..🆉) — these are emoji-class characters, so both rows are
drawn by the same emoji font at the same cell width. Column alignment
becomes a property of Unicode, not of the client's monospace font.

  🟩🟨🟩🟩
  🅲🆁🅰🅽🅴
2026-04-20 22:45:49 +07:00
tiennm99 0c3eaf3885 fix(wordle): align markers with letters using fullwidth Latin
Color-square emoji (🟩🟨) render at ~2 monospace cells wide in
Telegram's <pre> blocks; the previous letter row used " X " (3 cells
per letter = 15 cells total for a 5-letter word) against markers that
only span ~10 cells, so columns drifted.

Switching letters to fullwidth Latin (U+FF21..U+FF3A, e.g. 'A' instead
of 'A') puts each letter at exactly 2 cells via East Asian Width =
Fullwidth, matching one emoji per letter with no padding:

  🟩🟨🟩🟩
  CRANE

No spacing heuristics — alignment is a consequence of character width,
which every monospace-capable Telegram client respects.
2026-04-20 22:37:51 +07:00
tiennm99 c9de4b7c04 feat(wordle): render board in monospace so markers align with letters
Wrap renderGuess / renderBoard output in <pre> and send replies with
parse_mode: HTML. In Telegram's monospace font each " X " letter cell
is 3 characters wide, which is roughly the width of a single emoji
marker, so colored squares stack cleanly over the letter they score.

No user-controlled content lands inside the <pre> (guesses are
validated [a-z]{5}), so no HTML escaping is needed in the grid. The
inline <code>/wordle &lt;word&gt;</code> placeholder is properly
entity-encoded for HTML parse mode.
2026-04-20 22:30:17 +07:00
dependabot[bot]andGitHub 1287861100 build(deps): bump undici and wrangler
Bumps [undici](https://github.com/nodejs/undici) to 7.24.4 and updates ancestor dependency [wrangler](https://github.com/cloudflare/workers-sdk/tree/HEAD/packages/wrangler). These dependencies need to be updated together.


Updates `undici` from 5.29.0 to 7.24.4
- [Release notes](https://github.com/nodejs/undici/releases)
- [Commits](https://github.com/nodejs/undici/compare/v5.29.0...v7.24.4)

Updates `wrangler` from 3.114.17 to 4.81.1
- [Release notes](https://github.com/cloudflare/workers-sdk/releases)
- [Commits](https://github.com/cloudflare/workers-sdk/commits/wrangler@4.81.1/packages/wrangler)

---
updated-dependencies:
- dependency-name: undici
  dependency-version: 7.24.4
  dependency-type: indirect
- dependency-name: wrangler
  dependency-version: 4.81.1
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-20 15:10:46 +00:00
tiennm99 bd270a6835 fix: project-wide review — trading safety, loldle drift guard, doc refresh
Code fixes:
- trading/handlers + stats-handler: guard ctx.from?.id to prevent
  cross-user state corruption when channel posts or inline queries lack
  a sender
- trading/prices + trading/symbols: encodeURIComponent on ticker before
  interpolating into TCBS API URLs
- trading/stats-handler: parallelize per-stock price fetches with
  Promise.allSettled so N-stock portfolios don't stack serial latency
- loldle/handlers: guard target champion lookup against champions.json
  refresh drift — start a fresh round or fall back to the stored id
- wordle + loldle: explicitly initialize giveup:false in startFreshGame
  for stable state shape
- wordle/lookup: fix stale JSDoc that claimed null return
- biome: ignore auto-generated champions.json / champions-data.js /
  words-data.js
- Apply formatter to src/index.js, loldle/handlers.js imports, and
  loldle/compare.test.js (previously red)

Docs refresh:
- README: 105+ tests -> 200+; wordle/loldle described as real modules
- architecture: module tree updated, test count 105 -> 200, runtime
  ~500ms -> ~2s, stub list narrowed to misc only
- codebase-summary: module table rewritten (wordle/loldle now Complete
  with real command lists and KV schema); test coverage table updated
- loldle/README: full rewrite matching the current implementation
  (was describing the original stub)
- New docs/development-roadmap.md tracking upcoming features
  (daily-mode for wordle + loldle, crypto/gold/forex trading, shared
  picker util, handler-level tests, coverage reporting, staging env)

Tests: 200/200 passing. Lint: clean.
2026-04-20 22:08:58 +07:00
tiennm99 574d855806 feat(wordle): port classic 5-letter guessing game
Replaces the wordle stub with a full implementation mirroring the loldle
module layout: compare/lookup/daily/render/state/handlers/index split,
per-subject KV state, standard 6 guesses, two-pass duplicate-letter
marking.

Commands: /wordle, /wordle_new, /wordle_giveup, /wordle_stats.

Word list (14,855 entries) sourced from dracos's gist
(https://gist.github.com/dracos/dd0668f281e685bad51479e5acaadb93) and
bundled via scripts/build-wordle-data.js. Credits in module README and
generated file headers.

Dispatcher test updated for the new command count (12 → 13).
2026-04-20 22:08:58 +07:00
github-actions[bot] b68b38feca chore(data): sync champions from loldle-data@7a9d4f6 2026-04-20 14:31:23 +00:00
github-actions[bot] 0717161da0 chore(data): sync champions from loldle-data@b01967f 2026-04-20 14:19:07 +00:00
tiennm99 e79339fc1f chore(ci): remove sync workflow; loldle-data now pushes directly 2026-04-20 18:33:20 +07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>tiennm99
b0a5c613a1 chore(deps): bump vite and vitest (#1)
* build(deps): bump vite and vitest

Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) to 8.0.8 and updates ancestor dependency [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest). These dependencies need to be updated together.


Updates `vite` from 5.4.21 to 8.0.8
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v8.0.8/packages/vite)

Updates `vitest` from 2.1.9 to 4.1.4
- [Release notes](https://github.com/vitest-dev/vitest/releases)
- [Commits](https://github.com/vitest-dev/vitest/commits/v4.1.4/packages/vitest)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 8.0.8
  dependency-type: indirect
- dependency-name: vitest
  dependency-version: 4.1.4
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>

* feat(loldle): share game state per-chat in groups

Groups and supergroups now share one daily puzzle + one stats counter
across all members. Private chats remain per-user.

- state.js: renamed key arg from userId to subject (user|chat id)
- handlers.js: getSubject(ctx) picks user id in DM, chat id in groups
- /loldle_stats labels scope as "your" vs "group" accordingly

* feat(loldle): add /loldle_new + switch to self-paced rounds

- /loldle_new starts a new random round. If the previous round is not
  solved/given-up, it's recorded as a loss (auto-giveup) before rerolling.
- Drop daily-seeded targets: each round picks a uniformly-random champion
  (pickRandom in daily.js; pickDaily kept for future use).
- state.js: one active round per subject (no date in key). TTL raised to
  7 days; streak = consecutive wins (round-based, not date-based).
- Register /loldle_new in module index; now 8 public loldle commands.
- Tests: add pickRandom cases; bump expected command count to 12.

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: tiennm99 <tiennm99@outlook.com>
2026-04-20 17:55:40 +07:00
tiennm99 12345e9aba feat(obs): enable Workers Observability (logs + traces)
- wrangler.toml: [observability] block with logs + traces at
  head_sampling_rate=1 (200k free events/day on free plan).
- src/index.js: structured request log emitted on every fetch
  (method/path/status/ms) — JSON shape so Observability's filter UI
  can index individual fields.
- Refactored fetch body into a route() helper so the log line sees the
  final response status.
2026-04-20 17:36:16 +07:00
tiennm99 2c201a4fc3 build: add umbrella 'build' script chaining per-module builds 2026-04-20 17:27:18 +07:00
tiennm99 64d37e603e feat(loldle): port classic-mode game from loldle repo
Adds loldle module with classic-mode champion guessing. Ports comparison
logic from tiennm99/loldle (lib/classic-mode.js) and bundles champion
data from tiennm99/loldle-data. Adds GH Actions workflow that re-syncs
champions.json on cross-repo dispatch from loldle-data.

- Three public commands: /loldle, /loldle_giveup, /loldle_stats
- Per-user daily state + streak stats in KV (3-day TTL on games)
- champions-data.js wrapper sidesteps Node 24 / esbuild disagreement on
  JSON import attributes; generator script + npm run build:loldle-data
- register script now tolerates missing .env.deploy (env-file-if-exists)
  so Workers Builds can inject env vars directly
- fix(scripts): escape stray */ in migrate.js docstring that broke node
- 16 new unit tests (compare, daily, lookup); dispatcher test updated
  for the new command set
2026-04-20 17:10:08 +07:00
tiennm99 3b16e9fb25 chore: remove completed plans and reports 2026-04-15 13:30:44 +07:00