mirror of
https://github.com/tiennm99/noitu.git
synced 2026-10-20 02:18:08 +00:00
feat(game): a score a player can see the cause of, and a dead end they can claim
pointsFor now returns the named terms behind a word's total alongside the total itself, trimmed from the trailing term when the maxPointsPerWord cap bites, so PlayedWord.parts always sums exactly to points. ClaimDeadEnd lets the player to act say a syllable has no answer instead of waiting out the clock; the room checks HasLegalMove and settles it exactly as a timeout would, or refuses with the clock untouched. dictionary.Store gains a diacritic-stripped index built at Open, so MoveRejected.suggestion can offer the one real word a typo differs from by tone marks alone. ReportWord is validated and rate-limited on the session, logged with the room's own context, and answered with WordReported.
This commit is contained in:
1 parent
865222d2cd
commit
16fd37978a
12 files changed
+905
-13
No files matched your search
@@ -127,6 +127,85 @@ func TestEndReasonMappingIsExhaustive(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func enginePointKinds() []game.PointKind {
|
||||
out := make([]game.PointKind, 0, game.NumPointKinds)
|
||||
for i := game.PointKind(0); i < game.NumPointKinds; i++ {
|
||||
out = append(out, i)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// TestPointKindMappingIsExhaustive mirrors the reject-reason walk. Only
|
||||
// PointKindNone is allowed to land on UNSPECIFIED: a PointPart exists only for
|
||||
// a term that contributed, so no real move ever carries that value.
|
||||
func TestPointKindMappingIsExhaustive(t *testing.T) {
|
||||
kinds := enginePointKinds()
|
||||
if len(kinds) < 2 {
|
||||
t.Fatalf("enumerated %d engine point kinds, expected the full set", len(kinds))
|
||||
}
|
||||
|
||||
seen := make(map[noituv1.PointKind]game.PointKind, len(kinds))
|
||||
for _, k := range kinds {
|
||||
got := PointKind(k)
|
||||
if k == game.PointKindNone {
|
||||
if got != noituv1.PointKind_POINT_KIND_UNSPECIFIED {
|
||||
t.Errorf("PointKindNone should map to UNSPECIFIED, got %v", got)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if got == noituv1.PointKind_POINT_KIND_UNSPECIFIED {
|
||||
t.Errorf("game.PointKind(%d) %q has no wire mapping", int(k), k)
|
||||
continue
|
||||
}
|
||||
if prev, dup := seen[got]; dup {
|
||||
t.Errorf("%v is produced by both %q and %q", got, prev, k)
|
||||
}
|
||||
seen[got] = k
|
||||
}
|
||||
|
||||
for _, v := range enumValues(noituv1.PointKind_POINT_KIND_UNSPECIFIED.Descriptor()) {
|
||||
w := noituv1.PointKind(v)
|
||||
if w == noituv1.PointKind_POINT_KIND_UNSPECIFIED {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[w]; !ok {
|
||||
t.Errorf("wire value %v is unreachable: no engine point kind maps to it", w)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestPlayedWordCarriesItsScoreBreakdown guards the invariant the client
|
||||
// relies on: the parts it draws beside the total must sum to it exactly.
|
||||
func TestPlayedWordCarriesItsScoreBreakdown(t *testing.T) {
|
||||
m := game.Move{
|
||||
Player: "p1",
|
||||
Word: "bình yên",
|
||||
Points: 25,
|
||||
Parts: []game.PointPart{
|
||||
{Kind: game.PointKindBase, Value: 10},
|
||||
{Kind: game.PointKindSpeed, Value: 15},
|
||||
},
|
||||
}
|
||||
|
||||
rendered := PlayedWord(m, true, nil)
|
||||
if len(rendered.GetParts()) != 2 {
|
||||
t.Fatalf("rendered %d parts, want 2", len(rendered.GetParts()))
|
||||
}
|
||||
sum := uint32(0)
|
||||
for _, p := range rendered.GetParts() {
|
||||
sum += p.GetValue()
|
||||
}
|
||||
if sum != rendered.GetPoints() {
|
||||
t.Errorf("parts sum to %d, want points %d", sum, rendered.GetPoints())
|
||||
}
|
||||
if rendered.GetParts()[0].GetKind() != noituv1.PointKind_POINT_KIND_BASE {
|
||||
t.Errorf("first part kind = %v, want BASE", rendered.GetParts()[0].GetKind())
|
||||
}
|
||||
if rendered.GetParts()[1].GetKind() != noituv1.PointKind_POINT_KIND_SPEED {
|
||||
t.Errorf("second part kind = %v, want SPEED", rendered.GetParts()[1].GetKind())
|
||||
}
|
||||
}
|
||||
|
||||
// TestDifficultyMapping checks each level and, more importantly, that an
|
||||
// unknown wire difficulty is refused rather than defaulted. Difficulty arrives
|
||||
// from the client, so silently treating garbage as Easy would let a malformed
|
||||
|
||||
Reference in new issue
Block a user