← Back to Realms

Realm

gno.land/r/moul/x/daily/bullscows/v0

Overview

Realm Path
gno.land/r/moul/x/daily/bullscows/v0
Exported Functions
3
State Entries
9
Source Files
2
Total Package Entries
23

Exported Functions

3 exported functions

State

9 state entries

Source Code

FILES
bullscows.gno
go
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}
228

Raw Package Data

Raw JSON data