agentbbs/plugins/arcade/board.go
Anthony Ettinger f3b085a08f AgentBBS: M0 core hub, M1 arcade, pods with CoinPay membership
A modern BBS over SSH for humans and AI agents (docs/PRD.md), plus the
pods addendum (docs/pods.md). Go + charmbracelet (wish/bubbletea).

SSH routes by username:
- bbs@/play@   hub as guest
- <name>@      hub as member/agent (key required; one key = one account)
- join@        onboarding: registers the key, prints instructions
               (incl. coinpay pay command with HMAC payment ref), kicks
- pod@         personal Linux container, paid membership $1/mo via
               CoinPay; rootless podman preferred, hardened docker
               fallback (cap-drop ALL, no-new-privileges, uid 1000,
               cpu/mem/pids caps, per-user volume)

M0: plugin contract (ID/Title/Description/RequiresAuth/New + ExitMsg),
hub menu, SQLite store (users/sessions/scores/pod_subscriptions),
session audit, grant-pod ops command.

M1 arcade: doom-ascii + Freedoom via scripts/fetch-assets.sh, sandbox
runner (bwrap/prlimit), PTY-bridged exec with orphan reaping, snake
with global leaderboard, member save dirs + private ~/wads scan.

Verified over real SSH: join/paywall/grant/pod attach + write
persistence across reconnects, guest+member hubs, DOOM launch, no
orphaned processes after hard disconnect.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 11:08:17 +00:00

58 lines
1.3 KiB
Go

package arcade
import (
"fmt"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"github.com/profullstack/agentbbs/internal/plugin"
"github.com/profullstack/agentbbs/internal/store"
)
// board renders the global top scores (PRD §5.1 leaderboards).
type board struct {
ctx plugin.Context
scores []store.Score
err error
}
func newBoard(ctx plugin.Context) *board { return &board{ctx: ctx} }
func (b *board) Init() tea.Cmd {
return func() tea.Msg {
scores, err := b.ctx.Store.TopScores("snake", 10)
return boardMsg{scores: scores, err: err}
}
}
type boardMsg struct {
scores []store.Score
err error
}
func (b *board) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case boardMsg:
b.scores, b.err = msg.scores, msg.err
case tea.KeyMsg:
return b, back
}
return b, nil
}
func (b *board) View() string {
s := tStyle.Render("Leaderboard — snake") + "\n\n"
switch {
case b.err != nil:
s += eStyle.Render("error: " + b.err.Error())
case len(b.scores) == 0:
s += dStyle.Render("no scores yet — be the first")
default:
for i, sc := range b.scores {
s += fmt.Sprintf("%2d. %-20s %6d\n", i+1, sc.User, sc.Score)
}
}
s += "\n" + dStyle.Render("any key to return")
return lipgloss.NewStyle().Padding(1, 2).Render(s)
}