mirror of
https://github.com/jcreek/CosmicClash.git
synced 2026-09-11 08:23:45 +00:00
feat: add casual bot backfill policy
This commit is contained in:
@@ -0,0 +1,59 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
type CasualPhase string
|
||||
|
||||
const (
|
||||
CasualKickoff CasualPhase = "KICKOFF"
|
||||
CasualLive CasualPhase = "LIVE"
|
||||
)
|
||||
|
||||
type CasualSlot struct {
|
||||
Slot int
|
||||
Team int
|
||||
PlayerID string
|
||||
IsBot bool
|
||||
}
|
||||
|
||||
// BuildCasualLineup freezes the live six-slot shape. Missing humans become
|
||||
// explicit server bots; no human is inserted after live play begins by this
|
||||
// function.
|
||||
func BuildCasualLineup(participants []ConnectParticipant) ([]CasualSlot, error) {
|
||||
if len(participants) < 2 || len(participants) > 6 {
|
||||
return nil, fmt.Errorf("casual lineup needs 2 through 6 humans")
|
||||
}
|
||||
seen := make(map[string]bool, len(participants))
|
||||
teamHuman := map[int]bool{}
|
||||
lineup := make([]CasualSlot, 6)
|
||||
usedSlots := make(map[int]bool)
|
||||
for i, participant := range participants {
|
||||
if participant.PlayerID == "" || participant.Team < 0 || participant.Team > 1 || seen[participant.PlayerID] || usedSlots[i] {
|
||||
return nil, fmt.Errorf("invalid casual participant")
|
||||
}
|
||||
seen[participant.PlayerID] = true
|
||||
usedSlots[i] = true
|
||||
teamHuman[participant.Team] = true
|
||||
lineup[i] = CasualSlot{Slot: i, Team: participant.Team, PlayerID: participant.PlayerID}
|
||||
}
|
||||
if !teamHuman[0] || !teamHuman[1] {
|
||||
return nil, fmt.Errorf("casual lineup requires one human on each team")
|
||||
}
|
||||
for i := range lineup {
|
||||
if lineup[i].PlayerID == "" {
|
||||
lineup[i] = CasualSlot{Slot: i, Team: i % 2, PlayerID: fmt.Sprintf("bot-slot-%d", i), IsBot: true}
|
||||
}
|
||||
}
|
||||
return lineup, nil
|
||||
}
|
||||
|
||||
func CanCasualBackfill(phase CasualPhase, slot CasualSlot) bool {
|
||||
return phase == CasualKickoff && slot.IsBot
|
||||
}
|
||||
|
||||
// CasualBackfillPenalty is intentionally zero: a kickoff-only backfill does
|
||||
// not receive a hidden-rating update or an abandon/decline cooldown.
|
||||
func CasualBackfillPenalty() time.Duration { return 0 }
|
||||
@@ -0,0 +1,23 @@
|
||||
package domain
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestCasualLineupUsesBotsOnlyForMissingSlotsAndRequiresBothTeams(t *testing.T) {
|
||||
lineup, err := BuildCasualLineup([]ConnectParticipant{{PlayerID: "p2", Team: 1}, {PlayerID: "p1", Team: 0}})
|
||||
if err != nil || len(lineup) != 6 || lineup[0].IsBot || lineup[1].IsBot || !lineup[2].IsBot || lineup[2].Team != 0 {
|
||||
t.Fatalf("casual lineup = %+v err=%v", lineup, err)
|
||||
}
|
||||
if _, err := BuildCasualLineup([]ConnectParticipant{{PlayerID: "p1", Team: 0}, {PlayerID: "p2", Team: 0}}); err == nil {
|
||||
t.Fatal("lineup without a human on team 1 was accepted")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCasualBackfillIsKickoffOnlyAndUnrated(t *testing.T) {
|
||||
slot := CasualSlot{Slot: 2, Team: 0, PlayerID: "bot-slot-2", IsBot: true}
|
||||
if !CanCasualBackfill(CasualKickoff, slot) || CanCasualBackfill(CasualLive, slot) || CasualBackfillPenalty() != 0 {
|
||||
t.Fatal("casual backfill policy is incorrect")
|
||||
}
|
||||
if CanCasualBackfill(CasualKickoff, CasualSlot{Slot: 2, Team: 0, PlayerID: "human", IsBot: false}) {
|
||||
t.Fatal("human slot was treated as backfillable")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user