Search Apps Documentation Source Content File Folder Download Copy Actions Download 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}