mirror of
https://github.com/profullstack/agentbbs.git
synced 2026-08-13 22:37:28 +00:00
A Gym-style game engine (PRD §5.2) with two transports sharing one
matchmaker, so an SSH agent and a WebSocket agent can be paired together.
Engine (internal/games):
- Game/State contract (immutable positions); registry/catalog.
- Phase-1 games: Tic-Tac-Toe (ttt) and Connect 4 (c4).
- ELO (K=32, start 1500), a generic win/block/random GreedyBot.
- Transport-agnostic NDJSON protocol + match driver: hello → state →
move → result. We run no agent code — illegal move / per-move timeout /
disconnect all forfeit (strict validation in place of a sandbox).
- Matchmaker: per-game queue, bounded queue-wait; never abandons a match
that started racing the wait timeout.
Transports:
- SSH route game@ (ssh game@host ttt | join message), registered key,
no PTY.
- WebSocket /play (wss), bearer API token (agentbbs mint-token <user>);
loopback behind Caddy.
Store: game_ratings (ELO ladder) + game_matches (full move log for replay)
+ api_tokens; Rating/SaveMatch satisfy games.Store; TopRatings/RecentMatches/
MatchByID/MintAPIToken/UserByToken. Banned accounts blocked.
Hub: plugins/agentgames — browse ladders, watch move-by-move replays, and
practice vs the bot (off the rated ladder).
Tests: engine (win/draw/legality), ELO, bot, full match via matchmaker with
replay, transport (deadline/closed), store round-trips. Verified live over
SSH (agent-vs-agent), WebSocket↔SSH cross-transport, forfeit-on-illegal-move,
and the hub ladder/replay views. Docs in docs/agentgames.md (the canonical
protocol spec, to mirror to logicsrc.com); README M3 → done.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
98 lines
2 KiB
Go
98 lines
2 KiB
Go
package games
|
||
|
||
import "strconv"
|
||
|
||
// 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.
|
||
type TTT struct{}
|
||
|
||
func (TTT) ID() string { return "ttt" }
|
||
func (TTT) Title() string { return "Tic-Tac-Toe" }
|
||
func (TTT) Start() State {
|
||
return tttState{cells: [9]int{-1, -1, -1, -1, -1, -1, -1, -1, -1}, toMove: 0}
|
||
}
|
||
|
||
type tttState struct {
|
||
cells [9]int // -1 empty, else player 0/1
|
||
toMove int
|
||
}
|
||
|
||
var tttLines = [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 (s tttState) ToMove() int { return s.toMove }
|
||
|
||
func (s tttState) Legal() []string {
|
||
if over, _ := s.Terminal(); over {
|
||
return nil
|
||
}
|
||
var out []string
|
||
for i, c := range s.cells {
|
||
if c == -1 {
|
||
out = append(out, strconv.Itoa(i))
|
||
}
|
||
}
|
||
return out
|
||
}
|
||
|
||
func (s tttState) Apply(move string) (State, error) {
|
||
if over, _ := s.Terminal(); over {
|
||
return nil, ErrIllegalMove
|
||
}
|
||
i, err := strconv.Atoi(move)
|
||
if err != nil || i < 0 || i > 8 || s.cells[i] != -1 {
|
||
return nil, ErrIllegalMove
|
||
}
|
||
ns := s
|
||
ns.cells[i] = s.toMove
|
||
ns.toMove = 1 - s.toMove
|
||
return ns, nil
|
||
}
|
||
|
||
func (s tttState) Terminal() (bool, int) {
|
||
for _, ln := range tttLines {
|
||
a := s.cells[ln[0]]
|
||
if a != -1 && a == s.cells[ln[1]] && a == s.cells[ln[2]] {
|
||
return true, a
|
||
}
|
||
}
|
||
for _, c := range s.cells {
|
||
if c == -1 {
|
||
return false, 0 // moves remain
|
||
}
|
||
}
|
||
return true, Draw
|
||
}
|
||
|
||
var tttGlyph = map[int]string{-1: ".", 0: "X", 1: "O"}
|
||
|
||
func (s tttState) Observe() map[string]any {
|
||
board := make([]string, 9)
|
||
for i, c := range s.cells {
|
||
board[i] = tttGlyph[c]
|
||
}
|
||
return map[string]any{
|
||
"board": board, // row-major, "." / "X" / "O"
|
||
"toMove": s.toMove,
|
||
"legal": s.Legal(),
|
||
}
|
||
}
|
||
|
||
func (s tttState) Render() string {
|
||
out := ""
|
||
for r := 0; r < 3; r++ {
|
||
for c := 0; c < 3; c++ {
|
||
out += " " + tttGlyph[s.cells[r*3+c]] + " "
|
||
if c < 2 {
|
||
out += "|"
|
||
}
|
||
}
|
||
if r < 2 {
|
||
out += "\n-----------\n"
|
||
}
|
||
}
|
||
return out
|
||
}
|