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