life.gno
4.90 Kb · 246 lines
1// Conway's Game of Life on Gno — the entire board lives on-chain.
2// Anyone can call Step() to tick the simulation forward, load classic
3// patterns, or sculpt cells one by one.
4package life
5
6import (
7 "strconv"
8 "strings"
9)
10
11const (
12 MaxWidth = 50
13 MaxHeight = 40
14)
15
16var (
17 width = 20
18 height = 15
19 cells []bool
20 generation int
21 population int
22)
23
24func init() {
25 initBoard(width, height)
26 LoadPreset("glider")
27}
28
29func initBoard(w, h int) {
30 if w < 1 {
31 w = 1
32 }
33 if w > MaxWidth {
34 w = MaxWidth
35 }
36 if h < 1 {
37 h = 1
38 }
39 if h > MaxHeight {
40 h = MaxHeight
41 }
42 width = w
43 height = h
44 cells = make([]bool, width*height)
45 generation = 0
46 population = 0
47}
48
49func cellIdx(x, y int) int {
50 return y*width + x
51}
52
53func alive(x, y int) bool {
54 if x < 0 || x >= width || y < 0 || y >= height {
55 return false
56 }
57 return cells[cellIdx(x, y)]
58}
59
60func countNeighbors(x, y int) int {
61 n := 0
62 for dy := -1; dy <= 1; dy++ {
63 for dx := -1; dx <= 1; dx++ {
64 if dx == 0 && dy == 0 {
65 continue
66 }
67 if alive(x+dx, y+dy) {
68 n++
69 }
70 }
71 }
72 return n
73}
74
75// Step advances the simulation by exactly one generation.
76func Step() {
77 next := make([]bool, width*height)
78 newPop := 0
79 for y := 0; y < height; y++ {
80 for x := 0; x < width; x++ {
81 n := countNeighbors(x, y)
82 var live bool
83 if alive(x, y) {
84 live = n == 2 || n == 3
85 } else {
86 live = n == 3
87 }
88 next[cellIdx(x, y)] = live
89 if live {
90 newPop++
91 }
92 }
93 }
94 cells = next
95 population = newPop
96 generation++
97}
98
99// MultiStep advances the simulation by n generations (capped at 100).
100func MultiStep(n int) {
101 if n < 1 {
102 n = 1
103 }
104 if n > 100 {
105 n = 100
106 }
107 for i := 0; i < n; i++ {
108 Step()
109 }
110}
111
112// SetCell sets or clears a specific cell.
113func SetCell(x, y int, live bool) {
114 if x < 0 || x >= width || y < 0 || y >= height {
115 return
116 }
117 i := cellIdx(x, y)
118 was := cells[i]
119 cells[i] = live
120 if live && !was {
121 population++
122 } else if !live && was {
123 population--
124 }
125}
126
127// Reset clears the board and resizes it to w×h.
128func Reset(w, h int) {
129 initBoard(w, h)
130}
131
132// LoadPreset loads a classic Game of Life pattern onto the current board.
133// Names: glider, blinker, toad, beacon, block, pulsar.
134func LoadPreset(name string) {
135 for i := range cells {
136 cells[i] = false
137 }
138 generation = 0
139 population = 0
140
141 cx := width / 2
142 cy := height / 2
143
144 switch name {
145 case "glider":
146 // .O.
147 // ..O
148 // OOO
149 SetCell(1, 0, true)
150 SetCell(2, 1, true)
151 SetCell(0, 2, true)
152 SetCell(1, 2, true)
153 SetCell(2, 2, true)
154
155 case "blinker":
156 // OOO (period-2 oscillator)
157 SetCell(cx-1, cy, true)
158 SetCell(cx, cy, true)
159 SetCell(cx+1, cy, true)
160
161 case "toad":
162 // period-2 oscillator
163 SetCell(cx, cy-1, true)
164 SetCell(cx+1, cy-1, true)
165 SetCell(cx+2, cy-1, true)
166 SetCell(cx-1, cy, true)
167 SetCell(cx, cy, true)
168 SetCell(cx+1, cy, true)
169
170 case "beacon":
171 // period-2 oscillator
172 SetCell(cx-2, cy-2, true)
173 SetCell(cx-1, cy-2, true)
174 SetCell(cx-2, cy-1, true)
175 SetCell(cx, cy, true)
176 SetCell(cx+1, cy, true)
177 SetCell(cx, cy+1, true)
178 SetCell(cx+1, cy+1, true)
179
180 case "block":
181 // 2×2 still life
182 SetCell(cx, cy, true)
183 SetCell(cx+1, cy, true)
184 SetCell(cx, cy+1, true)
185 SetCell(cx+1, cy+1, true)
186
187 case "pulsar":
188 // period-3 oscillator (needs at least 17×17 board)
189 for _, d := range []int{-4, -3, -2, 2, 3, 4} {
190 SetCell(cx+d, cy-6, true)
191 SetCell(cx+d, cy-1, true)
192 SetCell(cx+d, cy+1, true)
193 SetCell(cx+d, cy+6, true)
194 SetCell(cx-6, cy+d, true)
195 SetCell(cx-1, cy+d, true)
196 SetCell(cx+1, cy+d, true)
197 SetCell(cx+6, cy+d, true)
198 }
199
200 default:
201 LoadPreset("glider")
202 }
203}
204
205// Render returns a Markdown snapshot of the current board.
206func Render(path string) string {
207 out := "# Conway's Game of Life\n\n"
208 out += "> A classic cellular automaton, ported to Gno — board state lives on-chain.\n\n"
209 out += "**Generation:** " + strconv.Itoa(generation)
210 out += " | **Population:** " + strconv.Itoa(population)
211 out += " | **Board:** " + strconv.Itoa(width) + "×" + strconv.Itoa(height) + "\n\n"
212
213 border := "+" + strings.Repeat("-", width) + "+\n"
214 out += "```\n"
215 out += border
216 for y := 0; y < height; y++ {
217 row := "|"
218 for x := 0; x < width; x++ {
219 if alive(x, y) {
220 row += "O"
221 } else {
222 row += "."
223 }
224 }
225 row += "|\n"
226 out += row
227 }
228 out += border
229 out += "```\n\n"
230
231 out += "## Transactions\n\n"
232 out += "| Call | Effect |\n"
233 out += "|---|---|\n"
234 out += "| `Step()` | Advance one generation |\n"
235 out += "| `MultiStep(n)` | Advance *n* generations (max 100) |\n"
236 out += "| `SetCell(x, y, alive)` | Flip a single cell |\n"
237 out += "| `Reset(w, h)` | Clear and resize (max " + strconv.Itoa(MaxWidth) + "×" + strconv.Itoa(MaxHeight) + ") |\n"
238 out += "| `LoadPreset(name)` | `glider` · `blinker` · `toad` · `beacon` · `block` · `pulsar` |\n\n"
239
240 out += "## Rules\n\n"
241 out += "1. A live cell with 2 or 3 live neighbours survives.\n"
242 out += "2. A dead cell with exactly 3 live neighbours becomes alive.\n"
243 out += "3. All other cells die or remain dead.\n"
244
245 return out
246}