Search Apps Documentation Source Content File Folder Download Copy Actions Download

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}