hangman_test.gno
3.46 Kb · 136 lines
1package hangman
2
3import (
4 "strings"
5 "testing"
6
7 "gno.land/p/nt/avl/v0"
8 "gno.land/p/nt/testutils/v0"
9 "gno.land/p/nt/uassert/v0"
10)
11
12// TestPureHelpers checks the deterministic, non-crossing logic.
13func TestPureHelpers(t *testing.T) {
14 // normalizeLetter
15 if c, ok := normalizeLetter("A"); !ok || c != "a" {
16 t.Fatalf("normalizeLetter(A) = %q,%v; want a,true", c, ok)
17 }
18 if _, ok := normalizeLetter("ab"); ok {
19 t.Fatalf("normalizeLetter(ab) should fail")
20 }
21 if _, ok := normalizeLetter("3"); ok {
22 t.Fatalf("normalizeLetter(3) should fail")
23 }
24
25 // wordForDay is deterministic and cycles the list.
26 w0 := wordForDay(0)
27 if w0 != words[0] {
28 t.Fatalf("wordForDay(0) = %q; want %q", w0, words[0])
29 }
30 if wordForDay(int64(len(words))) != words[0] {
31 t.Fatalf("wordForDay should wrap around the list length")
32 }
33
34 // dayKey formatting.
35 if got := dayKey(0); got != "0" {
36 t.Fatalf("dayKey(0) = %q; want 0", got)
37 }
38 if got := dayKey(123); got != "123" {
39 t.Fatalf("dayKey(123) = %q; want 123", got)
40 }
41
42 // gallows clamps and grows.
43 if gallows(0) == gallows(6) {
44 t.Fatalf("gallows should differ between 0 and 6 wrong")
45 }
46 if gallows(99) != gallows(6) {
47 t.Fatalf("gallows should clamp out-of-range to max stage")
48 }
49}
50
51// TestRoundMasking checks masked/win/lose logic directly on a round.
52func TestRoundMasking(t *testing.T) {
53 r := &round{word: "gno", guessed: avl.NewTree(), wrong: 0}
54
55 if got := r.masked(); got != "_ _ _" {
56 t.Fatalf("initial masked = %q; want _ _ _", got)
57 }
58 if r.isWon() {
59 t.Fatalf("fresh round should not be won")
60 }
61
62 r.guessed.Set("g", struct{}{})
63 if got := r.masked(); got != "g _ _" {
64 t.Fatalf("masked after g = %q; want g _ _", got)
65 }
66
67 r.guessed.Set("n", struct{}{})
68 r.guessed.Set("o", struct{}{})
69 if !r.isWon() {
70 t.Fatalf("round should be won after guessing all letters")
71 }
72
73 // Lose path.
74 r2 := &round{word: "gno", guessed: avl.NewTree(), wrong: maxWrong}
75 if !r2.isLost() {
76 t.Fatalf("round with %d wrong should be lost", maxWrong)
77 }
78 if !r2.over() {
79 t.Fatalf("lost round should be over")
80 }
81}
82
83// TestGuessCrossing exercises the crossing entrypoint end to end.
84func TestGuessCrossing(cur realm, t *testing.T) {
85 user := testutils.TestAddress("player")
86 testing.SetRealm(testing.NewUserRealm(user))
87
88 r := current()
89 word := r.word
90
91 // A correct guess: pick the first letter of today's word.
92 first := string(word[0])
93 Guess(cross(cur), first)
94 if !r.guessed.Has(first) {
95 t.Fatalf("guessed set should contain %q after Guess", first)
96 }
97 if r.wrong != 0 {
98 t.Fatalf("correct guess should not increase wrong count, got %d", r.wrong)
99 }
100
101 // Re-guessing the same letter aborts.
102 uassert.AbortsWithMessage(t, cur, "letter already guessed: "+first, func() {
103 Guess(cross(cur), first)
104 })
105
106 // Invalid input aborts.
107 uassert.AbortsWithMessage(t, cur, "guess must be a single letter a-z", func() {
108 Guess(cross(cur), "42")
109 })
110
111 // A wrong guess: find a letter not in the word.
112 miss := firstMissing(word)
113 if miss != "" {
114 before := r.wrong
115 Guess(cross(cur), miss)
116 if r.wrong != before+1 {
117 t.Fatalf("wrong guess should increment wrong count: got %d want %d", r.wrong, before+1)
118 }
119 }
120
121 // Render always returns non-empty markdown.
122 out := Render("")
123 if !strings.Contains(out, "Hangman") {
124 t.Fatalf("Render output missing title")
125 }
126}
127
128// firstMissing returns a lowercase letter a-z not present in word, or "".
129func firstMissing(word string) string {
130 for c := byte('a'); c <= 'z'; c++ {
131 if !strings.Contains(word, string(c)) {
132 return string(c)
133 }
134 }
135 return ""
136}