1// Package bullscows is a shared on-chain Bulls & Cows puzzle: one secret2// 4-digit code (distinct digits) per round, guessed cooperatively/competitively3// by anyone who calls Guess. Bulls = right digit, right spot. Cows = right4// digit, wrong spot. Whoever hits 4 bulls wins the round and a leaderboard5// entry; a fresh secret is drawn immediately after.6package bullscows78import (9 "chain/runtime"10 "strconv"11 "strings"12)1314const (15 maxHistory = 1016 maxRecords = 517)1819type guessLog struct {20 player string21 guess string22 bulls int23 cows int24}2526type record struct {27 player string28 attempts int29 round int30}3132var (33 secret [4]int34 round int35 attempts int36 history []guessLog37 records []record38)3940func init() {41 round = 142 secret = newSecret(seedFor(round))43}4445// seedFor derives a per-round PRNG seed from the current chain height so46// nobody can precompute the next secret before the round actually starts.47func seedFor(r int) int64 {48 return runtime.ChainHeight()*1000003 + int64(r)*7919 + 1749}5051// nextRand is a minimal LCG (glibc constants) — no crypto strength needed52// for a casual puzzle, just enough spread across digits.53func nextRand(seed int64) int64 {54 return (seed*1103515245 + 12345) & 0x7fffffff55}5657func newSecret(seed int64) [4]int {58 var used [10]bool59 var digits [4]int60 s := seed61 for i := 0; i < 4; i++ {62 for {63 s = nextRand(s)64 d := int(s % 10)65 if !used[d] {66 used[d] = true67 digits[i] = d68 break69 }70 }71 }72 return digits73}7475// parseGuess validates a 4-digit, distinct-digit guess string.76func parseGuess(guess string) ([4]int, bool) {77 var out [4]int78 if len(guess) != 4 {79 return out, false80 }81 var used [10]bool82 for i := 0; i < 4; i++ {83 c := guess[i]84 if c < '0' || c > '9' {85 return out, false86 }87 d := int(c - '0')88 if used[d] {89 return out, false90 }91 used[d] = true92 out[i] = d93 }94 return out, true95}9697// score returns (bulls, cows) for guess against target, assuming both have98// distinct digits (guaranteed by parseGuess / newSecret).99func score(target, guess [4]int) (int, int) {100 bulls, cows := 0, 0101 for i := 0; i < 4; i++ {102 if guess[i] == target[i] {103 bulls++104 continue105 }106 for j := 0; j < 4; j++ {107 if i != j && guess[i] == target[j] {108 cows++109 break110 }111 }112 }113 return bulls, cows114}115116func addHistory(l guessLog) {117 history = append(history, l)118 if len(history) > maxHistory {119 history = history[len(history)-maxHistory:]120 }121}122123// 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}133134func startNewRound() {135 round++136 attempts = 0137 history = nil138 secret = newSecret(seedFor(round))139}140141// 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 }148149 player := cur.Previous().Address().String()150 attempts++151 bulls, cows := score(secret, digits)152 addHistory(guessLog{player: player, guess: guess, bulls: bulls, cows: cows})153154 if bulls == 4 {155 wonRound := round156 wonAttempts := attempts157 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 }163164 return guess + " -> " + strconv.Itoa(bulls) + " bulls, " + strconv.Itoa(cows) + " cows"165}166167// GiveUp reveals the current secret and starts a fresh round without168// awarding a leaderboard record.169func GiveUp(cur realm) string {170 revealed := digitsToString(secret)171 oldRound := round172 startNewRound()173 return "round " + strconv.Itoa(oldRound) + "'s code was " + revealed +174 " — round " + strconv.Itoa(round) + " is live now."175}176177func digitsToString(d [4]int) string {178 var sb strings.Builder179 for _, v := range d {180 sb.WriteString(strconv.Itoa(v))181 }182 return sb.String()183}184185func Render(path string) string {186 var sb strings.Builder187 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")191192 sb.WriteString("## Round " + strconv.Itoa(round) + "\n\n")193 sb.WriteString("Attempts so far: **" + strconv.Itoa(attempts) + "**\n\n")194195 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 }207208 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 }218219 return sb.String()220}221222func shortAddr(a string) string {223 if len(a) <= 12 {224 return a225 }226 return a[:6] + "…" + a[len(a)-4:]227}228GiveUp(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}) string
Guess(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, guess string) string
Render(path string) string
Signatures reconstructed verbatim from vm/qfuncs — interface params keep their inline definitions.
vm/qrender output, sanitized (docs/render-security.md) and displayed in an empty-sandbox iframe — scripts, forms and popups cannot run. Links stay inert in-preview; right-click to open.