Skip to content Search Apps Documentation Source Content File Folder Download Copy Actions State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

trivia.gno

6.02 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 := scores.Get(addr); v != nil {
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 := scores.Get(addr); v != nil {
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}