Search Apps Documentation Source Content File Folder Download Copy Actions Download State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

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}