tictactoe.gno
4.13 Kb · 216 lines
1package tictactoe
2
3import (
4 "strconv"
5
6 "chain"
7 "gno.land/p/nt/avl/v0"
8)
9
10type Cell int
11
12const (
13 Empty Cell = 0
14 X Cell = 1
15 O Cell = 2
16)
17
18type Game struct {
19 ID int
20 Board [9]Cell
21 PlayerX address
22 PlayerO address
23 Turn Cell
24 Winner Cell
25 Draw bool
26}
27
28var (
29 games = avl.NewTree()
30 gameCount int
31)
32
33// NewGame creates a new game where the caller plays X and opponent plays O.
34func NewGame(cur realm, opponent address) int {
35 if !cur.IsCurrent() {
36 panic("spoofed realm")
37 }
38 caller := cur.Previous().Address()
39 gameCount++
40 id := gameCount
41 g := &Game{
42 ID: id,
43 PlayerX: caller,
44 PlayerO: opponent,
45 Turn: X,
46 }
47 games.Set(strconv.Itoa(id), g)
48 chain.Emit("GameCreated",
49 "id", strconv.Itoa(id),
50 "playerX", caller.String(),
51 "playerO", opponent.String(),
52 )
53 return id
54}
55
56// Move places the caller's mark at position pos (0-8, row-major).
57func Move(cur realm, gameID int, pos int) {
58 if !cur.IsCurrent() {
59 panic("spoofed realm")
60 }
61 caller := cur.Previous().Address()
62 g := mustGetGame(gameID)
63 if g.Winner != Empty || g.Draw {
64 panic("game is over")
65 }
66 if g.Turn == X && caller != g.PlayerX {
67 panic("not your turn")
68 }
69 if g.Turn == O && caller != g.PlayerO {
70 panic("not your turn")
71 }
72 if pos < 0 || pos > 8 {
73 panic("position out of range [0-8]")
74 }
75 if g.Board[pos] != Empty {
76 panic("position already taken")
77 }
78 g.Board[pos] = g.Turn
79 if checkWin(g.Board, g.Turn) {
80 g.Winner = g.Turn
81 chain.Emit("GameOver",
82 "id", strconv.Itoa(gameID),
83 "winner", cellStr(g.Turn),
84 )
85 } else if checkDraw(g.Board) {
86 g.Draw = true
87 chain.Emit("GameOver",
88 "id", strconv.Itoa(gameID),
89 "winner", "draw",
90 )
91 } else {
92 if g.Turn == X {
93 g.Turn = O
94 } else {
95 g.Turn = X
96 }
97 chain.Emit("Move",
98 "id", strconv.Itoa(gameID),
99 "player", caller.String(),
100 "pos", strconv.Itoa(pos),
101 )
102 }
103}
104
105func mustGetGame(id int) *Game {
106 v, ok := games.Get(strconv.Itoa(id))
107 if !ok {
108 panic("game not found")
109 }
110 return v.(*Game)
111}
112
113func checkWin(board [9]Cell, player Cell) bool {
114 lines := [8][3]int{
115 {0, 1, 2}, {3, 4, 5}, {6, 7, 8},
116 {0, 3, 6}, {1, 4, 7}, {2, 5, 8},
117 {0, 4, 8}, {2, 4, 6},
118 }
119 for _, l := range lines {
120 if board[l[0]] == player && board[l[1]] == player && board[l[2]] == player {
121 return true
122 }
123 }
124 return false
125}
126
127func checkDraw(board [9]Cell) bool {
128 for _, c := range board {
129 if c == Empty {
130 return false
131 }
132 }
133 return true
134}
135
136func cellStr(c Cell) string {
137 switch c {
138 case X:
139 return "X"
140 case O:
141 return "O"
142 default:
143 return "."
144 }
145}
146
147func renderBoard(board [9]Cell) string {
148 out := ""
149 for i, c := range board {
150 out += cellStr(c)
151 if i%3 < 2 {
152 out += "|"
153 } else if i < 8 {
154 out += "\n-+-+-\n"
155 }
156 }
157 return out
158}
159
160func Render(path string) string {
161 if path == "" {
162 return renderHome()
163 }
164 id, err := strconv.Atoi(path)
165 if err != nil {
166 return "> [!WARNING]\n> Invalid game ID\n"
167 }
168 v, ok := games.Get(strconv.Itoa(id))
169 if !ok {
170 return "> [!WARNING]\n> Game not found\n"
171 }
172 return renderGame(v.(*Game))
173}
174
175func renderHome() string {
176 out := "# Tic Tac Toe\n\n"
177 out += "**Total games:** " + strconv.Itoa(gameCount) + "\n\n"
178 if gameCount == 0 {
179 out += "No games yet. Call `NewGame(opponent)` to start!\n"
180 return out
181 }
182 out += "## Games\n\n"
183 count := 0
184 games.Iterate("", "", func(key string, value any) bool {
185 g := value.(*Game)
186 out += "- [Game #" + strconv.Itoa(g.ID) + "](" + strconv.Itoa(g.ID) + ")"
187 if g.Winner != Empty {
188 out += " — Winner: **" + cellStr(g.Winner) + "**"
189 } else if g.Draw {
190 out += " — **Draw**"
191 } else {
192 out += " — in progress, turn: **" + cellStr(g.Turn) + "**"
193 }
194 out += "\n"
195 count++
196 return count >= 20
197 })
198 return out
199}
200
201func renderGame(g *Game) string {
202 out := "# Game #" + strconv.Itoa(g.ID) + "\n\n"
203 out += "| Player | Address |\n"
204 out += "| --- | --- |\n"
205 out += "| **X** | " + g.PlayerX.String() + " |\n"
206 out += "| **O** | " + g.PlayerO.String() + " |\n\n"
207 out += "```\n" + renderBoard(g.Board) + "\n```\n\n"
208 if g.Winner != Empty {
209 out += "## Result\n\n**Winner: " + cellStr(g.Winner) + "** \n"
210 } else if g.Draw {
211 out += "## Result\n\n**Draw!**\n"
212 } else {
213 out += "**Next turn: " + cellStr(g.Turn) + "**\n"
214 }
215 return out
216}