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

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}