Files
miti99bot/internal/modules/wordle/compare.go
T
tiennm99 15a3239ab1 refactor(storage): replace KVStore with generic typed DocStore[T]
Delete the byte-oriented KVStore/VersionedStore abstraction and the
DynamoDB/memory KV backends. Add a generic typed store (DocStore[T] with
Provider/Collection/Typed) persisting each value as a flattened native
Mongo document (storedDoc[T] via bson inline) — no value envelope.

- MongoDB is the only runtime backend; memory kept for tests/local.
- All modules + deploynotify use typed stores; persisted structs carry
  bson tags == json names (incl. nested lolschedule/wordle types).
- lolschedule wraps its array/scalar values in named structs.
- migrate-dynamo-to-mongo writes the flattened shape via Typed[bson.M]
  with wrap rules; Scan/--dry-run/--verify retained.

Verified: go vet/build clean; full go test green hermetically and
in-container vs real Mongo 7 + DynamoDB Local (storage integration +
migrator e2e).
2026-06-28 18:02:11 +07:00

78 lines
2.4 KiB
Go

// Package wordle implements the classic 5-letter word-guess game, scored
// letter-by-letter green/yellow/grey.
package wordle
// WordLength is wordle's fixed 5. Exposed so render.go and tests can reuse it
// without magic numbers.
const WordLength = 5
// LetterResult labels a single guessed letter's state. Values are part of
// the stored game's JSON shape: "correct" | "partial" | "wrong".
const (
ResultCorrect = "correct"
ResultPartial = "partial"
ResultWrong = "wrong"
)
// LetterScore is the shape stored per guess (nested in GameState). bson tags
// mirror the json names so migrated docs (which keep the original JSON keys)
// read back verbatim.
type LetterScore struct {
Letter string `json:"letter" bson:"letter"`
Result string `json:"result" bson:"result"`
}
// CompareWords scores guess against target letter-by-letter. Both are assumed
// lowercase a-z and exactly WordLength long; callers validate via
// validateGuess before reaching here.
//
// Two-pass marking is required to handle duplicate letters correctly:
// - pass 1: positional matches → "correct"; consume those slots from the
// target's available pool.
// - pass 2: remaining guess letters → "partial" if still in the pool (and
// consume), else "wrong".
//
// Example: target "abbey", guess "babes" →
//
// b@0 partial, a@1 partial, b@2 correct, e@3 correct, s@4 wrong.
func CompareWords(guess, target string) []LetterScore {
out := make([]LetterScore, WordLength)
pool := make([]byte, 0, WordLength)
// Pass 1 — positional matches.
for i := 0; i < WordLength; i++ {
if guess[i] == target[i] {
out[i] = LetterScore{Letter: string(guess[i]), Result: ResultCorrect}
} else {
pool = append(pool, target[i])
}
}
// Pass 2 — partial matches against the remaining-pool, with consumption.
for i := 0; i < WordLength; i++ {
if out[i].Result == ResultCorrect {
continue
}
idx := indexOfByte(pool, guess[i])
if idx >= 0 {
pool = append(pool[:idx], pool[idx+1:]...)
out[i] = LetterScore{Letter: string(guess[i]), Result: ResultPartial}
} else {
out[i] = LetterScore{Letter: string(guess[i]), Result: ResultWrong}
}
}
return out
}
// indexOfByte returns the first index of c in s, or -1.
// (bytes.IndexByte gives the same answer; inlined to keep this file
// dependency-free since the scoring algorithm is the whole point.)
func indexOfByte(s []byte, c byte) int {
for i, b := range s {
if b == c {
return i
}
}
return -1
}