Add support for up to 6 players.

This commit is contained in:
Greyson Parrelli
2026-07-28 07:36:09 -04:00
parent e542118175
commit a4f5f6910d
38 changed files with 2306 additions and 713 deletions
+38 -13
View File
@@ -2,6 +2,7 @@ package server
import (
"errors"
"fmt"
"log/slog"
"math/rand/v2"
"time"
@@ -10,19 +11,23 @@ import (
"github.com/greyson/super-auto-pets-board-game/internal/game"
)
// botDifficulty maps the API's difficulty names to a skill level and a
// table name for the bot. The levels are calibrated against a competent
// player (ai.competentLevel, ~0.60): easy loses ~95% of games to them, medium
// is an even match, and hard wins ~80% (see TestDiagWinRateCurve). Medium is
// pinned to the competent level itself; hard is the strongest bot the engine
// can field.
// botDifficulty maps the API's difficulty names to a skill level and a pool of
// table names. The levels are calibrated against a competent player
// (ai.competentLevel, ~0.60): easy loses ~95% of games to them, medium is an
// even match, and hard wins ~80% (see TestDiagWinRateCurve). Medium is pinned
// to the competent level itself; hard is the strongest bot the engine can
// field.
//
// A table can hold up to five bots, so each difficulty carries a list of names
// rather than one: the first unused name is taken, keeping every seat
// distinguishable while the name still hints at how hard it plays.
var botDifficulty = map[string]struct {
level float64
name string
names []string
}{
"easy": {0.25, "Robo Rookie"},
"medium": {0.60, "Robo Rival"},
"hard": {1.00, "Robo Ace"},
"easy": {0.25, []string{"Robo Rookie", "Bitsy", "Clunk", "Pip", "Sprocket"}},
"medium": {0.60, []string{"Robo Rival", "Gizmo", "Widget", "Rusty", "Cogsworth"}},
"hard": {1.00, []string{"Robo Ace", "Vex", "Apex", "Onyx", "Zenith"}},
}
// requireHost authorizes a lobby-management action: only the host (seat 0)
@@ -36,17 +41,37 @@ func requireHost(g *game.Game, playerID string) error {
}
// addBot seats a computer opponent for a difficulty name, translating the
// name to the engine's skill level. Capacity and phase are enforced by the
// engine's AddPlayer.
// name to the engine's skill level and giving it a name nobody at the table is
// already using. Capacity and phase are enforced by the engine's AddPlayer.
func addBot(g *game.Game, difficulty string) error {
bot, ok := botDifficulty[difficulty]
if !ok {
return errors.New("unknown bot difficulty")
}
_, err := g.AddBot(bot.name, bot.level)
_, err := g.AddBot(botName(g, bot.names), bot.level)
return err
}
// botName picks the first name in the pool that no seat has taken. If a host
// somehow exhausts the pool, it falls back to numbering the first name.
func botName(g *game.Game, pool []string) string {
taken := make(map[string]bool, len(g.Players))
for _, p := range g.Players {
taken[p.Name] = true
}
for _, name := range pool {
if !taken[name] {
return name
}
}
for n := 2; ; n++ {
name := fmt.Sprintf("%s %d", pool[0], n)
if !taken[name] {
return name
}
}
}
// commitLocked is the one path every game mutation goes through: bots update
// their memories from the new public state, the game is persisted, every
// client gets its view, and the next bot move (if any) is scheduled. Callers