tictactoe.gno
3.95 Kb · 203 lines
1// Package tictactoe is a two-player tic-tac-toe realm.
2//
3// Join(cur, "X") / Join(cur, "O") claims a mark; Play(cur, pos) plays
4// cell pos (0-8, row-major); Reset(cur) clears the board once a game
5// has ended. Render shows the current board.
6package tictactoe
7
8import (
9 "strconv"
10
11 "chain"
12)
13
14const (
15 empty byte = 0
16 markX byte = 1
17 markO byte = 2
18)
19
20var (
21 board [9]byte
22 turn byte = markX
23 playerX address
24 playerO address
25 winner byte
26 moveCount int
27 gameOver bool
28)
29
30var zeroAddr address
31
32// Join claims the X or O mark for the calling account. Each mark may
33// only be claimed once per game; call Reset to start a new game.
34func Join(cur realm, mark string) {
35 if !cur.Previous().IsUserCall() {
36 panic("EOA call only")
37 }
38 caller := cur.Previous().Address()
39 switch mark {
40 case "X":
41 if playerX != zeroAddr {
42 panic("X already taken")
43 }
44 playerX = caller
45 case "O":
46 if playerO != zeroAddr {
47 panic("O already taken")
48 }
49 playerO = caller
50 default:
51 panic(`mark must be "X" or "O"`)
52 }
53 chain.Emit("Join", "player", caller.String(), "mark", mark)
54}
55
56// Play places the calling player's mark at pos (0-8, row-major).
57func Play(cur realm, pos int) {
58 if !cur.Previous().IsUserCall() {
59 panic("EOA call only")
60 }
61 if gameOver {
62 panic("game is over, call Reset to start a new one")
63 }
64 if pos < 0 || pos > 8 {
65 panic("position out of range 0-8")
66 }
67 if board[pos] != empty {
68 panic("cell already occupied")
69 }
70
71 caller := cur.Previous().Address()
72 var mark byte
73 if turn == markX {
74 if playerX == zeroAddr {
75 panic("X has not joined yet")
76 }
77 if caller != playerX {
78 panic("not your turn")
79 }
80 mark = markX
81 } else {
82 if playerO == zeroAddr {
83 panic("O has not joined yet")
84 }
85 if caller != playerO {
86 panic("not your turn")
87 }
88 mark = markO
89 }
90
91 board[pos] = mark
92 moveCount++
93 chain.Emit("Play", "player", caller.String(), "pos", strconv.Itoa(pos))
94
95 if w := checkWinner(); w != empty {
96 winner = w
97 gameOver = true
98 chain.Emit("GameOver", "result", markString(w)+" wins")
99 } else if moveCount == 9 {
100 gameOver = true
101 chain.Emit("GameOver", "result", "draw")
102 } else if turn == markX {
103 turn = markO
104 } else {
105 turn = markX
106 }
107}
108
109// Reset clears the board and both player slots. Only a current player
110// may reset, and only after the game has ended.
111func Reset(cur realm) {
112 if !cur.Previous().IsUserCall() {
113 panic("EOA call only")
114 }
115 if !gameOver {
116 panic("game still in progress")
117 }
118 caller := cur.Previous().Address()
119 if caller != playerX && caller != playerO {
120 panic("only a player of the current game may reset it")
121 }
122
123 board = [9]byte{}
124 turn = markX
125 winner = empty
126 moveCount = 0
127 gameOver = false
128 playerX = zeroAddr
129 playerO = zeroAddr
130 chain.Emit("Reset", "by", caller.String())
131}
132
133var winLines = [8][3]int{
134 {0, 1, 2}, {3, 4, 5}, {6, 7, 8},
135 {0, 3, 6}, {1, 4, 7}, {2, 5, 8},
136 {0, 4, 8}, {2, 4, 6},
137}
138
139func checkWinner() byte {
140 for _, l := range winLines {
141 a, b, c := board[l[0]], board[l[1]], board[l[2]]
142 if a != empty && a == b && b == c {
143 return a
144 }
145 }
146 return empty
147}
148
149func markString(m byte) string {
150 switch m {
151 case markX:
152 return "X"
153 case markO:
154 return "O"
155 default:
156 return "_"
157 }
158}
159
160// Render draws the current board and game status as markdown.
161func Render(_ string) string {
162 out := "# Tic-Tac-Toe\n\n"
163
164 out += "```\n"
165 for row := 0; row < 3; row++ {
166 for col := 0; col < 3; col++ {
167 out += markString(board[row*3+col])
168 if col < 2 {
169 out += " | "
170 }
171 }
172 out += "\n"
173 if row < 2 {
174 out += "---------\n"
175 }
176 }
177 out += "```\n\n"
178
179 out += "X: "
180 if playerX == zeroAddr {
181 out += "_open_"
182 } else {
183 out += playerX.String()
184 }
185 out += "\n\nO: "
186 if playerO == zeroAddr {
187 out += "_open_"
188 } else {
189 out += playerO.String()
190 }
191 out += "\n\n"
192
193 switch {
194 case gameOver && winner != empty:
195 out += "**" + markString(winner) + " wins!** Call Reset to play again.\n"
196 case gameOver:
197 out += "**Draw.** Call Reset to play again.\n"
198 default:
199 out += "Turn: **" + markString(turn) + "**\n"
200 }
201
202 return out
203}