Search Apps Documentation Source Content File Folder Download Copy Actions Download

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}