tictactoe.gno
5.13 Kb · 226 lines
1package tictactoe
2
3import (
4 "chain"
5 "chain/runtime/unsafe"
6 "errors"
7 "strconv"
8 "strings"
9
10 "gno.land/p/moul/kit/store/v0"
11)
12
13// Cell states.
14const (
15 empty = 0
16 markX = 1
17 markO = 2
18)
19
20// Game holds a single tic-tac-toe match. It carries no id field: the id
21// belongs to the store, which hands it back on lookup and iteration.
22type Game struct {
23 playerX address // caller of NewGame
24 playerO address // opponent
25 board [9]int // 0 empty, 1 X, 2 O
26 moves int // number of moves played
27 turn int // markX or markO — whose turn it is
28 winner int // 0 none, 1 X, 2 O
29 draw bool
30 finished bool
31}
32
33// games is keyed by store.ID, whose avl key is fixed-width by construction.
34// v0 kept its own nextID plus a pad() that zero-padded to width 12, which
35// stopped ordering correctly at the 10^12th game.
36var games = store.Named("game")
37
38// NewGame creates a game. Caller is X, opponent is O. Returns the game id.
39func NewGame(cur realm, opponent address) int {
40 caller := unsafe.PreviousRealm().Address()
41 if opponent == caller {
42 panic("opponent must differ from caller")
43 }
44 if !opponent.IsValid() {
45 panic("invalid opponent address")
46 }
47 id := int(games.Add(&Game{
48 playerX: caller,
49 playerO: opponent,
50 turn: markX,
51 }))
52 chain.Emit("GameCreated", "id", strconv.Itoa(id), "x", caller.String(), "o", opponent.String())
53 return id
54}
55
56// Move plays cell (0-8) for the calling player. Enforces turn order and rejects taken cells.
57func Move(cur realm, gameID int, cell int) {
58 g := games.MustGet(store.ID(gameID)).(*Game)
59 if g.finished {
60 panic("game already finished")
61 }
62 if cell < 0 || cell > 8 {
63 panic("cell out of range (0-8)")
64 }
65 if g.board[cell] != empty {
66 panic("cell already taken")
67 }
68 caller := unsafe.PreviousRealm().Address()
69
70 var mark int
71 switch caller {
72 case g.playerX:
73 mark = markX
74 case g.playerO:
75 mark = markO
76 default:
77 panic("caller is not a player in this game")
78 }
79 if mark != g.turn {
80 panic("not your turn")
81 }
82
83 g.board[cell] = mark
84 g.moves++
85
86 if won(g.board, mark) {
87 g.winner = mark
88 g.finished = true
89 chain.Emit("GameWon", "id", strconv.Itoa(gameID), "winner", markName(mark))
90 } else if g.moves == 9 {
91 g.draw = true
92 g.finished = true
93 chain.Emit("GameDraw", "id", strconv.Itoa(gameID))
94 } else {
95 if g.turn == markX {
96 g.turn = markO
97 } else {
98 g.turn = markX
99 }
100 chain.Emit("Move", "id", strconv.Itoa(gameID), "cell", strconv.Itoa(cell), "mark", markName(mark))
101 }
102}
103
104var lines = [8][3]int{
105 {0, 1, 2}, {3, 4, 5}, {6, 7, 8}, // rows
106 {0, 3, 6}, {1, 4, 7}, {2, 5, 8}, // cols
107 {0, 4, 8}, {2, 4, 6}, // diagonals
108}
109
110func won(b [9]int, mark int) bool {
111 for _, l := range lines {
112 if b[l[0]] == mark && b[l[1]] == mark && b[l[2]] == mark {
113 return true
114 }
115 }
116 return false
117}
118
119func markName(m int) string {
120 switch m {
121 case markX:
122 return "X"
123 case markO:
124 return "O"
125 default:
126 return "-"
127 }
128}
129
130func cellGlyph(m int) string {
131 switch m {
132 case markX:
133 return "X"
134 case markO:
135 return "O"
136 default:
137 return "·"
138 }
139}
140
141// GetGame returns a game by id (read helper for tests / callers).
142func GetGame(id int) (*Game, error) {
143 v, ok := games.Get(store.ID(id))
144 if !ok {
145 return nil, errors.New("game not found")
146 }
147 return v.(*Game), nil
148}
149
150func renderBoard(b [9]int) string {
151 var sb strings.Builder
152 for r := 0; r < 3; r++ {
153 sb.WriteString("| ")
154 for c := 0; c < 3; c++ {
155 sb.WriteString(cellGlyph(b[r*3+c]))
156 sb.WriteString(" | ")
157 }
158 sb.WriteString("\n")
159 }
160 return sb.String()
161}
162
163func gameStatus(g *Game) string {
164 if g.finished {
165 if g.draw {
166 return "Draw."
167 }
168 return "Winner: " + markName(g.winner)
169 }
170 return "Turn: " + markName(g.turn)
171}
172
173// Render draws the list of games, or a single game board at path "/<id>".
174func Render(path string) string {
175 p := strings.TrimPrefix(path, "/")
176 if p == "" {
177 return renderList()
178 }
179 id, err := strconv.Atoi(p)
180 if err != nil {
181 return "# Tic-Tac-Toe\n\nInvalid game id: " + p + "\n"
182 }
183 g, gerr := GetGame(id)
184 if gerr != nil {
185 return "# Tic-Tac-Toe\n\nGame " + p + " not found.\n"
186 }
187 var sb strings.Builder
188 sb.WriteString("# Tic-Tac-Toe — Game #")
189 sb.WriteString(strconv.Itoa(id))
190 sb.WriteString("\n\n")
191 sb.WriteString("- X: `")
192 sb.WriteString(g.playerX.String())
193 sb.WriteString("`\n- O: `")
194 sb.WriteString(g.playerO.String())
195 sb.WriteString("`\n\n")
196 sb.WriteString(renderBoard(g.board))
197 sb.WriteString("\n**")
198 sb.WriteString(gameStatus(g))
199 sb.WriteString("**\n\nCell indices:\n\n")
200 sb.WriteString("| 0 | 1 | 2 |\n| 3 | 4 | 5 |\n| 6 | 7 | 8 |\n")
201 return sb.String()
202}
203
204func renderList() string {
205 var sb strings.Builder
206 sb.WriteString("# Tic-Tac-Toe\n\n")
207 sb.WriteString("2-player tic-tac-toe. Call `NewGame(opponent)` to start; view a game at `/<id>`.\n\n")
208 if games.Len() == 0 {
209 sb.WriteString("_No games yet._\n")
210 return sb.String()
211 }
212 sb.WriteString("| # | X | O | Status |\n|---|---|---|--------|\n")
213 games.Each(func(id store.ID, value any) {
214 g := value.(*Game)
215 sb.WriteString("| ")
216 sb.WriteString(id.String())
217 sb.WriteString(" | `")
218 sb.WriteString(g.playerX.String())
219 sb.WriteString("` | `")
220 sb.WriteString(g.playerO.String())
221 sb.WriteString("` | ")
222 sb.WriteString(gameStatus(g))
223 sb.WriteString(" |\n")
224 })
225 return sb.String()
226}