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