Search Apps Documentation Source Content File Folder Download Copy Actions Download

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, ok := games.Get(pad(gameID))
 71	if !ok {
 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, ok := games.Get(pad(id))
160	if !ok {
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}