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

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}