Search Apps Documentation Source Content File Folder Download Copy Actions Download

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}