trivia.gno
6.01 Kb · 199 lines
1// Package trivia is a rotating multiple-choice trivia quiz realm.
2//
3// One question is live at a time. Anyone can call Answer with a choice
4// index; the first caller to answer correctly scores a point and the quiz
5// advances to the next question. The starting question is picked
6// deterministically from the block height at deploy time so repeated
7// deploys don't always open on question one.
8package trivia
9
10import (
11 "strconv"
12 "strings"
13
14 "chain"
15 "chain/runtime"
16
17 "gno.land/p/nt/avl/v0"
18)
19
20// question is one fixed trivia entry.
21type question struct {
22 text string
23 choices [4]string
24 correctIdx int
25}
26
27// round holds the mutable state of the currently open question.
28type round struct {
29 number int // 1-based round counter, increments forever
30 qIdx int // index into questions for this round
31 attempts int // guesses made against this round so far
32 solved bool
33 winner string
34}
35
36var (
37 questions = []question{
38 {"What is the smallest prime number?", [4]string{"0", "1", "2", "3"}, 2},
39 {"Which gas does a plant primarily absorb for photosynthesis?", [4]string{"Oxygen", "Nitrogen", "Carbon dioxide", "Hydrogen"}, 2},
40 {"How many continents are there on Earth?", [4]string{"5", "6", "7", "8"}, 2},
41 {"In binary, what is 1 + 1?", [4]string{"2", "10", "11", "0"}, 1},
42 {"What does 'HTTP' stand for?", [4]string{"HyperText Transfer Protocol", "High Transfer Text Protocol", "Home Tool Transfer Protocol", "HyperText Transmission Path"}, 0},
43 {"Which planet is known as the Red Planet?", [4]string{"Venus", "Jupiter", "Mars", "Saturn"}, 2},
44 {"What is the time complexity of binary search?", [4]string{"O(n)", "O(log n)", "O(n^2)", "O(1)"}, 1},
45 {"Who wrote the original Go proverb 'Don't communicate by sharing memory'?", [4]string{"Linus Torvalds", "Rob Pike", "Guido van Rossum", "Brendan Eich"}, 1},
46 }
47
48 current *round
49 scores = avl.NewTree() // addr(string) -> *int, points across all rounds
50
51 totalCorrect int // lifetime count of correctly-answered questions
52)
53
54func init() {
55 current = newRound(1, runtime.ChainHeight())
56}
57
58// newRound builds a fresh round. The question index cycles through the fixed
59// list, offset by a height-derived starting point so consecutive deploys
60// don't all open on the same question.
61func newRound(number int, height int64) *round {
62 return &round{
63 number: number,
64 qIdx: questionIndex(number, height),
65 }
66}
67
68// questionIndex maps a round number and a height (entropy source for the
69// starting offset) to a slot in the questions slice. Deterministic and pure
70// so it's easy to unit test.
71func questionIndex(number int, height int64) int {
72 if height < 0 {
73 height = -height
74 }
75 n := len(questions)
76 offset := int(height % int64(n))
77 return (offset + number - 1) % n
78}
79
80// Answer submits a choice (0..3) for the current question. Crossing
81// function: caller invokes as Answer(cross(cur), choiceIdx).
82func Answer(cur realm, choiceIdx int) string {
83 if !cur.IsCurrent() {
84 panic("spoofed realm")
85 }
86 q := questions[current.qIdx]
87 if choiceIdx < 0 || choiceIdx >= len(q.choices) {
88 panic("choice must be in 0.." + strconv.Itoa(len(q.choices)-1))
89 }
90 if current.solved {
91 panic("this question is already solved; the next one is already live")
92 }
93
94 current.attempts++
95 caller := cur.Previous().Address().String()
96
97 if choiceIdx != q.correctIdx {
98 return "Wrong — try again."
99 }
100
101 current.solved = true
102 current.winner = caller
103 addScore(caller)
104 totalCorrect++
105
106 chain.Emit("QuestionSolved",
107 "round", strconv.Itoa(current.number),
108 "winner", caller,
109 "answer", q.choices[choiceIdx],
110 )
111
112 solvedRound := current.number
113 current = newRound(solvedRound+1, runtime.ChainHeight())
114
115 return "Correct! The answer was \"" + q.choices[choiceIdx] + "\". A new question is now live."
116}
117
118// addScore bumps addr's lifetime point count.
119func addScore(addr string) {
120 if v, ok := scores.Get(addr); ok {
121 p := v.(*int)
122 *p++
123 return
124 }
125 one := 1
126 scores.Set(addr, &one)
127}
128
129func scoreOf(addr string) int {
130 if v, ok := scores.Get(addr); ok {
131 return *(v.(*int))
132 }
133 return 0
134}
135
136// leaderboardEntry is a snapshot row used only for rendering, sorted by
137// score descending.
138type leaderboardEntry struct {
139 addr string
140 score int
141}
142
143func leaderboard() []leaderboardEntry {
144 var rows []leaderboardEntry
145 scores.Iterate("", "", func(addr string, v any) bool {
146 rows = append(rows, leaderboardEntry{addr: addr, score: *(v.(*int))})
147 return false
148 })
149 // simple insertion sort by score desc; leaderboards here stay small.
150 for i := 1; i < len(rows); i++ {
151 j := i
152 for j > 0 && rows[j-1].score < rows[j].score {
153 rows[j-1], rows[j] = rows[j], rows[j-1]
154 j--
155 }
156 }
157 return rows
158}
159
160// Render produces the gnoweb Markdown view of the quiz.
161func Render(path string) string {
162 var b strings.Builder
163
164 b.WriteString("# Trivia Quiz\n\n")
165 b.WriteString("A rotating on-chain trivia quiz. Call `Answer(choiceIdx)` with 0-3 ")
166 b.WriteString("to answer the current question — first correct answer scores the point ")
167 b.WriteString("and a new question goes live.\n\n")
168
169 q := questions[current.qIdx]
170 b.WriteString("## Round " + strconv.Itoa(current.number) + "\n\n")
171 b.WriteString("**" + q.text + "**\n\n")
172 letters := [4]string{"A", "B", "C", "D"}
173 for i, c := range q.choices {
174 b.WriteString("- **" + letters[i] + "** (" + strconv.Itoa(i) + "): " + c + "\n")
175 }
176 b.WriteString("\n- Attempts so far this round: " + strconv.Itoa(current.attempts) + "\n\n")
177
178 b.WriteString("## Leaderboard\n\n")
179 rows := leaderboard()
180 if len(rows) == 0 {
181 b.WriteString("_No one has scored yet — be the first!_\n\n")
182 } else {
183 b.WriteString("| Player | Points |\n|---|---|\n")
184 for _, r := range rows {
185 b.WriteString("| `" + r.addr + "` | " + strconv.Itoa(r.score) + " |\n")
186 }
187 b.WriteString("\n")
188 }
189
190 b.WriteString("Total questions answered correctly across all rounds: " + strconv.Itoa(totalCorrect) + "\n\n")
191
192 b.WriteString("## How to play\n\n")
193 b.WriteString("```\n")
194 b.WriteString("gnokey maketx call -pkgpath gno.land/r/g12cs4cehujpffpjpywmkqj43m6u5ya53nj69sjz/trivia \\\n")
195 b.WriteString(" -func Answer -args <0|1|2|3> ...\n")
196 b.WriteString("```\n")
197
198 return b.String()
199}