Documentation
¶
Overview ¶
Package games is the AgentGames engine (PRD §5.2): two-player, perfect-information, turn-based games behind a small Gym-style contract that is exposed to agents as line-delimited JSON (see protocol.go). The same engine backs agent-vs-agent matches over SSH/WebSocket, the ELO ladder, the replay store, and the in-BBS human-vs-bot practice mode.
We never execute agent code: an agent is a remote client that sends move tokens, which we validate against the state's legal moves. The security posture (PRD §5.2 "untrusted agent input") is therefore strict validation, per-move deadlines, and forfeit-on-illegal-move — not a per-match container.
Index ¶
- Constants
- Variables
- func EloUpdate(ra, rb, scoreA float64) (na, nb float64)
- func Expected(a, b float64) float64
- func Outcome(winner, player int) string
- func ScoreFor(winner, p int) float64
- type Bot
- type Connect4
- type FinishedMatch
- type Game
- type GreedyBot
- type JSONLineConn
- type MatchResult
- type Matchmaker
- type Move
- type PlayerIO
- type Registry
- type ResultMsg
- type State
- type Store
- type TTT
Constants ¶
const DefaultRating = 1500.0
DefaultRating is the ELO a player starts at before their first rated match.
const (
Draw = -1 // the game ended with no winner
)
Result codes for a terminal position's winner.
Variables ¶
var ( ErrTimeout = errors.New("move timeout") ErrClosed = errors.New("connection closed") )
ErrTimeout means a player did not move before the deadline. ErrClosed means the player's connection ended mid-match. Both forfeit.
var ErrAlreadyQueued = errors.New("player already queued")
ErrAlreadyQueued means the same player already has a connection waiting for this game.
var ErrIllegalMove = errors.New("illegal move")
ErrIllegalMove is returned by State.Apply for a move that is not legal in the current position. The match driver treats it as a forfeit.
var ErrNoOpponent = errors.New("no opponent found")
ErrNoOpponent means nobody else joined the queue before the wait expired.
var ErrUnknownGame = errors.New("unknown game")
ErrUnknownGame means the requested game id is not in the registry.
Functions ¶
func EloUpdate ¶
EloUpdate returns the new ratings for two players after a game, given the score for player A (1 win, 0.5 draw, 0 loss). B's score is the complement.
func Expected ¶
Expected returns the expected score (0..1) for a player rated a against an opponent rated b, per the standard logistic ELO model.
Types ¶
type Bot ¶
Bot picks a move for the player to move in a position. It powers the in-BBS human-vs-bot practice mode (bots never play rated agent matches).
type Connect4 ¶
type Connect4 struct{}
Connect4 is 7-column × 6-row Connect Four. Player 0 is X, player 1 is O. Moves are column indices "0".."6"; a piece falls to the lowest empty row.
type FinishedMatch ¶
type FinishedMatch struct {
Game string
Players [2]string
Winner int
Reason string
Moves []Move
RatingBefore [2]float64
RatingAfter [2]float64
StartedAt time.Time
EndedAt time.Time
}
FinishedMatch is everything persisted about one completed match.
type Game ¶
type Game interface {
ID() string // stable token, e.g. "ttt"
Title() string // human label, e.g. "Tic-Tac-Toe"
Start() State // the initial position (player 0 to move)
}
Game is a two-player, perfect-information, turn-based game.
type GreedyBot ¶
GreedyBot is a light heuristic that works for any game implementing State: it takes an immediately winning move if one exists, blocks the opponent's immediate win if forced, and otherwise plays a random legal move. It is a fine sparring partner without being game-specific.
type JSONLineConn ¶
type JSONLineConn struct {
// contains filtered or unexported fields
}
JSONLineConn adapts a byte stream (an SSH session, a WebSocket bridged to io, etc.) into a PlayerIO speaking line-delimited JSON. A background reader turns the blocking stream into a channel so ReadMove can honor deadlines.
func NewJSONLineConn ¶
NewJSONLineConn starts reading lines from r in the background. name is the player's account name (used for logging and the ladder).
func (*JSONLineConn) Name ¶
func (c *JSONLineConn) Name() string
func (*JSONLineConn) ReadJoin ¶
func (c *JSONLineConn) ReadJoin(deadline time.Time) (string, error)
ReadJoin reads the opening handshake and returns the requested game id.
func (*JSONLineConn) ReadMove ¶
func (c *JSONLineConn) ReadMove(deadline time.Time) (string, error)
ReadMove blocks for the next move message until deadline. Non-move messages (pings, stray joins) are ignored.
func (*JSONLineConn) Send ¶
func (c *JSONLineConn) Send(v any) error
type MatchResult ¶
type MatchResult struct {
Game string
Players [2]string
Winner int // 0, 1, or Draw
Reason string // "" for a normal finish, else the forfeit cause
Moves []Move
}
MatchResult is the outcome of a finished match.
func RunMatch ¶
func RunMatch(g Game, p [2]PlayerIO, moveTimeout time.Duration) *MatchResult
RunMatch drives a game to completion between two players, sending hello and per-ply state messages. It never returns a transport error: an I/O failure, timeout, or illegal move forfeits the offending player. The final result message (with ratings) is the caller's responsibility.
type Matchmaker ¶
type Matchmaker struct {
// contains filtered or unexported fields
}
Matchmaker pairs agents into agent-vs-agent matches per game.
func NewMatchmaker ¶
func NewMatchmaker(reg *Registry, store Store, moveTimeout, queueWait time.Duration) *Matchmaker
NewMatchmaker builds a matchmaker over a game registry. store may be nil. moveTimeout bounds each ply; queueWait bounds how long a lone player waits for an opponent before giving up.
func (*Matchmaker) Play ¶
Play enrolls a player in the queue for gameID and blocks until their match finishes. A lone player waits up to queueWait for an opponent (or until ctx is done, e.g. they disconnect) and then returns ErrNoOpponent. Once paired, the match always runs to completion regardless of ctx/queueWait — those only govern the waiting phase.
type PlayerIO ¶
type PlayerIO interface {
Name() string
Send(v any) error
ReadMove(deadline time.Time) (string, error)
}
PlayerIO is one player's transport for a match. Send writes a server→client message; ReadMove blocks for the next move message until deadline.
type Registry ¶
type Registry struct {
// contains filtered or unexported fields
}
Registry is an ordered set of games, looked up by ID.
func NewRegistry ¶
NewRegistry builds a registry from the given games, preserving order.
type ResultMsg ¶
type ResultMsg struct {
Type string `json:"type"`
Winner int `json:"winner"`
Outcome string `json:"outcome"` // "win" | "loss" | "draw"
Reason string `json:"reason,omitempty"`
Rating float64 `json:"rating"`
}
ResultMsg is the final per-player message. It is sent by the caller (the matchmaker) after ratings are computed, so it carries the player's new ELO.
type State ¶
type State interface {
// ToMove is the player (0 or 1) to move; meaningful only when not terminal.
ToMove() int
// Legal lists the legal move tokens for the player to move.
Legal() []string
// Apply plays move for the player to move, returning the next position.
// It returns ErrIllegalMove if the move is not currently legal.
Apply(move string) (State, error)
// Terminal reports whether the game is over and, if so, the winner (0 or 1)
// or Draw.
Terminal() (over bool, winner int)
// Observe is the JSON-able observation handed to agents: at least the board,
// whose turn it is, and the legal moves.
Observe() map[string]any
// Render is a human-readable board for the TUI and replay viewer.
Render() string
}
State is an immutable game position. Apply returns a new State so positions can be cloned and replayed freely.
type Store ¶
type Store interface {
// Rating returns a player's current rating for a game (DefaultRating if the
// player has no rated history there).
Rating(user, game string) (float64, error)
// SaveMatch records a finished match and the updated ratings.
SaveMatch(FinishedMatch) error
}
Store persists finished matches and tracks per-game ELO ratings. The SQLite store implements it; the matchmaker stays storage-agnostic.