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

tictactoe.gno

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