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