tictactoe.gno
3.55 Kb · 168 lines
1// Package tictactoe is a two-player tic-tac-toe realm. The first caller to
2// Join becomes X, the second becomes O; players then alternate Move calls by
3// cell index (0-8, left-to-right top-to-bottom) until someone wins or the
4// board fills. Reset starts a fresh game once the current one has ended.
5package tictactoe
6
7import (
8 "strconv"
9
10 "chain"
11)
12
13var (
14 zero address
15
16 board [9]string // "" empty, "X", "O"
17 playerX address
18 playerO address
19 turn string // "X" or "O", whose move is next
20 winner string // "", "X", "O", or "Draw"
21 moves int
22)
23
24func init() {
25 turn = "X"
26}
27
28// Join seats the caller as X (if the seat is open) or O (if X is taken).
29// Panics if both seats are already filled.
30func Join(cur realm) string {
31 caller := cur.Previous().Address()
32 switch {
33 case playerX == zero:
34 playerX = caller
35 chain.Emit("Join", "player", caller.String(), "mark", "X")
36 return "joined as X"
37 case playerO == zero:
38 if caller == playerX {
39 panic("already joined as X")
40 }
41 playerO = caller
42 chain.Emit("Join", "player", caller.String(), "mark", "O")
43 return "joined as O"
44 default:
45 panic("game is full")
46 }
47}
48
49// Move places the caller's mark at the given cell (0-8) if it is their turn
50// and the cell is empty, then checks for a win or a draw.
51func Move(cur realm, pos int) {
52 if winner != "" {
53 panic("game already over, call Reset to start a new one")
54 }
55 if pos < 0 || pos > 8 {
56 panic("position must be between 0 and 8")
57 }
58 if board[pos] != "" {
59 panic("cell already occupied")
60 }
61
62 var expected address
63 if turn == "X" {
64 expected = playerX
65 } else {
66 expected = playerO
67 }
68 if expected == zero {
69 panic("waiting for both players to join")
70 }
71
72 caller := cur.Previous().Address()
73 if caller != expected {
74 panic("not your turn")
75 }
76
77 board[pos] = turn
78 moves++
79 chain.Emit("Move", "player", caller.String(), "mark", turn, "pos", strconv.Itoa(pos))
80
81 if checkWin(turn) {
82 winner = turn
83 chain.Emit("GameOver", "result", winner)
84 } else if moves == 9 {
85 winner = "Draw"
86 chain.Emit("GameOver", "result", winner)
87 } else if turn == "X" {
88 turn = "O"
89 } else {
90 turn = "X"
91 }
92}
93
94// Reset clears the board for a new game. Only callable once the current
95// game has ended (someone won or the board is full).
96func Reset(cur realm) {
97 if winner == "" {
98 panic("game still in progress")
99 }
100 board = [9]string{}
101 playerX = zero
102 playerO = zero
103 turn = "X"
104 winner = ""
105 moves = 0
106 chain.Emit("Reset")
107}
108
109var winLines = [8][3]int{
110 {0, 1, 2}, {3, 4, 5}, {6, 7, 8}, // rows
111 {0, 3, 6}, {1, 4, 7}, {2, 5, 8}, // columns
112 {0, 4, 8}, {2, 4, 6}, // diagonals
113}
114
115func checkWin(mark string) bool {
116 for _, line := range winLines {
117 if board[line[0]] == mark && board[line[1]] == mark && board[line[2]] == mark {
118 return true
119 }
120 }
121 return false
122}
123
124func cell(pos int) string {
125 if board[pos] == "" {
126 return strconv.Itoa(pos)
127 }
128 return board[pos]
129}
130
131// Render draws the current board and game status as markdown.
132func Render(path string) string {
133 out := "# Tic-Tac-Toe\n\n"
134 out += "| | | |\n"
135 out += "|---|---|---|\n"
136 for row := 0; row < 3; row++ {
137 out += "| "
138 for col := 0; col < 3; col++ {
139 out += cell(row*3+col) + " | "
140 }
141 out += "\n"
142 }
143 out += "\n"
144
145 switch {
146 case winner == "Draw":
147 out += "**Game over: draw.**\n"
148 case winner != "":
149 out += "**Game over: " + winner + " wins!**\n"
150 default:
151 out += "Turn: **" + turn + "**\n"
152 }
153
154 out += "\nPlayer X: "
155 if playerX == zero {
156 out += "_(open)_"
157 } else {
158 out += playerX.String()
159 }
160 out += "\n\nPlayer O: "
161 if playerO == zero {
162 out += "_(open)_"
163 } else {
164 out += playerO.String()
165 }
166 out += "\n"
167 return out
168}