memory.gno
5.17 Kb Β· 182 lines
1// Package memory implements "Memory Match", a concentration game on a
2// 4x4 grid of 8 emoji pairs. The board is shuffled deterministically from
3// the current block height (no on-chain RNG exists), so a NewGame started at
4// a given height always produces the same layout.
5package memory
6
7import (
8 "strconv"
9 "strings"
10
11 "chain"
12 "chain/runtime"
13)
14
15const gridSize = 16 // 4x4
16const numPairs = 8
17
18// faces are the 8 distinct emoji; each appears exactly twice on the board.
19var faces = [numPairs]string{"πΆ", "π±", "π¦", "πΈ", "π΅", "πΌ", "π¦", "π§"}
20
21// tile holds the face index (0..7) for a cell and whether it is matched.
22type tile struct {
23 face int
24 matched bool
25}
26
27// board is the full game state. It is a package-level singleton so the realm
28// persists a single shared game.
29var board = struct {
30 tiles [gridSize]tile
31 flipped []int // indices currently face-up but not yet resolved (0, 1 or 2)
32 moves int // number of completed two-tile attempts
33 pairs int // matched pairs so far
34 height int64 // block height the current game was seeded from
35 started bool
36}{}
37
38// shuffle produces a deterministic permutation of the 16 tile faces from a
39// 64-bit seed. It fills a slice with two of each face index then applies a
40// 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 + 1442695040888963407
48 for i := len(vals) - 1; i > 0; i-- {
49 state = state*6364136223846793005 + 1442695040888963407
50 j := int(state % uint64(i+1))
51 vals[i], vals[j] = vals[j], vals[i]
52 }
53 var out [gridSize]tile
54 for i := 0; i < gridSize; i++ {
55 out[i] = tile{face: vals[i], matched: false}
56 }
57 return out
58}
59
60func ensureStarted() {
61 if !board.started {
62 reset(runtime.ChainHeight())
63 }
64}
65
66func reset(height int64) {
67 board.tiles = shuffle(height)
68 board.flipped = nil
69 board.moves = 0
70 board.pairs = 0
71 board.height = height
72 board.started = true
73}
74
75// 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}
80
81// 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 reveals
86// the requested tile as the first of a new attempt.
87// - Revealing the second tile of an attempt completes a move: matching
88// faces stay face-up (matched); a mismatch leaves both showing until the
89// next Flip clears them.
90// - Flipping an already-matched or already-showing tile is rejected.
91func Flip(cur realm, index int) {
92 ensureStarted()
93
94 if index < 0 || index >= gridSize {
95 panic("index out of range")
96 }
97 if board.tiles[index].matched {
98 panic("tile already matched")
99 }
100
101 // A completed unmatched attempt (2 face-up, non-matching) is cleared
102 // before the next reveal.
103 if len(board.flipped) == 2 {
104 board.flipped = nil
105 }
106
107 for _, f := range board.flipped {
108 if f == index {
109 panic("tile already flipped")
110 }
111 }
112
113 board.flipped = append(board.flipped, index)
114
115 if len(board.flipped) != 2 {
116 chain.Emit("Flip", "index", strconv.Itoa(index))
117 return
118 }
119
120 // 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 = true
125 board.tiles[b].matched = true
126 board.pairs++
127 board.flipped = nil
128 chain.Emit("Match", "index", strconv.Itoa(index), "pairs", strconv.Itoa(board.pairs))
129 } else {
130 // leave both showing; next Flip clears them
131 chain.Emit("Miss", "index", strconv.Itoa(index))
132 }
133}
134
135func solved() bool {
136 return board.pairs == numPairs
137}
138
139// 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}
151
152// Render draws the board as Markdown: a 4x4 grid, move count and solved state.
153func Render(path string) string {
154 ensureStarted()
155
156 var sb strings.Builder
157 sb.WriteString("# π§ Memory Match\n\n")
158 sb.WriteString("Match all 8 emoji pairs on the 4x4 grid.\n\n")
159
160 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 }
169
170 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")
174
175 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 }
180
181 return sb.String()
182}