connect4.gno
6.77 Kb Β· 305 lines
1package connect4
2
3import (
4 "chain"
5 "chain/runtime/unsafe"
6 "errors"
7 "strconv"
8 "strings"
9
10 "gno.land/p/nt/avl/v0"
11)
12
13const (
14 cols = 7
15 rows = 6
16)
17
18// cell values
19const (
20 empty = 0
21 red = 1 // π΄ game creator (caller of NewGame)
22 yel = 2 // π‘ opponent
23)
24
25// Game holds the full state of one Connect Four match.
26// board is indexed board[row][col]; row 0 is the BOTTOM row.
27type Game struct {
28 ID int64
29 Red address // π΄ creator, moves first
30 Yellow address // π‘ opponent
31 Board [rows][cols]int
32 Turn int // whose turn: red or yel
33 Winner int // empty until decided; red/yel = winner
34 Draw bool // true when board full with no winner
35 Finished bool
36}
37
38var (
39 games = avl.NewTree() // id (zero-padded string) -> *Game
40 nextID int64
41)
42
43func idKey(id int64) string {
44 // zero-pad so avl iteration order matches numeric order for Render lists
45 s := strconv.FormatInt(id, 10)
46 for len(s) < 12 {
47 s = "0" + s
48 }
49 return s
50}
51
52// NewGame creates a match between the caller (π΄) and opponent (π‘).
53// Returns the new game id.
54func NewGame(cur realm, opponent address) int64 {
55 caller := unsafe.PreviousRealm().Address()
56 if opponent == caller {
57 panic("opponent must differ from caller")
58 }
59 if opponent.String() == "" {
60 panic("opponent address is empty")
61 }
62 nextID++
63 g := &Game{
64 ID: nextID,
65 Red: caller,
66 Yellow: opponent,
67 Turn: red,
68 }
69 games.Set(idKey(g.ID), g)
70 chain.Emit("GameCreated",
71 "id", strconv.FormatInt(g.ID, 10),
72 "red", caller.String(),
73 "yellow", opponent.String(),
74 )
75 return g.ID
76}
77
78func getGame(id int64) (*Game, error) {
79 v, ok := games.Get(idKey(id))
80 if !ok {
81 return nil, errors.New("game not found: " + strconv.FormatInt(id, 10))
82 }
83 return v.(*Game), nil
84}
85
86// Drop places the caller's disc into the given column (0-6).
87// Enforces turn order by caller, rejects full columns, and detects
88// a 4-in-a-row win or a draw.
89func Drop(cur realm, gameID int64, column int) {
90 if column < 0 || column >= cols {
91 panic("column out of range (0-6)")
92 }
93 g, err := getGame(gameID)
94 if err != nil {
95 panic(err.Error())
96 }
97 if g.Finished {
98 panic("game already finished")
99 }
100
101 caller := unsafe.PreviousRealm().Address()
102 var mover int
103 switch caller {
104 case g.Red:
105 mover = red
106 case g.Yellow:
107 mover = yel
108 default:
109 panic("caller is not a player in this game")
110 }
111 if mover != g.Turn {
112 panic("not your turn")
113 }
114
115 // find lowest empty row in the column
116 placed := -1
117 for r := 0; r < rows; r++ {
118 if g.Board[r][column] == empty {
119 g.Board[r][column] = mover
120 placed = r
121 break
122 }
123 }
124 if placed == -1 {
125 panic("column is full")
126 }
127
128 if wins(&g.Board, placed, column, mover) {
129 g.Winner = mover
130 g.Finished = true
131 chain.Emit("GameWon",
132 "id", strconv.FormatInt(g.ID, 10),
133 "winner", caller.String(),
134 )
135 } else if full(&g.Board) {
136 g.Draw = true
137 g.Finished = true
138 chain.Emit("GameDraw", "id", strconv.FormatInt(g.ID, 10))
139 } else {
140 if g.Turn == red {
141 g.Turn = yel
142 } else {
143 g.Turn = red
144 }
145 chain.Emit("DiscDropped",
146 "id", strconv.FormatInt(g.ID, 10),
147 "col", strconv.Itoa(column),
148 "player", caller.String(),
149 )
150 }
151
152 games.Set(idKey(g.ID), g)
153}
154
155func full(b *[rows][cols]int) bool {
156 for c := 0; c < cols; c++ {
157 if b[rows-1][c] == empty {
158 return false
159 }
160 }
161 return true
162}
163
164// wins checks whether the disc just placed at (r,c) for player p completes
165// a run of 4 in any of the four directions.
166func wins(b *[rows][cols]int, r, c, p int) bool {
167 // direction pairs: horizontal, vertical, diag /, diag \
168 dirs := [4][2]int{{0, 1}, {1, 0}, {1, 1}, {1, -1}}
169 for _, d := range dirs {
170 count := 1
171 count += run(b, r, c, d[0], d[1], p)
172 count += run(b, r, c, -d[0], -d[1], p)
173 if count >= 4 {
174 return true
175 }
176 }
177 return false
178}
179
180func run(b *[rows][cols]int, r, c, dr, dc, p int) int {
181 n := 0
182 for i := 1; i < 4; i++ {
183 rr := r + dr*i
184 cc := c + dc*i
185 if rr < 0 || rr >= rows || cc < 0 || cc >= cols {
186 break
187 }
188 if b[rr][cc] != p {
189 break
190 }
191 n++
192 }
193 return n
194}
195
196func glyph(v int) string {
197 switch v {
198 case red:
199 return "π΄"
200 case yel:
201 return "π‘"
202 default:
203 return "Β·"
204 }
205}
206
207// Render draws the board with π΄π‘Β· and shows whose turn / the winner.
208// path "" lists all games; path "<id>" shows a single board.
209func Render(path string) string {
210 path = strings.TrimSpace(strings.Trim(path, "/"))
211 if path == "" {
212 return renderList()
213 }
214 id, err := strconv.ParseInt(path, 10, 64)
215 if err != nil {
216 return "# Connect Four\n\nInvalid game id: `" + path + "`\n"
217 }
218 g, err := getGame(id)
219 if err != nil {
220 return "# Connect Four\n\n" + err.Error() + "\n"
221 }
222 return renderGame(g)
223}
224
225func renderList() string {
226 var sb strings.Builder
227 sb.WriteString("# Connect Four\n\n")
228 sb.WriteString("Two-player Connect Four on a 7Γ6 board. π΄ is the game creator, π‘ the opponent.\n\n")
229 if games.Size() == 0 {
230 sb.WriteString("_No games yet. Call `NewGame(opponent)` to start one._\n")
231 return sb.String()
232 }
233 sb.WriteString("## Games\n\n")
234 sb.WriteString("| ID | π΄ Red | π‘ Yellow | Status |\n")
235 sb.WriteString("|----|--------|-----------|--------|\n")
236 games.Iterate("", "", func(_ string, v any) bool {
237 g := v.(*Game)
238 status := ""
239 switch {
240 case g.Winner == red:
241 status = "π΄ won"
242 case g.Winner == yel:
243 status = "π‘ won"
244 case g.Draw:
245 status = "draw"
246 case g.Turn == red:
247 status = "π΄ to move"
248 default:
249 status = "π‘ to move"
250 }
251 sb.WriteString("| [" + strconv.FormatInt(g.ID, 10) + "](/r:" + strconv.FormatInt(g.ID, 10) + ") | " +
252 short(g.Red) + " | " + short(g.Yellow) + " | " + status + " |\n")
253 return false
254 })
255 sb.WriteString("\nOpen a game by its id (e.g. path `1`).\n")
256 return sb.String()
257}
258
259func short(a address) string {
260 s := a.String()
261 if len(s) <= 12 {
262 return "`" + s + "`"
263 }
264 return "`" + s[:8] + "β¦" + s[len(s)-4:] + "`"
265}
266
267func renderGame(g *Game) string {
268 var sb strings.Builder
269 sb.WriteString("# Connect Four β Game " + strconv.FormatInt(g.ID, 10) + "\n\n")
270 sb.WriteString("- π΄ Red: `" + g.Red.String() + "`\n")
271 sb.WriteString("- π‘ Yellow: `" + g.Yellow.String() + "`\n\n")
272
273 // status line
274 switch {
275 case g.Winner == red:
276 sb.WriteString("**π΄ Red wins!**\n\n")
277 case g.Winner == yel:
278 sb.WriteString("**π‘ Yellow wins!**\n\n")
279 case g.Draw:
280 sb.WriteString("**Draw β board full.**\n\n")
281 case g.Turn == red:
282 sb.WriteString("**Turn: π΄ Red**\n\n")
283 default:
284 sb.WriteString("**Turn: π‘ Yellow**\n\n")
285 }
286
287 // board top-down (row rows-1 at top, row 0 at bottom)
288 sb.WriteString("```\n")
289 for r := rows - 1; r >= 0; r-- {
290 for c := 0; c < cols; c++ {
291 sb.WriteString(glyph(g.Board[r][c]))
292 }
293 sb.WriteString("\n")
294 }
295 // column indices
296 for c := 0; c < cols; c++ {
297 sb.WriteString(strconv.Itoa(c))
298 }
299 sb.WriteString("\n```\n\n")
300
301 if !g.Finished {
302 sb.WriteString("Drop a disc: `Drop(" + strconv.FormatInt(g.ID, 10) + ", <col 0-6>)`\n")
303 }
304 return sb.String()
305}