Add lobby and pack concept.
This commit is contained in:
+71
-7
@@ -84,8 +84,11 @@ type PendingTrade struct {
|
||||
// Game is the complete authoritative state. It is a pure state machine: no
|
||||
// goroutines, no clocks, no I/O. Callers are responsible for locking.
|
||||
type Game struct {
|
||||
ID string `json:"id"`
|
||||
Code string `json:"code"`
|
||||
ID string `json:"id"`
|
||||
Code string `json:"code"`
|
||||
// Pack is the selected card pack (see packs.go). Chosen in the lobby by
|
||||
// the host; determines which cards fill the shop decks.
|
||||
Pack string `json:"pack"`
|
||||
Phase Phase `json:"phase"`
|
||||
Round int `json:"round"` // 1-based
|
||||
Players []*Player `json:"players"`
|
||||
@@ -168,18 +171,27 @@ func New() *Game {
|
||||
g := &Game{
|
||||
ID: randomID(16),
|
||||
Code: randomCode(),
|
||||
Pack: DefaultPack,
|
||||
Phase: PhaseLobby,
|
||||
WinnerSeat: -1,
|
||||
}
|
||||
g.buildDecks()
|
||||
return g
|
||||
}
|
||||
|
||||
// buildDecks (re)creates and shuffles the shop decks for the current pack.
|
||||
// Called on creation and whenever the pack changes, so ShopDecks always match
|
||||
// g.Pack and are ready the moment the game starts.
|
||||
func (g *Game) buildDecks() {
|
||||
g.buildShopDecks()
|
||||
for i := range g.ShopDecks {
|
||||
shuffle(g.ShopDecks[i])
|
||||
}
|
||||
return g
|
||||
}
|
||||
|
||||
// AddPlayer seats a new player during the lobby phase and returns them (with
|
||||
// their secret token). The game starts automatically once full.
|
||||
// their secret token). The host starts the game explicitly once the lobby is
|
||||
// ready (see StartGame).
|
||||
func (g *Game) AddPlayer(name string) (*Player, error) {
|
||||
if g.Phase != PhaseLobby {
|
||||
return nil, fmt.Errorf("%w: game already started", ErrWrongPhase)
|
||||
@@ -197,9 +209,6 @@ func (g *Game) AddPlayer(name string) (*Player, error) {
|
||||
Seat: len(g.Players),
|
||||
}
|
||||
g.Players = append(g.Players, p)
|
||||
if len(g.Players) == MaxPlayers {
|
||||
g.start()
|
||||
}
|
||||
return p, nil
|
||||
}
|
||||
|
||||
@@ -216,6 +225,61 @@ func (g *Game) AddBot(name string, level float64) (*Player, error) {
|
||||
return p, nil
|
||||
}
|
||||
|
||||
// SetPack changes the game's card pack during the lobby and rebuilds the shop
|
||||
// decks to match. Only playable packs may be selected.
|
||||
func (g *Game) SetPack(packID string) error {
|
||||
if g.Phase != PhaseLobby {
|
||||
return fmt.Errorf("%w: game already started", ErrWrongPhase)
|
||||
}
|
||||
pack, ok := packByID(packID)
|
||||
if !ok {
|
||||
return fmt.Errorf("%w: unknown pack", ErrInvalidAction)
|
||||
}
|
||||
if !pack.Playable {
|
||||
return fmt.Errorf("%w: that pack isn't available yet", ErrInvalidAction)
|
||||
}
|
||||
g.Pack = pack.ID
|
||||
g.buildDecks()
|
||||
return nil
|
||||
}
|
||||
|
||||
// RemovePlayer drops a seat from the lobby. The host (seat 0) can't be
|
||||
// removed. Remaining players are re-seated so seats stay contiguous.
|
||||
func (g *Game) RemovePlayer(targetID string) error {
|
||||
if g.Phase != PhaseLobby {
|
||||
return fmt.Errorf("%w: game already started", ErrWrongPhase)
|
||||
}
|
||||
idx := slices.IndexFunc(g.Players, func(p *Player) bool { return p.ID == targetID })
|
||||
if idx < 0 {
|
||||
return fmt.Errorf("%w: no such player", ErrInvalidAction)
|
||||
}
|
||||
if idx == 0 {
|
||||
return fmt.Errorf("%w: the host can't be removed", ErrInvalidAction)
|
||||
}
|
||||
g.Players = slices.Delete(g.Players, idx, idx+1)
|
||||
for i, p := range g.Players {
|
||||
p.Seat = i
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// StartGame begins the match from the lobby once enough players are seated.
|
||||
// The shop decks are already built for g.Pack (see buildDecks); this just
|
||||
// validates and makes the transition.
|
||||
func (g *Game) StartGame() error {
|
||||
if g.Phase != PhaseLobby {
|
||||
return fmt.Errorf("%w: game already started", ErrWrongPhase)
|
||||
}
|
||||
if len(g.Players) < MinPlayers {
|
||||
return fmt.Errorf("%w: need at least %d players to start", ErrInvalidAction, MinPlayers)
|
||||
}
|
||||
if pack, ok := packByID(g.Pack); !ok || !pack.Playable {
|
||||
return fmt.Errorf("%w: that pack isn't available yet", ErrInvalidAction)
|
||||
}
|
||||
g.start()
|
||||
return nil
|
||||
}
|
||||
|
||||
// PlayerByID returns the player, or nil.
|
||||
func (g *Game) PlayerByID(id string) *Player {
|
||||
for _, p := range g.Players {
|
||||
|
||||
Reference in New Issue
Block a user