Files
2026-08-10 16:24:32 -04:00

257 lines
12 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Super Auto Pets: The Board Game — Online
A web app for playing the Super Auto Pets board game remotely, at 2, 4, or 6
players. Play friends by room code, fill any empty seat with a computer
opponent (easy / medium / hard), or play solo against a table of them.
## Stack
- **Backend**: Go (stdlib-first). Pure game engine in `internal/game`,
WebSocket sync via `coder/websocket`, persistence as JSON blobs in SQLite
(`modernc.org/sqlite`, no cgo).
- **Frontend**: React + Vite + TypeScript in `web/`. DOM/CSS card rendering
and battle animations.
- **Tasks**: [mise](https://mise.jdx.dev) (`mise.toml`).
## Quick start
```sh
mise run install-web # once: npm install
mise run serve # build frontend + server, run on :8080
```
Open http://localhost:8080, host a game, and join from another browser (or
incognito window) with the 5-letter code.
### Development
```sh
mise run dev # Go server on :8080 + Vite hot reload on :5173
mise run test # Go tests (game engine)
mise run check # go vet + frontend type-check
```
During development open the Vite URL (http://localhost:5173); it proxies
`/api` (including the WebSocket) to the Go server.
## Configuration
Set via environment or a `.env` file (see `.env.example`):
| Variable | Default | Purpose |
| ------------ | ---------- | ------------------------------------------- |
| `DATA_DIR` | `data` | Directory holding the SQLite DB |
| `PORT` | `8080` | HTTP port |
| `STATIC_DIR` | `web/dist` | Built frontend to serve |
| `DEBUG` | off | Unlocks the in-game buy-any-card panel |
## Debugging a game that went wrong
Any game can be dumped as a **debug report**: the full state (every deck card by
card, the shop row and the order of the remaining tier decks, discards, pending
choices, each player's banked Mana/Trumpets/apples), the round's battles with
their lineups and *the dice they rolled*, and the entire event log.
**In game**, the `⋯` menu has **🐛 Report a bug**: a note field, a
**Download report** button, and **Copy as text**. It works on any server, DEBUG
or not, so a bug hit in a real game can actually be reported. That does hand the
player their opponents' hands and the shop deck order — an accepted trade, since
a report only ever goes to someone seated at that table (`GET
/api/debug/report`, same credentials as the WebSocket).
**From the command line**, against the server's database:
```sh
mise run report # list recent games
mise run report -- QWERT # the report, as text, for reading
mise run report -- -json -out internal/game/testdata/bug.json QWERT
```
The JSON form is the useful one, because it replays. Drop it in
`internal/game/testdata/` and the battle re-runs exactly — same lineups, same
dice, same events, right down to the log text:
```go
rep, err := LoadDebugReportFile("testdata/bug.json")
res, err := rep.ReplayBattle(0) // 0 = the round's first pairing
if res.WinnerSeat != 1 { // assert what *should* have happened
t.Fatalf("expected seat 1 to win, got %d", res.WinnerSeat)
}
```
That works because every battle records the randomness it consumed
(`BattleResult.Draws`) alongside what it started from (`Lineups`, `Inputs`), so
a result is replayable long after the round cleared those banks. `rep.Game()`
hands back the whole game if the bug wasn't in the battle — carry on from the
shop, replay a different pairing, or read a deck card by card. The text form of
the report ends with a paste-ready version of the test above.
## Rules implemented
Six rounds, each with its own shop tier deck. Per round:
1. **Shop** — each player has 3 gold and players alternate actions: **buy**
one of 4 face-up cards (1 gold — the only action that costs anything);
**sell** any number of hand cards for free (each becomes an 🍎 apple, +1
power food, and Sell effects fire); or **trade in** 3 same-suit pets (the
Triple action, also free) to pick 1 of the top 2 cards of the next tier's
deck — Triple effects fire on the traded cards and the received pet's Buy
effect fires. **Passing** is a final action, only legal at 5 pets or
fewer: it forfeits remaining gold and ends that player's shopping for the
round. The shop closes once everyone has passed.
2. **Arrange** — Battle Prep effects fire first (e.g. Giraffe hands out
apples), then players secretly order their decks. Food cards apply to the
next pet after them; trailing foods are wasted. A pet only benefits from
its last-applied **perk** (e.g. Honey, Garlic).
3. **Battle** — automatic stack machine. Cards reveal off the top of each
deck until a pet is in play. Play effects fire on reveal (rocks roll a d6
with faces 0/0/1/1/2/2 and hit the opposing pet before the clash; Skunk
strips foods; Wolverine steals apples; Chili mills the enemy deck). The
two pets simultaneously deal their full power to each other as damage
markers; a pet with markers ≥ power faints (attack never drops while
wounded). Faint effects push summons onto either deck, recycle apples
(Dodo), set aside delayed/recurring/conditional rock volleys (Badger,
Blowfish, Snake, Crocodile), raise auras (Turkey, Mammoth), or arm a
team shield (Turtle). Survivors that took damage fire Hurt effects
(Peacock, Camel, Gorilla). Shields (Turtle/Gorilla/Melon) block whole
hits, Garlic shaves 1 per attack, and Scorpion KOs anything its clash
attack manages to hurt. Hippo heals when enemies faint; Rhino rocks each
enemy pet as it's played. A clash that changes nothing is a stalemate:
both pets faint so the ones behind them can settle it. Last player able
to field a pet wins: 1 trophy for rounds 15, 2 for round 6. A battle
that runs both sides out is a draw, and draws award nothing.
Apples and bees are **temporary**: they leave your deck after the battle.
Most trophies after round 6 wins.
## 4 & 6 player mode
A game seats an even number of players — 2, 4, or 6 — so everyone has an
opponent every round. A lobby with an odd number of humans fills the gap with
a bot; any seat can be a bot, so a table of one human and five computers is
just as valid as six humans.
Above two players the shop is shared but the battles are not: each round
splits the table into pairs, and every pair fights its own battle
simultaneously. The pairings come from the fixed table printed in the rulebook
(`internal/game/schedule.go`) — a round-robin that runs everyone past everyone
before replaying earlier rounds to fill out the six. All of it is public: you
can replay any table's battle, not just your own, and peek at any player's
lineup afterwards.
Two rules change with the bigger table:
- **First shopper** — at two players the priority token does double duty
(shops first, acts first in battle) and passes from a winner to the loser.
With more players it's a separate token that starts at seat A and walks one
seat along each round, while every battle flips its own first player.
- **Packs** — the rulebook asks for one pack per pair (2 packs for 4 players,
3 for 6). The host picks them in the lobby and their tier decks shuffle
together: all the tier 1 cards into one tier 1 deck, and so on. Two players
may combine packs too, for a deeper shop.
Ties on trophies are settled by counting back from the last round: whoever won
round 6 takes it, else round 5, and so on. Players with identical records
share the victory.
## Packs
Three card packs are playable, and the host combines one or more of them in
the lobby. Each pack is six tiers, one per round, and every pet ships as two
copies.
### Turtle pack
| Tier | Pets | Food |
| ---- | ---- | ---- |
| 1 | Ant, Cricket, Duck, Otter, Mosquito, Fish | — |
| 2 | Worm, Flamingo, Peacock, Swan, Rat, Spider | Honey |
| 3 | Dog, Dolphin, Giraffe, Camel, Sheep, Dodo, Badger | Garlic |
| 4 | Squirrel, Turtle, Rooster, Bison, Blowfish, Skunk, Hippo | Pineapple |
| 5 | Monkey, Rhino, Crocodile, Scorpion, Seal, Shark, Turkey | Chili |
| 6 | Gorilla, Fly, Leopard, Mammoth, Cat, Snake, Wolverine | Melon |
### Golden pack
| Tier | Pets | Food |
| ---- | ---- | ---- |
| 1 | Groundhog, Pied Tamarin, Chipmunk, Cone Snail, Bulldog, Opossum | — |
| 2 | Black-Necked Stilt, Lizard, Hercules Beetle, Stoat, Desert Rain Frog, Honduran White Bat | — |
| 3 | Guinea Fowl, Surgeon Fish, Osprey, Anteater, Bear, Royal Flycatcher, Flea | Avocado |
| 4 | Saiga Antelope, Vaquita, Poison Dart Frog, Manta Ray, Slug, Cockatoo, Manatee | Potato |
| 5 | Nyala, Nurse Shark, Giant Isopod, Blue-Ringed Octopus, Raccoon, Fire Ant, Macaque | Durian |
| 6 | Highland Cow, Wildebeest, Grizzly Bear, Catfish, Komodo, Bird of Paradise, German Shepherd | Tomato |
The Golden pack adds mechanics the Turtle pack doesn't have:
- **Trumpets** — an ephemeral battle resource pets earn and spend mid-fight.
- **Golden Retriever** — once per battle, a side that runs out of cards but
still holds Trumpets fields one, its power equal to those Trumpets.
- **Avocado** — a persistent set-aside token you can discard in place of gold.
- **Nurse Shark** — on entry it spends every Trumpet it can (up to 3),
throwing two rocks at the enemy per Trumpet spent.
- **Cockatoo** — a shop-time reveal, plus Manta Ray's free first buy,
Blue-Ringed Octopus' per-buy apples, and more.
### Unicorn pack
| Tier | Pets | Food |
| ---- | ---- | ---- |
| 1 | Alchemedes, Cuddle Toad, Pengobble, Barghest, Basilisk, Baku | — |
| 2 | Thunderbird, Gargoyle, Frost Wolf, Mothman, Nightcrawler, Bigfoot | Fairy Dust |
| 3 | Fur-Bearing Trout, Calygreyhound, Tatzelwurm, Skeleton Dog, Mandrake, Lucky Cat, Slime | Water of Youth |
| 4 | Roc, Chimera, Kraken, Unicorn, Abomination, Rootlin, Fairy | Health Potion |
| 5 | Kitsune, Pixiu, Red Dragon, Amalgamation, Vampire Bat, Werewolf, Loveland Frogman | Big Mana Potion |
| 6 | Sleipnir, Sea Serpent, Bakunawa, Manticore, Team Spirit, Behemoth, Quetzalcoatl | Cornucopia |
The Unicorn pack adds two mechanics the others don't have:
- **Mana** — a **persistent** resource (unlike ephemeral Trumpets): pets earn
it in the shop (Cuddle Toad, Thunderbird, Roc) and in battle (Alchemedes,
Fur-Bearing Trout, Kitsune, Tatzelwurm per rock blank), and spend it to power
abilities (Pengobble, Gargoyle, Sea Serpent, Bakunawa; Sleipnir's very Power
equals your Mana). It carries over between rounds until spent, so you can bank
it up.
- **Ailments** — temporary debuff cards attached to a pet during battle,
removed at round end like apples. **Spooked** lowers the pet's clash attack
(1 each, min 0); **Exposed** raises the damage it takes per hit (+1 each).
They stack, can be added to the enemy pet or dropped on top of the enemy deck
(Frost Wolf, Slime, Kraken, Red Dragon). **Baku** shrugs off the first one it
takes, **Unicorn** turns the next friendly one into apples, and **Manticore**
makes enemy ailments count for more.
- Plus a **game-wide discard pile** the higher tiers reach into (Chimera,
Abomination), pet-recycling (Fairy, Loveland Frogman's bounce), and shop
tricks — Bigfoot's deck peek, Water of Youth's upgrade, Quetzalcoatl's reveal.
## Layout
```
cmd/server/ entrypoint
cmd/report/ debug report dumper (reads the DB directly)
internal/game/ rules engine (pure, fully tested)
internal/ai/ computer opponent (decides from a player View only)
internal/server/ HTTP + WebSocket rooms; drives bot turns
internal/store/ SQLite persistence
internal/env/ .env loading
web/ React frontend
```
## The computer opponent
Any seat can be a bot (`Player.IsBot`); humans and bots are interchangeable
to the engine, so a table can mix them freely. The AI in `internal/ai` never
touches the `Game` — it decides from a `game.View`, the same per-player state
a human client is sent, plus a persisted memory of public observations
(battle lineups, the event log, shop row changes). It cannot see your deck
order, hidden trade picks, or the shuffled shop decks. It scores candidate
moves by Monte-Carlo battle rollouts (`game.SimulateBattle`) against sampled
guesses of your deck and ordering, blended with a long-term deck-value
heuristic; difficulty tunes a softmax over the scored moves plus the rollout
budget.
At a bigger table it keeps a model of *every* seat, not just one rival — it
will face each of them eventually, and every battle is fought in the open —
but plans each round against the specific opponent the schedule pairs it
with. Its card counting spans the whole table's known cards and the combined
contents of the packs in play.