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

Community realm. Read the code first. Community realm, deployed by its author. Read the code before you interact or send coins.

bullscows.gno

5.65 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/nym-moultest123/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}