Search Apps Documentation Source Content File Folder Download Copy Actions Download State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

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}