Files
super-auto-pets-board-game/internal/game/packs.go
T

109 lines
2.9 KiB
Go

package game
import (
"fmt"
"strings"
)
// Card packs are the selectable sets of pets and food a game is played with.
// Turtle, Golden, and Unicorn all ship with full six-tier card data.
// PackInfo describes one selectable pack. Playable gates whether a lobby may
// choose it and start a game with it.
type PackInfo struct {
ID string `json:"id"`
Name string `json:"name"`
Emoji string `json:"emoji"`
Playable bool `json:"playable"`
}
// DefaultPack is the pack a freshly created game starts on.
const DefaultPack = "turtle"
// Packs is the ordered catalog of packs shown in the lobby.
var Packs = []PackInfo{
{ID: "turtle", Name: "Turtle Pack", Emoji: "🐢", Playable: true},
{ID: "golden", Name: "Golden Pack", Emoji: "🥇", Playable: true},
{ID: "unicorn", Name: "Unicorn Pack", Emoji: "🦄", Playable: true},
}
// packByID looks up a pack, returning false if the id is unknown.
func packByID(id string) (PackInfo, bool) {
for _, p := range Packs {
if p.ID == id {
return p, true
}
}
return PackInfo{}, false
}
// PacksNeeded is how many packs a game must combine to seat n players: the
// rulebook asks for at least 2 packs at 4 players and 3 at 6, i.e. one per
// pair. More packs than the minimum are always allowed — a deeper shop just
// means fewer repeated pets.
func PacksNeeded(players int) int {
if players < MinPlayers {
return 1
}
return players / 2
}
// sortPacks puts a pack selection into catalog order, so the same choice
// always reads the same way in views and logs.
func sortPacks(ids []string) []string {
out := make([]string, 0, len(ids))
for _, p := range Packs {
for _, id := range ids {
if id == p.ID {
out = append(out, p.ID)
break
}
}
}
return out
}
// validatePacks checks a pack selection: non-empty, known, playable, and free
// of duplicates. It returns the selection in catalog order.
func validatePacks(ids []string) ([]string, error) {
if len(ids) == 0 {
return nil, fmt.Errorf("%w: pick at least one pack", ErrInvalidAction)
}
seen := map[string]bool{}
for _, id := range ids {
pack, ok := packByID(id)
if !ok {
return nil, fmt.Errorf("%w: unknown pack %q", ErrInvalidAction, id)
}
if !pack.Playable {
return nil, fmt.Errorf("%w: the %s isn't available yet", ErrInvalidAction, pack.Name)
}
if seen[id] {
return nil, fmt.Errorf("%w: %s is selected twice", ErrInvalidAction, pack.Name)
}
seen[id] = true
}
return sortPacks(ids), nil
}
// PackNames renders a pack selection as a readable list ("Turtle Pack and
// Golden Pack") for log lines.
func PackNames(ids []string) string {
names := make([]string, 0, len(ids))
for _, id := range ids {
if p, ok := packByID(id); ok {
names = append(names, p.Name)
}
}
switch len(names) {
case 0:
return "no packs"
case 1:
return names[0]
case 2:
return names[0] + " and " + names[1]
default:
return strings.Join(names[:len(names)-1], ", ") + ", and " + names[len(names)-1]
}
}