mirror of
https://github.com/tiennm99/tiennm99bot.git
synced 2026-10-19 04:16:45 +00:00
chore: drop CF→AWS migration tooling and stale JS-port references
The CF→AWS data migration (closed 2026-05-16) is long done and the tooling isn't wired into any production path. Remove the one-shot binary, its support package, and the migration runbook. In live code, replace 'JS-parity' / 'same shape as JS' / 'cross-runtime KV migration' comments with the real, stable reason for each behavior (wire-format invariant, null-vs-zero distinction, CloudWatch alarm field name, etc.). 24 files touched across lolschedule, loldle, wordle, twentyq, trading, misc, util, server, metrics, ai, keylock. - delete cmd/migrate_cf_data/ - delete internal/migration/ - delete docs/cf-to-aws-migration-runbook.md
This commit is contained in:
1 parent
c901f3ad40
commit
ec77a24db7
47 files changed
+142
-1491
No files matched your search
@@ -1,4 +1,4 @@
|
||||
// Package loldle ports the JS loldle classic mode — guess the League
|
||||
// Package loldle implements the loldle classic mode — guess the League
|
||||
// champion from attribute hints (gender, species, regions, etc.).
|
||||
package loldle
|
||||
|
||||
@@ -9,8 +9,8 @@ import (
|
||||
)
|
||||
|
||||
// Champion is one row of champions.json. Field tags match loldle.net's
|
||||
// scraped schema verbatim so the embedded JSON file lifts unmodified from
|
||||
// the JS source.
|
||||
// scraped schema verbatim so the embedded JSON file can be regenerated from
|
||||
// the upstream scrape without a transform step.
|
||||
type Champion struct {
|
||||
ChampionName string `json:"championName"`
|
||||
Gender string `json:"gender"`
|
||||
@@ -22,9 +22,8 @@ type Champion struct {
|
||||
ReleaseDate string `json:"release_date"` // YYYY-MM-DD
|
||||
}
|
||||
|
||||
// rawChampions holds the embedded JSON byte stream. The data file is copied
|
||||
// byte-for-byte from src/modules/loldle/champions.json in the JS repo so
|
||||
// dictionaries are identical across runtimes (no normalization at port time).
|
||||
// rawChampions holds the embedded JSON byte stream — the loldle.net champion
|
||||
// dictionary scraped offline and checked into data/champions.json.
|
||||
//
|
||||
//go:embed data/champions.json
|
||||
var rawChampions []byte
|
||||
|
||||
@@ -4,8 +4,8 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// AttrType classifies how a single attribute is compared. Matches the JS
|
||||
// source's "exact" | "multi" | "year" discriminator.
|
||||
// AttrType classifies how a single attribute is compared:
|
||||
// "exact" | "multi" | "year".
|
||||
type AttrType string
|
||||
|
||||
const (
|
||||
@@ -14,16 +14,18 @@ const (
|
||||
attrYear AttrType = "year"
|
||||
)
|
||||
|
||||
// Result categories. Match JS strings byte-for-byte (handlers/render rely on
|
||||
// these literals via the marker maps).
|
||||
// Result categories. handlers/render rely on these literals via the marker
|
||||
// maps, so renaming a constant requires updating those maps in lockstep.
|
||||
const (
|
||||
ResultCorrect = "correct"
|
||||
ResultPartial = "partial"
|
||||
ResultWrong = "wrong"
|
||||
)
|
||||
|
||||
// AttributeRow describes one attribute's comparison output. JS shape:
|
||||
// {key, label, type, guessValue, targetValue, result, direction?}.
|
||||
// AttributeRow describes one attribute's comparison output as one row of
|
||||
// the render board: key/label identify the row, type drives the comparison
|
||||
// algorithm, result is the rendered marker, direction is set only for
|
||||
// wrong year-type rows ("up"/"down").
|
||||
type AttributeRow struct {
|
||||
Key string
|
||||
Label string
|
||||
@@ -35,8 +37,8 @@ type AttributeRow struct {
|
||||
}
|
||||
|
||||
// classicAttributes is the ordered comparison spec. Order matters for both
|
||||
// render output and the JS-port test that asserts `compareChampions(...)`
|
||||
// returns rows in this exact sequence.
|
||||
// render output and the test that asserts CompareChampions returns rows in
|
||||
// this exact sequence — reordering changes the on-screen board.
|
||||
var classicAttributes = []AttributeRow{
|
||||
{Key: "gender", Label: "Gender", Type: attrExact},
|
||||
{Key: "species", Label: "Species", Type: attrMulti},
|
||||
@@ -51,7 +53,7 @@ var classicAttributes = []AttributeRow{
|
||||
// Values are compared per attr.Type:
|
||||
// - exact: case-insensitive string equality.
|
||||
// - multi: set comparison; full match → correct, partial overlap → partial,
|
||||
// no overlap → wrong. Two empty sets are correct (matches JS).
|
||||
// no overlap → wrong. Two empty sets are correct.
|
||||
// - year: parses leading 4 digits; equal → correct, else wrong + a
|
||||
// direction hint ("up" if guess<target, "down" if guess>target).
|
||||
func CompareChampions(guess, target *Champion) []AttributeRow {
|
||||
@@ -126,8 +128,9 @@ func asStringSlice(v any) []string {
|
||||
case []string:
|
||||
return x
|
||||
case string:
|
||||
// JS toSet falls back to splitting on "," when the value isn't an array.
|
||||
// Champions.json never produces this branch but parity matters.
|
||||
// Defensive: champions.json never sends a comma-joined string for a
|
||||
// multi-valued attribute, but if a future data refresh ever does,
|
||||
// split on "," instead of treating it as a single token.
|
||||
if x == "" {
|
||||
return nil
|
||||
}
|
||||
@@ -138,7 +141,7 @@ func asStringSlice(v any) []string {
|
||||
|
||||
// compareMultiValue: full match (case-insensitive, order-independent) →
|
||||
// correct; any intersection → partial; otherwise wrong. Two empty sets
|
||||
// are correct (matches JS, e.g. for hypothetical "no positions" champs).
|
||||
// are correct (e.g. for a hypothetical "no positions" champion).
|
||||
func compareMultiValue(guess, target []string) string {
|
||||
g := toLowerSet(guess)
|
||||
t := toLowerSet(target)
|
||||
@@ -182,8 +185,7 @@ func setsEqual(a, b map[string]struct{}) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// parseYear extracts the first 4 digits of s. Returns 0 if absent/non-numeric
|
||||
// — JS regex parity (`^(\d{4})`).
|
||||
// parseYear extracts the first 4 digits of s. Returns 0 if absent/non-numeric.
|
||||
func parseYear(s string) int {
|
||||
if len(s) < 4 {
|
||||
return 0
|
||||
@@ -232,8 +234,8 @@ func compareYear(g, t int) (result, direction string) {
|
||||
return ResultWrong, "down"
|
||||
}
|
||||
|
||||
// formatValue mirrors JS's `formatValue`: empty → "—", array → comma-joined,
|
||||
// otherwise toString.
|
||||
// formatValue renders a value cell: empty → "—", array → comma-joined,
|
||||
// otherwise stringified.
|
||||
func formatValue(v any) string {
|
||||
switch x := v.(type) {
|
||||
case nil:
|
||||
|
||||
@@ -4,8 +4,8 @@ import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
// Fixtures lifted from tests/modules/loldle/compare.test.js so any future
|
||||
// drift between Go and JS scoring shows up as a test failure here.
|
||||
// Fixtures cover the compare-scoring rules. Drift in the scoring algorithm
|
||||
// or the canonical attribute order surfaces here as a test failure.
|
||||
|
||||
var aatrox = &Champion{
|
||||
ChampionName: "Aatrox",
|
||||
|
||||
@@ -2,9 +2,9 @@ package loldle
|
||||
|
||||
import "fmt"
|
||||
|
||||
// attemptFlavor returns a one-word reaction to a winning attempt. Branches
|
||||
// match the JS source exactly (1 → "First try!", 2 → "Sharp!", final →
|
||||
// "Phew — last one!", final-2 → "Close call!", else "Nice.").
|
||||
// attemptFlavor returns a one-word reaction to a winning attempt:
|
||||
// 1 → "First try!", 2 → "Sharp!", final → "Phew — last one!",
|
||||
// final-2 → "Close call!", else "Nice.".
|
||||
func attemptFlavor(attempt, max int) string {
|
||||
if attempt <= 1 {
|
||||
return "First try!"
|
||||
@@ -27,11 +27,11 @@ func attemptFlavor(attempt, max int) string {
|
||||
// < 60min → "3m 14s" (or "3m" when seconds == 0)
|
||||
// otherwise → "1h 12m" (or "1h" when remaining minutes == 0)
|
||||
//
|
||||
// JS parity. Negative inputs clamp to 0 (matches JS's Math.max(0, …)).
|
||||
// Negative inputs clamp to 0.
|
||||
func formatDuration(ms int64) string {
|
||||
total := ms / 1000
|
||||
if ms%1000 >= 500 {
|
||||
total++ // round-half-up to match JS Math.round
|
||||
total++ // round-half-up
|
||||
}
|
||||
if total < 0 {
|
||||
total = 0
|
||||
|
||||
@@ -13,7 +13,7 @@ func TestAttemptFlavor(t *testing.T) {
|
||||
6: "Close call!",
|
||||
7: "Close call!",
|
||||
8: "Phew — last one!",
|
||||
9: "Phew — last one!", // attempt > max — defensive; matches JS >=
|
||||
9: "Phew — last one!", // attempt > max — defensive (`>=` branch)
|
||||
}
|
||||
for attempt, want := range cases {
|
||||
if got := attemptFlavor(attempt, maxAttempts); got != want {
|
||||
@@ -28,7 +28,7 @@ func TestFormatDuration(t *testing.T) {
|
||||
want string
|
||||
}{
|
||||
{0, "0s"},
|
||||
{500, "1s"}, // round-half-up to JS Math.round
|
||||
{500, "1s"}, // round-half-up
|
||||
{499, "0s"},
|
||||
{42_000, "42s"},
|
||||
{60_000, "1m"},
|
||||
|
||||
@@ -37,7 +37,8 @@ func (s *state) findByName(name string) *Champion {
|
||||
|
||||
// rehydrateGuesses recomputes board rows from the stored championNames.
|
||||
// Champions removed from champions.json since the round started are skipped
|
||||
// (returns the surviving prefix), matching JS.
|
||||
// (returns the surviving prefix) so a data refresh never breaks an active
|
||||
// round.
|
||||
func (s *state) rehydrateGuesses(g *gameState) []boardEntry {
|
||||
target := s.findByName(g.Target)
|
||||
if target == nil {
|
||||
|
||||
@@ -3,9 +3,8 @@ package loldle
|
||||
import "strings"
|
||||
|
||||
// normalizeName folds a name to a comparable form: lowercase, alphanumeric
|
||||
// only. JS-parity with util/normalize-name.js — `String(s).toLowerCase().
|
||||
// replace(/[^a-z0-9]/g, "")`. Used for case/space/punctuation-insensitive
|
||||
// lookup so "Kai'Sa", "kaisa", and "KAI SA" all collapse to the same key.
|
||||
// only. Used for case/space/punctuation-insensitive lookup so "Kai'Sa",
|
||||
// "kaisa", and "KAI SA" all collapse to the same key.
|
||||
func normalizeName(s string) string {
|
||||
lower := strings.ToLower(s)
|
||||
out := make([]byte, 0, len(lower))
|
||||
|
||||
@@ -63,8 +63,8 @@ func buildRows(championName string, results []AttributeRow) []guessRow {
|
||||
}
|
||||
|
||||
// formatRowGroups joins one or more guess-row groups into a single <pre>
|
||||
// block. Label column width is the max label across ALL groups, so stacked
|
||||
// guesses on a board align with each other (JS parity).
|
||||
// block. Label column width is the max label across ALL groups so stacked
|
||||
// guesses on a board align with each other.
|
||||
func formatRowGroups(groups [][]guessRow) string {
|
||||
width := 0
|
||||
for _, g := range groups {
|
||||
|
||||
@@ -7,8 +7,7 @@ import (
|
||||
|
||||
// TestRenderGuess_AlignsLabelColumn locks the monospace column-alignment
|
||||
// invariant: the label column must be padded to the longest label so stacked
|
||||
// guesses on the board line up. Captures the JS render contract that the
|
||||
// rest of the test suite never exercises.
|
||||
// guesses on the board line up.
|
||||
func TestRenderGuess_AlignsLabelColumn(t *testing.T) {
|
||||
rows := []AttributeRow{
|
||||
{Key: "gender", Label: "Gender", Type: attrExact, GuessValue: "Male", Result: ResultCorrect},
|
||||
|
||||
@@ -15,11 +15,11 @@ const (
|
||||
MaxGuessesCap = 10
|
||||
)
|
||||
|
||||
// gameState is the per-subject KV record. Field tags match JS exactly so a
|
||||
// JS-written round decodes cleanly. StartedAt is *int64 because the JS
|
||||
// source initialises it to `null` (timer doesn't start until first guess) —
|
||||
// gameState is the per-subject KV record.
|
||||
//
|
||||
// StartedAt is *int64 because the timer doesn't start until the first guess;
|
||||
// using time.Time would marshal as "0001-01-01T00:00:00Z" instead of null
|
||||
// and break wire-format parity.
|
||||
// and lose that distinction.
|
||||
//
|
||||
// Guesses is just championNames; comparison rows are recomputed at render
|
||||
// time against current champions.json so a weekly data refresh updates
|
||||
@@ -30,8 +30,9 @@ type gameState struct {
|
||||
StartedAt *int64 `json:"startedAt"` // ms-since-epoch | null
|
||||
}
|
||||
|
||||
// stats lifetime score. JS shape — note no LastResultAt field (differs from
|
||||
// wordle's stats; the JS loldle source omits it, parity dictates we do too).
|
||||
// stats lifetime score. No LastResultAt field by design (differs from
|
||||
// wordle's Stats — loldle only ever needed running streaks, not "last
|
||||
// played at").
|
||||
type stats struct {
|
||||
Played int `json:"played"`
|
||||
Wins int `json:"wins"`
|
||||
@@ -80,8 +81,8 @@ func clearGame(ctx context.Context, kv storage.KVStore, subject string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// loadStats returns lifetime score; missing → fresh-zero record (matches
|
||||
// the JS `?? {…}` fallback).
|
||||
// loadStats returns lifetime score; missing → fresh-zero record so callers
|
||||
// never need a nil check.
|
||||
func loadStats(ctx context.Context, kv storage.KVStore, subject string) (*stats, error) {
|
||||
var s stats
|
||||
err := kv.GetJSON(ctx, statsKey(subject), &s)
|
||||
@@ -118,7 +119,7 @@ func recordResult(ctx context.Context, kv storage.KVStore, subject string, won b
|
||||
|
||||
// getMaxGuesses returns the effective round length: the per-subject override
|
||||
// if set and in range, otherwise MaxGuesses. Out-of-range values are
|
||||
// silently ignored (matches JS).
|
||||
// silently ignored — better to serve the default than 500 the user.
|
||||
func getMaxGuesses(ctx context.Context, kv storage.KVStore, subject string) (int, error) {
|
||||
var cfg roundConfig
|
||||
err := kv.GetJSON(ctx, configKey(subject), &cfg)
|
||||
|
||||
@@ -31,7 +31,9 @@ func TestGameState_StartedAtAsNumber(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestStats_NoLastResultAtField(t *testing.T) {
|
||||
// JS loldle stats schema differs from wordle: no lastResultAt. Lock that.
|
||||
// loldle's stats schema deliberately differs from wordle's — no
|
||||
// lastResultAt field. Lock that, since adding the field would silently
|
||||
// change the on-disk shape for every existing player.
|
||||
b, _ := json.Marshal(stats{})
|
||||
want := `{"played":0,"wins":0,"streak":0,"bestStreak":0}`
|
||||
if string(b) != want {
|
||||
|
||||
Reference in new issue
Block a user