package tictactoe
import (
"chain"
"chain/runtime/unsafe"
"errors"
"strconv"
"strings"
"gno.land/p/nt/avl/v0"
)
// Cell states.
const (
empty = 0
markX = 1
markO = 2
)
// Game holds a single tic-tac-toe match.
type Game struct {
id int
playerX address // caller of NewGame
playerO address // opponent
board [9]int // 0 empty, 1 X, 2 O
moves int // number of moves played
turn int // markX or markO — whose turn it is
winner int // 0 none, 1 X, 2 O
draw bool
finished bool
}
var (
games = avl.NewTree() // id (padded string) -> *Game
nextID = 1
)
func pad(id int) string {
s := strconv.Itoa(id)
for len(s) < 12 {
s = "0" + s
}
return s
}
// NewGame creates a game. Caller is X, opponent is O. Returns the game id.
func NewGame(cur realm, opponent address) int {
caller := unsafe.PreviousRealm().Address()
if opponent == caller {
panic("opponent must differ from caller")
}
if !opponent.IsValid() {
panic("invalid opponent address")
}
id := nextID
nextID++
g := &Game{
id: id,
playerX: caller,
playerO: opponent,
turn: markX,
}
games.Set(pad(id), g)
chain.Emit("GameCreated", "id", strconv.Itoa(id), "x", caller.String(), "o", opponent.String())
return id
}
// Move plays cell (0-8) for the calling player. Enforces turn order and rejects taken cells.
func Move(cur realm, gameID int, cell int) {
v := games.Get(pad(gameID))
if v == nil {
panic("game not found")
}
g := v.(*Game)
if g.finished {
panic("game already finished")
}
if cell < 0 || cell > 8 {
panic("cell out of range (0-8)")
}
if g.board[cell] != empty {
panic("cell already taken")
}
caller := unsafe.PreviousRealm().Address()
var mark int
switch caller {
case g.playerX:
mark = markX
case g.playerO:
mark = markO
default:
panic("caller is not a player in this game")
}
if mark != g.turn {
panic("not your turn")
}
g.board[cell] = mark
g.moves++
if won(g.board, mark) {
g.winner = mark
g.finished = true
chain.Emit("GameWon", "id", strconv.Itoa(gameID), "winner", markName(mark))
} else if g.moves == 9 {
g.draw = true
g.finished = true
chain.Emit("GameDraw", "id", strconv.Itoa(gameID))
} else {
if g.turn == markX {
g.turn = markO
} else {
g.turn = markX
}
chain.Emit("Move", "id", strconv.Itoa(gameID), "cell", strconv.Itoa(cell), "mark", markName(mark))
}
}
var lines = [8][3]int{
{0, 1, 2}, {3, 4, 5}, {6, 7, 8}, // rows
{0, 3, 6}, {1, 4, 7}, {2, 5, 8}, // cols
{0, 4, 8}, {2, 4, 6}, // diagonals
}
func won(b [9]int, mark int) bool {
for _, l := range lines {
if b[l[0]] == mark && b[l[1]] == mark && b[l[2]] == mark {
return true
}
}
return false
}
func markName(m int) string {
switch m {
case markX:
return "X"
case markO:
return "O"
default:
return "-"
}
}
func cellGlyph(m int) string {
switch m {
case markX:
return "X"
case markO:
return "O"
default:
return "·"
}
}
// GetGame returns a game by id (read helper for tests / callers).
func GetGame(id int) (*Game, error) {
v := games.Get(pad(id))
if v == nil {
return nil, errors.New("game not found")
}
return v.(*Game), nil
}
func renderBoard(b [9]int) string {
var sb strings.Builder
for r := 0; r < 3; r++ {
sb.WriteString("| ")
for c := 0; c < 3; c++ {
sb.WriteString(cellGlyph(b[r*3+c]))
sb.WriteString(" | ")
}
sb.WriteString("\n")
}
return sb.String()
}
func gameStatus(g *Game) string {
if g.finished {
if g.draw {
return "Draw."
}
return "Winner: " + markName(g.winner)
}
return "Turn: " + markName(g.turn)
}
// Render draws the list of games, or a single game board at path "/<id>".
func Render(path string) string {
p := strings.TrimPrefix(path, "/")
if p == "" {
return renderList()
}
id, err := strconv.Atoi(p)
if err != nil {
return "# Tic-Tac-Toe\n\nInvalid game id: " + p + "\n"
}
g, gerr := GetGame(id)
if gerr != nil {
return "# Tic-Tac-Toe\n\nGame " + p + " not found.\n"
}
var sb strings.Builder
sb.WriteString("# Tic-Tac-Toe — Game #")
sb.WriteString(strconv.Itoa(g.id))
sb.WriteString("\n\n")
sb.WriteString("- X: `")
sb.WriteString(g.playerX.String())
sb.WriteString("`\n- O: `")
sb.WriteString(g.playerO.String())
sb.WriteString("`\n\n")
sb.WriteString(renderBoard(g.board))
sb.WriteString("\n**")
sb.WriteString(gameStatus(g))
sb.WriteString("**\n\nCell indices:\n\n")
sb.WriteString("| 0 | 1 | 2 |\n| 3 | 4 | 5 |\n| 6 | 7 | 8 |\n")
return sb.String()
}
func renderList() string {
var sb strings.Builder
sb.WriteString("# Tic-Tac-Toe\n\n")
sb.WriteString("2-player tic-tac-toe. Call `NewGame(opponent)` to start; view a game at `/<id>`.\n\n")
if games.Size() == 0 {
sb.WriteString("_No games yet._\n")
return sb.String()
}
sb.WriteString("| # | X | O | Status |\n|---|---|---|--------|\n")
games.Iterate("", "", func(key string, value interface{}) bool {
g := value.(*Game)
sb.WriteString("| ")
sb.WriteString(strconv.Itoa(g.id))
sb.WriteString(" | `")
sb.WriteString(g.playerX.String())
sb.WriteString("` | `")
sb.WriteString(g.playerO.String())
sb.WriteString("` | ")
sb.WriteString(gameStatus(g))
sb.WriteString(" |\n")
return false
})
return sb.String()
}