mirror of
https://github.com/jcreek/CosmicClash.git
synced 2026-09-13 17:02:05 +00:00
feat(multiplayer): model initial connect outcomes
This commit is contained in:
@@ -1410,3 +1410,5 @@ Allocated boot now also validates the complete signed roster shape before openin
|
|||||||
The control plane now mirrors that topology fence at roster publication: signed entries with duplicate players, duplicate slots, or a team inconsistent with the canonical global slot are rejected before durable assignment rows are written. Focused store tests cover forged topology and duplicate entries; normal/race Go suites and vet pass.
|
The control plane now mirrors that topology fence at roster publication: signed entries with duplicate players, duplicate slots, or a team inconsistent with the canonical global slot are rejected before durable assignment rows are written. Focused store tests cover forged topology and duplicate entries; normal/race Go suites and vet pass.
|
||||||
|
|
||||||
The backend roster persistence boundary now enforces the same duplicate-player, duplicate-slot, and team/global-slot invariants as Godot startup. This closes the remaining local consistency gap in task 8.31; production signer/client-ticket publication and live Agones verification remain external gates.
|
The backend roster persistence boundary now enforces the same duplicate-player, duplicate-slot, and team/global-slot invariants as Godot startup. This closes the remaining local consistency gap in task 8.31; production signer/client-ticket publication and live Agones verification remain external gates.
|
||||||
|
|
||||||
|
The no-show policy now has an explicit domain translation layer (`PlanInitialConnect`): `WAIT` remains non-mutating, ranked no-shows produce a `CANCELLED` match plan with innocent-player IDs, and eligible casual play produces a `LIVE` plan plus the complete bot-filled six-slot lineup. Normal/race domain tests cover both branches; applying the plan transactionally to durable tickets/matches and wiring it into the allocated server lifecycle remain task 8.35 work.
|
||||||
|
|||||||
@@ -32,6 +32,53 @@ type InitialConnectDecision struct {
|
|||||||
Innocent []string
|
Innocent []string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// InitialConnectPlan translates the policy decision into the authoritative
|
||||||
|
// lifecycle result a store/orchestrator must apply. Keeping this translation
|
||||||
|
// in domain prevents one caller from requeueing innocents while another leaves
|
||||||
|
// them stuck in an accepted ticket, and makes the bot branch explicit.
|
||||||
|
type InitialConnectPlan struct {
|
||||||
|
Action InitialConnectAction
|
||||||
|
MatchState State
|
||||||
|
Connected []string
|
||||||
|
NoShows []Abandonment
|
||||||
|
CasualLineup []CasualSlot
|
||||||
|
}
|
||||||
|
|
||||||
|
func PlanInitialConnect(playlist Playlist, readyAt, now time.Time, participants []ConnectParticipant, priorAbandons map[string][]time.Time) (InitialConnectPlan, error) {
|
||||||
|
decision, err := EvaluateInitialConnect(playlist, readyAt, now, participants, priorAbandons)
|
||||||
|
if err != nil {
|
||||||
|
return InitialConnectPlan{}, err
|
||||||
|
}
|
||||||
|
plan := InitialConnectPlan{
|
||||||
|
Action: decision.Action,
|
||||||
|
Connected: append([]string(nil), decision.Innocent...),
|
||||||
|
NoShows: append([]Abandonment(nil), decision.NoShows...),
|
||||||
|
}
|
||||||
|
switch decision.Action {
|
||||||
|
case InitialConnectWait:
|
||||||
|
return plan, nil
|
||||||
|
case InitialConnectCancel:
|
||||||
|
plan.MatchState = Cancelled
|
||||||
|
return plan, nil
|
||||||
|
case InitialConnectStartWithBot:
|
||||||
|
connected := make([]ConnectParticipant, 0, len(decision.Innocent))
|
||||||
|
for _, participant := range participants {
|
||||||
|
if participant.Connected {
|
||||||
|
connected = append(connected, participant)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lineup, err := BuildCasualLineup(connected)
|
||||||
|
if err != nil {
|
||||||
|
return InitialConnectPlan{}, err
|
||||||
|
}
|
||||||
|
plan.MatchState = Live
|
||||||
|
plan.CasualLineup = lineup
|
||||||
|
return plan, nil
|
||||||
|
default:
|
||||||
|
return InitialConnectPlan{}, fmt.Errorf("unsupported initial-connect action")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// EvaluateInitialConnect only decides pre-live admission. It never computes a
|
// EvaluateInitialConnect only decides pre-live admission. It never computes a
|
||||||
// game result or rating update; those remain unavailable until a match is
|
// game result or rating update; those remain unavailable until a match is
|
||||||
// genuinely live and produces an authoritative result.
|
// genuinely live and produces an authoritative result.
|
||||||
|
|||||||
@@ -41,3 +41,26 @@ func TestCasualWaitsThenStartsWithBotsOnlyWithHumanOnEachTeam(t *testing.T) {
|
|||||||
t.Fatalf("empty-team decision = %+v err=%v", decision, err)
|
t.Fatalf("empty-team decision = %+v err=%v", decision, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestPlanInitialConnectMakesLifecycleActionExplicit(t *testing.T) {
|
||||||
|
readyAt := time.Unix(1000, 0)
|
||||||
|
participants := sixConnectParticipants(0, 1)
|
||||||
|
plan, err := PlanInitialConnect(Casual, readyAt, readyAt.Add(CasualBotStartAfter), participants, nil)
|
||||||
|
if err != nil || plan.Action != InitialConnectStartWithBot || plan.MatchState != Live || len(plan.CasualLineup) != 6 || len(plan.NoShows) != 4 {
|
||||||
|
t.Fatalf("casual initial-connect plan = %+v err=%v", plan, err)
|
||||||
|
}
|
||||||
|
botCount := 0
|
||||||
|
for _, slot := range plan.CasualLineup {
|
||||||
|
if slot.IsBot {
|
||||||
|
botCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if botCount != 4 {
|
||||||
|
t.Fatalf("casual plan bot count = %d, want 4", botCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
ranked, err := PlanInitialConnect(Ranked, readyAt, readyAt.Add(InitialConnectWindow), sixConnectParticipants(0, 1, 2, 3, 4), nil)
|
||||||
|
if err != nil || ranked.Action != InitialConnectCancel || ranked.MatchState != Cancelled || len(ranked.CasualLineup) != 0 || len(ranked.Connected) != 5 {
|
||||||
|
t.Fatalf("ranked initial-connect plan = %+v err=%v", ranked, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user