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.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}