Add himalaya + meli mail clients and Shedding Snake arcade game (#88)

Mail clients (internal/mailclients): launch himalaya and meli as
alternatives to the built-in AgentMail reader, pointed at the member's
mailbox with the same master IMAP / loopback-SMTP creds. They run
host-side (secrets never enter a pod), from a throwaway per-session
config that is deleted on exit; operators can override the config
template and argv via env. Wired into the hub ("Mail · Himalaya/Meli",
locked when the binary is absent) and the mail@ route
(ssh -t mail@host himalaya|meli).

Shedding Snake (plugins/arcade): a molting twist on Snake inspired by
cha.rlie.co/shedding-snake. The snake barely grows — each apple sheds
its whole body as a permanent field of scales you must not bite. Walls
wrap, speed ramps up, scales age fresh-teal → dusty-gray, and molt
counts feed a global "shedsnake" leaderboard. Grace period lets you
slither off a fresh molt.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Anthony Ettinger 2026-07-10 02:30:12 -07:00 committed by GitHub
parent ff9eef907d
commit b51fa5c0e6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 947 additions and 0 deletions

View file

@ -0,0 +1,104 @@
package arcade
import (
"testing"
tea "github.com/charmbracelet/bubbletea"
"github.com/profullstack/agentbbs/internal/auth"
"github.com/profullstack/agentbbs/internal/plugin"
)
func newTestShed() *shedsnake {
s := newShedSnake(auth.User{Kind: auth.Guest}, plugin.Context{}, 0, 0)
return s
}
// step advances one game tick.
func (s *shedsnake) step() *shedsnake {
next, _ := s.Update(shedTickMsg{})
return next.(*shedsnake)
}
func TestShedMoltsOnEat(t *testing.T) {
s := newTestShed()
startLen := len(s.body)
startSpeed := s.speedMS
// Put the apple directly in front of the head so the next tick eats it.
head := s.wrap(pos{s.body[0].x + s.dir.x, s.body[0].y + s.dir.y})
s.food = head
s = s.step()
if s.gen != 1 {
t.Fatalf("expected 1 molt after eating, got %d", s.gen)
}
// The whole body at molt time (startLen cells, straight → all unique) is shed.
if len(s.skin) != startLen {
t.Fatalf("expected %d shed scales, got %d", startLen, len(s.skin))
}
// The cell the snake ate on is part of the shed skin it stands on.
if _, ok := s.skin[head]; !ok {
t.Errorf("head cell %v where it ate should be a scale", head)
}
if len(s.body) != startLen+shedGrow {
t.Errorf("body should grow by %d on molt: got %d want %d", shedGrow, len(s.body), startLen+shedGrow)
}
if s.speedMS >= startSpeed {
t.Errorf("speed should increase (interval shrink) on molt: %d !< %d", s.speedMS, startSpeed)
}
}
func TestShedGracePeriodNoInstantDeath(t *testing.T) {
// After molting, the fresh scales sit under the body; the snake must be able
// to slither off them without dying.
s := newTestShed()
s.food = s.wrap(pos{s.body[0].x + s.dir.x, s.body[0].y + s.dir.y})
s = s.step() // eat + molt
if s.dead {
t.Fatal("died on the tick it molted")
}
// Slither straight for a few ticks over its own fresh scales.
for i := 0; i < len(s.body)+2; i++ {
s.food = pos{-9, -9} // keep food away
s = s.step()
if s.dead {
t.Fatalf("died sliding off fresh molt at tick %d", i)
}
}
}
func TestShedDeathOnBitingOldScale(t *testing.T) {
s := newTestShed()
// Plant an old scale one cell ahead of the head; heading right into it kills.
ahead := s.wrap(pos{s.body[0].x + s.dir.x, s.body[0].y + s.dir.y})
s.skin[ahead] = 0 // an old generation, not under the body
s.food = pos{-9, -9}
s = s.step()
if !s.dead {
t.Fatal("expected death from biting a shed scale")
}
}
func TestShedWraps(t *testing.T) {
s := newTestShed()
// Drive the head to the right edge, then one more step wraps to x=0.
s.body = []pos{{s.w - 1, 3}, {s.w - 2, 3}, {s.w - 3, 3}}
s.dir, s.next = pos{1, 0}, pos{1, 0}
s.food = pos{-9, -9}
s = s.step()
if s.body[0].x != 0 {
t.Fatalf("head should wrap to x=0, got x=%d", s.body[0].x)
}
}
func TestShedNo180(t *testing.T) {
// Pressing the reverse direction must not fold the snake back on itself.
s := newTestShed() // heading right
next, _ := s.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune("a")}) // left
s = next.(*shedsnake)
if s.next == (pos{-1, 0}) {
t.Fatal("180° reversal should be ignored while moving horizontally")
}
}