games

package
v0.1.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 1, 2026 License: MIT Imports: 12 Imported by: 0

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

View Source
const DefaultRating = 1500.0

DefaultRating is the ELO a player starts at before their first rated match.

View Source
const (
	Draw = -1 // the game ended with no winner
)

Result codes for a terminal position's winner.

Variables

View Source
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.

View Source
var ErrAlreadyQueued = errors.New("player already queued")

ErrAlreadyQueued means the same player already has a connection waiting for this game.

View Source
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.

View Source
var ErrNoOpponent = errors.New("no opponent found")

ErrNoOpponent means nobody else joined the queue before the wait expired.

View Source
var ErrUnknownGame = errors.New("unknown game")

ErrUnknownGame means the requested game id is not in the registry.

Functions

func EloUpdate

func EloUpdate(ra, rb, scoreA float64) (na, nb float64)

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

func Expected(a, b float64) float64

Expected returns the expected score (0..1) for a player rated a against an opponent rated b, per the standard logistic ELO model.

func Outcome

func Outcome(winner, player int) string

Outcome returns the per-player outcome string for a winner code.

func ScoreFor

func ScoreFor(winner, p int) float64

ScoreFor converts a terminal winner into player p's score (1/0.5/0).

Types

type Bot

type Bot interface {
	Name() string
	Move(s State) string
}

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.

func (Connect4) ID

func (Connect4) ID() string

func (Connect4) Start

func (Connect4) Start() State

func (Connect4) Title

func (Connect4) Title() string

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

type GreedyBot struct{ R *rand.Rand }

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.

func (GreedyBot) Move

func (b GreedyBot) Move(s State) string

func (GreedyBot) Name

func (GreedyBot) Name() string

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

func NewJSONLineConn(name string, r io.Reader, w io.Writer) *JSONLineConn

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

func (mm *Matchmaker) Play(ctx context.Context, gameID string, io PlayerIO) error

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 Move

type Move struct {
	Player int    `json:"player"`
	Move   string `json:"move"`
}

Move is one ply in a match, for replay.

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 Catalog

func Catalog() *Registry

Catalog is the v1 game catalog (PRD §5.2 phase 1).

func NewRegistry

func NewRegistry(gs ...Game) *Registry

NewRegistry builds a registry from the given games, preserving order.

func (*Registry) All

func (r *Registry) All() []Game

All returns the games in registration order.

func (*Registry) Get

func (r *Registry) Get(id string) (Game, bool)

Get returns the game with this ID.

func (*Registry) IDs

func (r *Registry) IDs() []string

IDs returns the registered game IDs, sorted.

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.

type TTT

type TTT struct{}

TTT is 3×3 tic-tac-toe. Player 0 is X, player 1 is O. Moves are cell indices "0".."8" in row-major order.

func (TTT) ID

func (TTT) ID() string

func (TTT) Start

func (TTT) Start() State

func (TTT) Title

func (TTT) Title() string

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL