bullscows.gno
5.64 Kb · 222 lines
1// Package bullscows is a shared on-chain Bulls & Cows puzzle: one secret
2// 4-digit code (distinct digits) per round, guessed cooperatively/competitively
3// by anyone who calls Guess. Bulls = right digit, right spot. Cows = right
4// digit, wrong spot. Whoever hits 4 bulls wins the round and a leaderboard
5// entry; a fresh secret is drawn immediately after.
6package bullscows
7
8import (
9 "chain/runtime"
10 "strconv"
11 "strings"
12
13 "gno.land/p/moul/kit/ui/v0"
14)
15
16const (
17 maxHistory = 10
18 maxRecords = 5
19)
20
21type guessLog struct {
22 player string
23 guess string
24 bulls int
25 cows int
26}
27
28type record struct {
29 player string
30 attempts int
31 round int
32}
33
34var (
35 secret [4]int
36 round int
37 attempts int
38 history []guessLog
39 records []record
40)
41
42func init() {
43 round = 1
44 secret = newSecret(seedFor(round))
45}
46
47// seedFor derives a per-round PRNG seed from the current chain height so
48// nobody can precompute the next secret before the round actually starts.
49func seedFor(r int) int64 {
50 return runtime.ChainHeight()*1000003 + int64(r)*7919 + 17
51}
52
53// nextRand is a minimal LCG (glibc constants) — no crypto strength needed
54// for a casual puzzle, just enough spread across digits.
55func nextRand(seed int64) int64 {
56 return (seed*1103515245 + 12345) & 0x7fffffff
57}
58
59func newSecret(seed int64) [4]int {
60 var used [10]bool
61 var digits [4]int
62 s := seed
63 for i := 0; i < 4; i++ {
64 for {
65 s = nextRand(s)
66 d := int(s % 10)
67 if !used[d] {
68 used[d] = true
69 digits[i] = d
70 break
71 }
72 }
73 }
74 return digits
75}
76
77// parseGuess validates a 4-digit, distinct-digit guess string.
78func parseGuess(guess string) ([4]int, bool) {
79 var out [4]int
80 if len(guess) != 4 {
81 return out, false
82 }
83 var used [10]bool
84 for i := 0; i < 4; i++ {
85 c := guess[i]
86 if c < '0' || c > '9' {
87 return out, false
88 }
89 d := int(c - '0')
90 if used[d] {
91 return out, false
92 }
93 used[d] = true
94 out[i] = d
95 }
96 return out, true
97}
98
99// score returns (bulls, cows) for guess against target, assuming both have
100// distinct digits (guaranteed by parseGuess / newSecret).
101func score(target, guess [4]int) (int, int) {
102 bulls, cows := 0, 0
103 for i := 0; i < 4; i++ {
104 if guess[i] == target[i] {
105 bulls++
106 continue
107 }
108 for j := 0; j < 4; j++ {
109 if i != j && guess[i] == target[j] {
110 cows++
111 break
112 }
113 }
114 }
115 return bulls, cows
116}
117
118func addHistory(l guessLog) {
119 history = append(history, l)
120 if len(history) > maxHistory {
121 history = history[len(history)-maxHistory:]
122 }
123}
124
125// addRecord keeps the top maxRecords fastest solves, ascending by attempts.
126func addRecord(r record) {
127 records = append(records, r)
128 for i := len(records) - 1; i > 0 && records[i].attempts < records[i-1].attempts; i-- {
129 records[i], records[i-1] = records[i-1], records[i]
130 }
131 if len(records) > maxRecords {
132 records = records[:maxRecords]
133 }
134}
135
136func startNewRound() {
137 round++
138 attempts = 0
139 history = nil
140 secret = newSecret(seedFor(round))
141}
142
143// Guess submits a 4-distinct-digit code against the current round's secret.
144// Returns the bulls/cows feedback, or "solved!" text when it's a win.
145func Guess(cur realm, guess string) string {
146 digits, ok := parseGuess(guess)
147 if !ok {
148 panic("guess must be 4 digits, 0-9, no repeats (e.g. \"1972\")")
149 }
150
151 player := cur.Previous().Address().String()
152 attempts++
153 bulls, cows := score(secret, digits)
154 addHistory(guessLog{player: player, guess: guess, bulls: bulls, cows: cows})
155
156 if bulls == 4 {
157 wonRound := round
158 wonAttempts := attempts
159 addRecord(record{player: player, attempts: wonAttempts, round: wonRound})
160 startNewRound()
161 return "*** SOLVED *** " + guess + " was it — round " + strconv.Itoa(wonRound) +
162 " cracked in " + strconv.Itoa(wonAttempts) + " guesses. New round " +
163 strconv.Itoa(round) + " has begun, good luck!"
164 }
165
166 return guess + " -> " + strconv.Itoa(bulls) + " bulls, " + strconv.Itoa(cows) + " cows"
167}
168
169// GiveUp reveals the current secret and starts a fresh round without
170// awarding a leaderboard record.
171func GiveUp(cur realm) string {
172 revealed := digitsToString(secret)
173 oldRound := round
174 startNewRound()
175 return "round " + strconv.Itoa(oldRound) + "'s code was " + revealed +
176 " — round " + strconv.Itoa(round) + " is live now."
177}
178
179func digitsToString(d [4]int) string {
180 var sb strings.Builder
181 for _, v := range d {
182 sb.WriteString(strconv.Itoa(v))
183 }
184 return sb.String()
185}
186
187func Render(path string) string {
188 var sb strings.Builder
189 sb.WriteString("# Bulls & Cows\n\n")
190 sb.WriteString("One shared secret 4-digit code (no repeated digits). Call `Guess(\"1972\")` ")
191 sb.WriteString("to get **bulls** (right digit, right spot) and **cows** (right digit, wrong spot). ")
192 sb.WriteString("First to 4 bulls wins the round; `GiveUp()` reveals the code and starts over.\n\n")
193
194 sb.WriteString("## Round " + strconv.Itoa(round) + "\n\n")
195 sb.WriteString("Attempts so far: **" + strconv.Itoa(attempts) + "**\n\n")
196
197 if len(history) == 0 {
198 sb.WriteString("_No guesses yet this round — be the first._\n\n")
199 } else {
200 sb.WriteString("### Recent guesses\n\n")
201 sb.WriteString("| player | guess | bulls | cows |\n|---|---|---|---|\n")
202 for i := len(history) - 1; i >= 0; i-- {
203 h := history[i]
204 sb.WriteString("| " + ui.AddrOf(h.player) + " | " + h.guess + " | " +
205 strconv.Itoa(h.bulls) + " | " + strconv.Itoa(h.cows) + " |\n")
206 }
207 sb.WriteString("\n")
208 }
209
210 sb.WriteString("## Leaderboard (fastest solves)\n\n")
211 if len(records) == 0 {
212 sb.WriteString("_Nobody has cracked a code yet._\n")
213 } else {
214 sb.WriteString("| rank | player | attempts | round |\n|---|---|---|---|\n")
215 for i, r := range records {
216 sb.WriteString("| " + strconv.Itoa(i+1) + " | " + ui.AddrOf(r.player) + " | " +
217 strconv.Itoa(r.attempts) + " | " + strconv.Itoa(r.round) + " |\n")
218 }
219 }
220
221 return sb.String()
222}