mirror of
https://github.com/profullstack/agentbbs.git
synced 2026-08-13 22:37:28 +00:00
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:
parent
ff9eef907d
commit
b51fa5c0e6
6 changed files with 947 additions and 0 deletions
104
plugins/arcade/shedsnake_test.go
Normal file
104
plugins/arcade/shedsnake_test.go
Normal 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")
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue