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

5.13 Kb · 226 lines
  1package tictactoe
  2
  3import (
  4	"chain"
  5	"chain/runtime/unsafe"
  6	"errors"
  7	"strconv"
  8	"strings"
  9
 10	"gno.land/p/moul/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: " + p + "\n"
182	}
183	g, gerr := GetGame(id)
184	if gerr != nil {
185		return "# Tic-Tac-Toe\n\nGame " + p + " 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}