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

Community realm. Read the code first. Community realm, deployed by its author. Read the code before you interact or send coins.

tictactoe.gno

5.17 Kb · 226 lines
  1package tictactoe
  2
  3import (
  4	"chain"
  5	"chain/runtime/unsafe"
  6	"errors"
  7	"strconv"
  8	"strings"
  9
 10	"gno.land/p/g1magld5ptgzaktqcexxq0pgq5lmdf0kz6re8hlv/kit/store/v0"
 11)
 12
 13// Cell states.
 14const (
 15	empty = 0
 16	markX = 1
 17	markO = 2
 18)
 19
 20// Game holds a single tic-tac-toe match. It carries no id field: the id
 21// belongs to the store, which hands it back on lookup and iteration.
 22type Game struct {
 23	playerX  address // caller of NewGame
 24	playerO  address // opponent
 25	board    [9]int  // 0 empty, 1 X, 2 O
 26	moves    int     // number of moves played
 27	turn     int     // markX or markO — whose turn it is
 28	winner   int     // 0 none, 1 X, 2 O
 29	draw     bool
 30	finished bool
 31}
 32
 33// games is keyed by store.ID, whose avl key is fixed-width by construction.
 34// v0 kept its own nextID plus a pad() that zero-padded to width 12, which
 35// stopped ordering correctly at the 10^12th game.
 36var games = store.Named("game")
 37
 38// NewGame creates a game. Caller is X, opponent is O. Returns the game id.
 39func NewGame(cur realm, opponent address) int {
 40	caller := unsafe.PreviousRealm().Address()
 41	if opponent == caller {
 42		panic("opponent must differ from caller")
 43	}
 44	if !opponent.IsValid() {
 45		panic("invalid opponent address")
 46	}
 47	id := int(games.Add(&Game{
 48		playerX: caller,
 49		playerO: opponent,
 50		turn:    markX,
 51	}))
 52	chain.Emit("GameCreated", "id", strconv.Itoa(id), "x", caller.String(), "o", opponent.String())
 53	return id
 54}
 55
 56// Move plays cell (0-8) for the calling player. Enforces turn order and rejects taken cells.
 57func Move(cur realm, gameID int, cell int) {
 58	g := games.MustGet(store.ID(gameID)).(*Game)
 59	if g.finished {
 60		panic("game already finished")
 61	}
 62	if cell < 0 || cell > 8 {
 63		panic("cell out of range (0-8)")
 64	}
 65	if g.board[cell] != empty {
 66		panic("cell already taken")
 67	}
 68	caller := unsafe.PreviousRealm().Address()
 69
 70	var mark int
 71	switch caller {
 72	case g.playerX:
 73		mark = markX
 74	case g.playerO:
 75		mark = markO
 76	default:
 77		panic("caller is not a player in this game")
 78	}
 79	if mark != g.turn {
 80		panic("not your turn")
 81	}
 82
 83	g.board[cell] = mark
 84	g.moves++
 85
 86	if won(g.board, mark) {
 87		g.winner = mark
 88		g.finished = true
 89		chain.Emit("GameWon", "id", strconv.Itoa(gameID), "winner", markName(mark))
 90	} else if g.moves == 9 {
 91		g.draw = true
 92		g.finished = true
 93		chain.Emit("GameDraw", "id", strconv.Itoa(gameID))
 94	} else {
 95		if g.turn == markX {
 96			g.turn = markO
 97		} else {
 98			g.turn = markX
 99		}
100		chain.Emit("Move", "id", strconv.Itoa(gameID), "cell", strconv.Itoa(cell), "mark", markName(mark))
101	}
102}
103
104var lines = [8][3]int{
105	{0, 1, 2}, {3, 4, 5}, {6, 7, 8}, // rows
106	{0, 3, 6}, {1, 4, 7}, {2, 5, 8}, // cols
107	{0, 4, 8}, {2, 4, 6}, // diagonals
108}
109
110func won(b [9]int, mark int) bool {
111	for _, l := range lines {
112		if b[l[0]] == mark && b[l[1]] == mark && b[l[2]] == mark {
113			return true
114		}
115	}
116	return false
117}
118
119func markName(m int) string {
120	switch m {
121	case markX:
122		return "X"
123	case markO:
124		return "O"
125	default:
126		return "-"
127	}
128}
129
130func cellGlyph(m int) string {
131	switch m {
132	case markX:
133		return "X"
134	case markO:
135		return "O"
136	default:
137		return "·"
138	}
139}
140
141// GetGame returns a game by id (read helper for tests / callers).
142func GetGame(id int) (*Game, error) {
143	v, ok := games.Get(store.ID(id))
144	if !ok {
145		return nil, errors.New("game not found")
146	}
147	return v.(*Game), nil
148}
149
150func renderBoard(b [9]int) string {
151	var sb strings.Builder
152	for r := 0; r < 3; r++ {
153		sb.WriteString("| ")
154		for c := 0; c < 3; c++ {
155			sb.WriteString(cellGlyph(b[r*3+c]))
156			sb.WriteString(" | ")
157		}
158		sb.WriteString("\n")
159	}
160	return sb.String()
161}
162
163func gameStatus(g *Game) string {
164	if g.finished {
165		if g.draw {
166			return "Draw."
167		}
168		return "Winner: " + markName(g.winner)
169	}
170	return "Turn: " + markName(g.turn)
171}
172
173// Render draws the list of games, or a single game board at path "/<id>".
174func Render(path string) string {
175	p := strings.TrimPrefix(path, "/")
176	if p == "" {
177		return renderList()
178	}
179	id, err := strconv.Atoi(p)
180	if err != nil {
181		return "# Tic-Tac-Toe\n\nInvalid game id.\n"
182	}
183	g, gerr := GetGame(id)
184	if gerr != nil {
185		return "# Tic-Tac-Toe\n\nGame " + strconv.Itoa(id) + " not found.\n"
186	}
187	var sb strings.Builder
188	sb.WriteString("# Tic-Tac-Toe — Game #")
189	sb.WriteString(strconv.Itoa(id))
190	sb.WriteString("\n\n")
191	sb.WriteString("- X: `")
192	sb.WriteString(g.playerX.String())
193	sb.WriteString("`\n- O: `")
194	sb.WriteString(g.playerO.String())
195	sb.WriteString("`\n\n")
196	sb.WriteString(renderBoard(g.board))
197	sb.WriteString("\n**")
198	sb.WriteString(gameStatus(g))
199	sb.WriteString("**\n\nCell indices:\n\n")
200	sb.WriteString("| 0 | 1 | 2 |\n| 3 | 4 | 5 |\n| 6 | 7 | 8 |\n")
201	return sb.String()
202}
203
204func renderList() string {
205	var sb strings.Builder
206	sb.WriteString("# Tic-Tac-Toe\n\n")
207	sb.WriteString("2-player tic-tac-toe. Call `NewGame(opponent)` to start; view a game at `/<id>`.\n\n")
208	if games.Len() == 0 {
209		sb.WriteString("_No games yet._\n")
210		return sb.String()
211	}
212	sb.WriteString("| # | X | O | Status |\n|---|---|---|--------|\n")
213	games.Each(func(id store.ID, value any) {
214		g := value.(*Game)
215		sb.WriteString("| ")
216		sb.WriteString(id.String())
217		sb.WriteString(" | `")
218		sb.WriteString(g.playerX.String())
219		sb.WriteString("` | `")
220		sb.WriteString(g.playerO.String())
221		sb.WriteString("` | ")
222		sb.WriteString(gameStatus(g))
223		sb.WriteString(" |\n")
224	})
225	return sb.String()
226}