Add support for up to 6 players.
This commit is contained in:
+38
-13
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user