1// Package memory implements "Memory Match", a concentration game on a2// 4x4 grid of 8 emoji pairs. The board is shuffled deterministically from3// the current block height (no on-chain RNG exists), so a NewGame started at4// a given height always produces the same layout.5package memory67import (8 "strconv"9 "strings"1011 "chain"12 "chain/runtime"13)1415const gridSize = 16 // 4x416const numPairs = 81718// faces are the 8 distinct emoji; each appears exactly twice on the board.19var faces = [numPairs]string{"🐶", "🐱", "🦊", "🐸", "🐵", "🐼", "🦁", "🐧"}2021// tile holds the face index (0..7) for a cell and whether it is matched.22type tile struct {23 face int24 matched bool25}2627// board is the full game state. It is a package-level singleton so the realm28// persists a single shared game.29var board = struct {30 tiles [gridSize]tile31 flipped []int // indices currently face-up but not yet resolved (0, 1 or 2)32 moves int // number of completed two-tile attempts33 pairs int // matched pairs so far34 height int64 // block height the current game was seeded from35 started bool36}{}3738// shuffle produces a deterministic permutation of the 16 tile faces from a39// 64-bit seed. It fills a slice with two of each face index then applies a40// Fisher-Yates shuffle driven by a simple LCG — fully deterministic, no RNG.41func shuffle(seed int64) [gridSize]tile {42 vals := make([]int, 0, gridSize)43 for i := 0; i < numPairs; i++ {44 vals = append(vals, i, i)45 }46 // LCG (Numerical Recipes constants); force unsigned-style math via uint64.47 state := uint64(seed)*6364136223846793005 + 144269504088896340748 for i := len(vals) - 1; i > 0; i-- {49 state = state*6364136223846793005 + 144269504088896340750 j := int(state % uint64(i+1))51 vals[i], vals[j] = vals[j], vals[i]52 }53 var out [gridSize]tile54 for i := 0; i < gridSize; i++ {55 out[i] = tile{face: vals[i], matched: false}56 }57 return out58}5960func ensureStarted() {61 if !board.started {62 reset(runtime.ChainHeight())63 }64}6566func reset(height int64) {67 board.tiles = shuffle(height)68 board.flipped = nil69 board.moves = 070 board.pairs = 071 board.height = height72 board.started = true73}7475// NewGame reshuffles the board using the current block height as the seed.76func NewGame(cur realm) {77 reset(runtime.ChainHeight())78 chain.Emit("NewGame", "height", strconv.FormatInt(board.height, 10))79}8081// Flip reveals the tile at index (0..15).82//83// Rules:84// - If two tiles are already showing from a previous unmatched attempt,85// this Flip first clears them (they turn back to hidden), then reveals86// the requested tile as the first of a new attempt.87// - Revealing the second tile of an attempt completes a move: matching88// faces stay face-up (matched); a mismatch leaves both showing until the89// next Flip clears them.90// - Flipping an already-matched or already-showing tile is rejected.91func Flip(cur realm, index int) {92 ensureStarted()9394 if index < 0 || index >= gridSize {95 panic("index out of range")96 }97 if board.tiles[index].matched {98 panic("tile already matched")99 }100101 // A completed unmatched attempt (2 face-up, non-matching) is cleared102 // before the next reveal.103 if len(board.flipped) == 2 {104 board.flipped = nil105 }106107 for _, f := range board.flipped {108 if f == index {109 panic("tile already flipped")110 }111 }112113 board.flipped = append(board.flipped, index)114115 if len(board.flipped) != 2 {116 chain.Emit("Flip", "index", strconv.Itoa(index))117 return118 }119120 // Second tile of the attempt: resolve.121 board.moves++122 a, b := board.flipped[0], board.flipped[1]123 if board.tiles[a].face == board.tiles[b].face {124 board.tiles[a].matched = true125 board.tiles[b].matched = true126 board.pairs++127 board.flipped = nil128 chain.Emit("Match", "index", strconv.Itoa(index), "pairs", strconv.Itoa(board.pairs))129 } else {130 // leave both showing; next Flip clears them131 chain.Emit("Miss", "index", strconv.Itoa(index))132 }133}134135func solved() bool {136 return board.pairs == numPairs137}138139// cellFace returns the emoji to display for a cell given the current state.140func cellFace(i int) string {141 if board.tiles[i].matched {142 return faces[board.tiles[i].face]143 }144 for _, f := range board.flipped {145 if f == i {146 return faces[board.tiles[i].face]147 }148 }149 return "❓"150}151152// Render draws the board as Markdown: a 4x4 grid, move count and solved state.153func Render(path string) string {154 ensureStarted()155156 var sb strings.Builder157 sb.WriteString("# 🧠 Memory Match\n\n")158 sb.WriteString("Match all 8 emoji pairs on the 4x4 grid.\n\n")159160 sb.WriteString("| | | | |\n")161 sb.WriteString("|:-:|:-:|:-:|:-:|\n")162 for row := 0; row < 4; row++ {163 sb.WriteString("|")164 for col := 0; col < 4; col++ {165 sb.WriteString(" " + cellFace(row*4+col) + " |")166 }167 sb.WriteString("\n")168 }169170 sb.WriteString("\n")171 sb.WriteString("- Moves: **" + strconv.Itoa(board.moves) + "**\n")172 sb.WriteString("- Pairs matched: **" + strconv.Itoa(board.pairs) + " / " + strconv.Itoa(numPairs) + "**\n")173 sb.WriteString("- Seed height: **" + strconv.FormatInt(board.height, 10) + "**\n\n")174175 if solved() {176 sb.WriteString("🎉 **Solved in " + strconv.Itoa(board.moves) + " moves!** Call `NewGame` to play again.\n")177 } else {178 sb.WriteString("Call `Flip(index)` with a cell index 0–15. `NewGame()` reshuffles.\n")179 }180181 return sb.String()182}183Flip(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}, index int)
NewGame(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})
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.