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

quizstreak.gno

6.70 Kb · 205 lines
  1// Package quizstreak is a multiple-choice trivia quiz where every player
  2// runs their own independent streak instead of racing others for a single
  3// shared question.
  4//
  5// Each address gets its own current question, picked deterministically from
  6// the address and the block height of its first answer so different players
  7// don't all start on the same question. A correct answer extends the
  8// player's streak and advances them to their next question; a wrong answer
  9// breaks the streak back to zero but leaves the same question live so they
 10// can retry. The realm tracks each player's best-ever streak on a global
 11// leaderboard.
 12package quizstreak
 13
 14import (
 15	"strconv"
 16	"strings"
 17
 18	"chain"
 19	"chain/runtime"
 20
 21	"gno.land/p/nt/avl/v0"
 22)
 23
 24// question is one fixed trivia entry.
 25type question struct {
 26	text       string
 27	choices    [4]string
 28	correctIdx int
 29}
 30
 31// playerState is one address's progress through the question bank.
 32type playerState struct {
 33	qIdx    int
 34	streak  int
 35	best    int
 36	correct int
 37	wrong   int
 38}
 39
 40var (
 41	questions = []question{
 42		{"What is the smallest prime number?", [4]string{"0", "1", "2", "3"}, 2},
 43		{"Which gas does a plant primarily absorb for photosynthesis?", [4]string{"Oxygen", "Nitrogen", "Carbon dioxide", "Hydrogen"}, 2},
 44		{"How many continents are there on Earth?", [4]string{"5", "6", "7", "8"}, 2},
 45		{"In binary, what is 1 + 1?", [4]string{"2", "10", "11", "0"}, 1},
 46		{"What does 'HTTP' stand for?", [4]string{"HyperText Transfer Protocol", "High Transfer Text Protocol", "Home Tool Transfer Protocol", "HyperText Transmission Path"}, 0},
 47		{"Which planet is known as the Red Planet?", [4]string{"Venus", "Jupiter", "Mars", "Saturn"}, 2},
 48		{"What is the time complexity of binary search?", [4]string{"O(n)", "O(log n)", "O(n^2)", "O(1)"}, 1},
 49		{"Which ocean is the largest by surface area?", [4]string{"Atlantic", "Indian", "Arctic", "Pacific"}, 3},
 50		{"What does 'CPU' stand for?", [4]string{"Central Processing Unit", "Computer Personal Unit", "Central Program Utility", "Core Processing Unicode"}, 0},
 51		{"How many bits are in a byte?", [4]string{"4", "8", "16", "32"}, 1},
 52	}
 53
 54	players = avl.NewTree() // addr(string) -> *playerState
 55)
 56
 57// startIndex picks a deterministic starting question for a brand-new player,
 58// spreading players across the bank using their address and the height of
 59// their first answer as entropy.
 60func startIndex(addr string, height int64) int {
 61	if height < 0 {
 62		height = -height
 63	}
 64	var sum int64
 65	for i := 0; i < len(addr); i++ {
 66		sum += int64(addr[i])
 67	}
 68	n := int64(len(questions))
 69	return int((sum + height) % n)
 70}
 71
 72// getPlayer returns addr's state, lazily creating it on first contact.
 73func getPlayer(addr string) *playerState {
 74	if v, ok := players.Get(addr).(*playerState); ok {
 75		return v
 76	}
 77	p := &playerState{qIdx: startIndex(addr, runtime.ChainHeight())}
 78	players.Set(addr, p)
 79	return p
 80}
 81
 82// nextIndex advances a solved player to a fresh-feeling next question: the
 83// step grows with the streak so a long run doesn't loop through the bank in
 84// a visibly fixed order.
 85func nextIndex(cur, streak, n int) int {
 86	return (cur + 1 + streak) % n
 87}
 88
 89// Answer submits a choice (0..3) for the caller's current question.
 90// Crossing function: caller invokes as Answer(cross(cur), choiceIdx).
 91func Answer(cur realm, choiceIdx int) string {
 92	if !cur.IsCurrent() {
 93		panic("spoofed realm")
 94	}
 95	if choiceIdx < 0 || choiceIdx >= len(questions[0].choices) {
 96		panic("choice must be in 0..3")
 97	}
 98
 99	addr := cur.Previous().Address().String()
100	p := getPlayer(addr)
101	q := questions[p.qIdx]
102
103	if choiceIdx != q.correctIdx {
104		brokeStreak := p.streak
105		p.streak = 0
106		p.wrong++
107		if brokeStreak > 0 {
108			return "Wrong — streak of " + strconv.Itoa(brokeStreak) + " broken. Same question stays live, try again."
109		}
110		return "Wrong — try again."
111	}
112
113	p.streak++
114	p.correct++
115	if p.streak > p.best {
116		p.best = p.streak
117	}
118	p.qIdx = nextIndex(p.qIdx, p.streak, len(questions))
119
120	chain.Emit("QuizAnswered",
121		"player", addr,
122		"streak", strconv.Itoa(p.streak),
123		"best", strconv.Itoa(p.best),
124	)
125
126	return "Correct! Streak is now " + strconv.Itoa(p.streak) + ". Next question is live."
127}
128
129// leaderboardEntry is a snapshot row used only for rendering, sorted by best
130// streak descending.
131type leaderboardEntry struct {
132	addr string
133	best int
134}
135
136func leaderboard() []leaderboardEntry {
137	var rows []leaderboardEntry
138	players.Iterate("", "", func(addr string, v any) bool {
139		rows = append(rows, leaderboardEntry{addr: addr, best: v.(*playerState).best})
140		return false
141	})
142	for i := 1; i < len(rows); i++ {
143		j := i
144		for j > 0 && rows[j-1].best < rows[j].best {
145			rows[j-1], rows[j] = rows[j], rows[j-1]
146			j--
147		}
148	}
149	return rows
150}
151
152func renderQuestion(b *strings.Builder, p *playerState) {
153	q := questions[p.qIdx]
154	letters := [4]string{"A", "B", "C", "D"}
155	b.WriteString("**" + q.text + "**\n\n")
156	for i, c := range q.choices {
157		b.WriteString("- **" + letters[i] + "** (" + strconv.Itoa(i) + "): " + c + "\n")
158	}
159	b.WriteString("\n- Current streak: " + strconv.Itoa(p.streak) + "\n")
160	b.WriteString("- Best streak: " + strconv.Itoa(p.best) + "\n")
161	b.WriteString("- Correct / wrong lifetime: " + strconv.Itoa(p.correct) + " / " + strconv.Itoa(p.wrong) + "\n\n")
162}
163
164// Render produces the gnoweb Markdown view. The root path shows the rules
165// and the leaderboard; a path of a bech32 address shows that player's
166// current question and stats.
167func Render(path string) string {
168	var b strings.Builder
169
170	b.WriteString("# Quiz Streak\n\n")
171	b.WriteString("Everyone plays their own trivia run at their own pace. Call ")
172	b.WriteString("`Answer(choiceIdx)` with 0-3 — a correct answer extends your streak ")
173	b.WriteString("and moves you to your next question; a wrong answer breaks your ")
174	b.WriteString("streak but leaves the same question live so you can retry.\n\n")
175
176	if path != "" {
177		if v, ok := players.Get(path).(*playerState); ok {
178			b.WriteString("## Your question (`" + path + "`)\n\n")
179			renderQuestion(&b, v)
180		} else {
181			b.WriteString("## `" + path + "`\n\n_No answers submitted yet — call `Answer` to get your first question._\n\n")
182		}
183	}
184
185	b.WriteString("## Best-streak leaderboard\n\n")
186	rows := leaderboard()
187	if len(rows) == 0 {
188		b.WriteString("_No one has played yet — be the first!_\n\n")
189	} else {
190		b.WriteString("| Player | Best streak |\n|---|---|\n")
191		for _, r := range rows {
192			b.WriteString("| `" + r.addr + "` | " + strconv.Itoa(r.best) + " |\n")
193		}
194		b.WriteString("\n")
195	}
196
197	b.WriteString("## How to play\n\n")
198	b.WriteString("```\n")
199	b.WriteString("gnokey maketx call -pkgpath gno.land/r/g12cs4cehujpffpjpywmkqj43m6u5ya53nj69sjz/quizstreak \\\n")
200	b.WriteString("  -func Answer -args <0|1|2|3> ...\n")
201	b.WriteString("```\n\n")
202	b.WriteString("View your own dashboard at `.../quizstreak:<your-address>`.\n")
203
204	return b.String()
205}