Files
90CNServer/server/internal/modules/dnf/worldmap/tournament.go
T
hanfuit 645b64fe21 DNF90 服务端与启动器完整源码(含 GM 工具功能增强)
服务端(Go):
- GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面
- GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新
- 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新)
- 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍
- 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0)
- 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算

启动器(Python/JS):
- 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭
- GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐)
- 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
2026-11-04 14:31:55 +08:00

790 lines
27 KiB
Go

package worldmap
import (
"errors"
"fmt"
"strings"
"sync"
)
const tournamentRoundCount = 4
var (
ErrTournamentDefinitionInvalid = errors.New("dnf tournament definition is invalid")
ErrTournamentRandomInvalid = errors.New("dnf tournament random source returned an invalid index")
ErrTournamentSequenceUnbound = errors.New("dnf tournament actor sequence is not bound")
)
const (
tournamentCardTypeCount = 2
tournamentCardsPerType = 2
)
// TournamentReward is the frozen face of one Yellow Dragon reward card.
// ItemID==0 represents a gold/empty wire tuple; Empty disambiguates the
// zero-value empty card from a real gold card with a zero amount.
type TournamentReward struct {
ItemID uint32
Count uint32
Gold bool
Empty bool
}
// TournamentRewardPlan mirrors the current client card topology: two cards
// in the upper group and, for champions only, two cards in the lower group.
// The plan is created once after the terminal actor death and then only moves
// through reservation/delivery states. It is intentionally independent of
// packet encoding and inventory persistence.
type TournamentRewardPlan struct {
CompletedRounds byte
Experience uint32
Champion bool
Upper [tournamentCardsPerType]TournamentReward
Lower [tournamentCardsPerType]TournamentReward
selected [tournamentCardTypeCount]int8
delivered [tournamentCardTypeCount]bool
}
func (p *TournamentRewardPlan) initSelection() {
if p == nil {
return
}
for index := range p.selected {
p.selected[index] = -1
}
}
func (p TournamentRewardPlan) CardCount(side byte) byte {
if side == 0 {
return tournamentCardsPerType
}
if side == 1 && p.Champion {
return tournamentCardsPerType
}
return 0
}
func (p TournamentRewardPlan) Card(side, index byte) (TournamentReward, bool) {
if index >= tournamentCardsPerType || index >= p.CardCount(side) {
return TournamentReward{}, false
}
if side == 0 {
return p.Upper[index], true
}
return p.Lower[index], true
}
// SelectedCard reports the committed card for one visible reward group.
// A reserved but not yet delivered selection is intentionally not projected
// to the client as owned.
func (p TournamentRewardPlan) SelectedCard(side byte) (byte, bool) {
if side >= tournamentCardTypeCount || !p.delivered[side] || p.selected[side] < 0 {
return 0, false
}
index := byte(p.selected[side])
if index >= p.CardCount(side) {
return 0, false
}
return index, true
}
// ReserveSelection performs the S4A12-style two-phase selection transition.
// The boolean replay result is true only for an already delivered identical
// selection, allowing a retry to be acknowledged without a second grant.
func (p *TournamentRewardPlan) ReserveSelection(side, index byte) (TournamentReward, bool, bool) {
if p == nil || index >= p.CardCount(side) || side >= tournamentCardTypeCount {
return TournamentReward{}, false, false
}
if p.delivered[side] {
// A retry is idempotent only when it repeats the exact card that was
// committed. A different card index must be rejected instead of being
// acknowledged with a mismatched reward row.
if p.selected[side] != int8(index) {
return TournamentReward{}, false, false
}
reward, ok := p.Card(side, index)
return reward, false, ok
}
if p.selected[side] >= 0 {
return TournamentReward{}, false, false
}
p.selected[side] = int8(index)
reward, _ := p.Card(side, index)
return reward, true, false
}
func (p *TournamentRewardPlan) CommitSelection(side, index byte) bool {
if p == nil || side >= tournamentCardTypeCount || index >= p.CardCount(side) || p.selected[side] != int8(index) || p.delivered[side] {
return false
}
p.delivered[side] = true
return true
}
func (p *TournamentRewardPlan) RollbackSelection(side, index byte) bool {
if p == nil || side >= tournamentCardTypeCount || index >= p.CardCount(side) || p.selected[side] != int8(index) || p.delivered[side] {
return false
}
p.selected[side] = -1
return true
}
func (p TournamentRewardPlan) IsSelectionComplete() bool {
if p.selected[0] < 0 || !p.delivered[0] {
return false
}
return !p.Champion || (p.selected[1] >= 0 && p.delivered[1])
}
func (p TournamentRewardPlan) Cardless() bool {
for side := byte(0); side < tournamentCardTypeCount; side++ {
for index := byte(0); index < p.CardCount(side); index++ {
reward, ok := p.Card(side, index)
if !ok || !reward.Empty {
return false
}
}
}
return true
}
func (p *TournamentRewardPlan) commitCardless() bool {
if p == nil || !p.Cardless() {
return false
}
if p.IsSelectionComplete() {
return true
}
for side := byte(0); side < tournamentCardTypeCount; side++ {
if p.CardCount(side) == 0 {
continue
}
p.selected[side] = 0
p.delivered[side] = true
}
return p.IsSelectionComplete()
}
type TournamentActorKind byte
const (
TournamentActorMonster TournamentActorKind = iota
TournamentActorAICharacter
)
type TournamentActor struct {
Kind TournamentActorKind `json:"kind"`
Code uint32 `json:"code"`
Strength uint16 `json:"strength"`
Name string `json:"name"`
Level byte `json:"level"`
Type byte `json:"type"`
}
type TournamentTeam struct {
Position byte `json:"position"`
Members []TournamentActor `json:"members"`
Player bool `json:"player"`
}
type TournamentRound struct {
Number byte `json:"number"`
Teams []TournamentTeam `json:"teams"`
}
type TournamentDefinition struct {
DungeonID uint32
MapID uint32
DifficultyLevel byte
PartyCount byte
RoundFatigue int64
GoldRate int64
Experience map[byte]uint32
ResultCards map[byte]TournamentResultCard
RewardItems []TournamentRewardItemRate
Candidates []TournamentActor
StartAreas []TournamentStartArea
}
type TournamentActorProjector func(candidate TournamentEnemyCandidate, basisLevel byte) (level byte, actorType byte, err error)
func (t *Table) TournamentDefinition(dungeonID, mapID int64, partyCount int, projector TournamentActorProjector) (TournamentDefinition, error) {
dungeon, ok := t.FindDungeon(dungeonID)
if !ok {
return TournamentDefinition{}, fmt.Errorf("%w: dungeon=%d is missing", ErrTournamentDefinitionInvalid, dungeonID)
}
tournamentMap, ok := t.FindMap(mapID)
if !ok {
return TournamentDefinition{}, fmt.Errorf("%w: map=%d is missing", ErrTournamentDefinitionInvalid, mapID)
}
return buildTournamentDefinition(dungeon, tournamentMap, partyCount, projector)
}
func buildTournamentDefinition(dungeon Dungeon, tournamentMap Map, partyCount int, projector TournamentActorProjector) (TournamentDefinition, error) {
if dungeon.Metadata.Flags == nil || !dungeon.Metadata.Flags["tournament dungeon"] {
return TournamentDefinition{}, fmt.Errorf("%w: dungeon=%d is not marked tournament", ErrTournamentDefinitionInvalid, dungeon.ID)
}
if dungeon.ID <= 0 || dungeon.ID > int64(^uint32(0)) || tournamentMap.ID <= 0 || tournamentMap.ID > int64(^uint32(0)) {
return TournamentDefinition{}, fmt.Errorf("%w: dungeon=%d map=%d exceeds wire range", ErrTournamentDefinitionInvalid, dungeon.ID, tournamentMap.ID)
}
if !tournamentMap.DungeonID.Set || tournamentMap.DungeonID.Value != dungeon.ID {
return TournamentDefinition{}, fmt.Errorf("%w: map=%d belongs to dungeon=%d", ErrTournamentDefinitionInvalid, tournamentMap.ID, tournamentMap.DungeonID.Value)
}
if tournamentMap.TournamentMalformed || partyCount < 1 || partyCount > 2 {
return TournamentDefinition{}, fmt.Errorf("%w: party_count=%d malformed_map=%t", ErrTournamentDefinitionInvalid, partyCount, tournamentMap.TournamentMalformed)
}
if !dungeon.Metadata.LimitPartyCount.Set || dungeon.Metadata.LimitPartyCount.Value != int64(partyCount) {
return TournamentDefinition{}, fmt.Errorf("%w: party_count=%d limit=%d", ErrTournamentDefinitionInvalid, partyCount, dungeon.Metadata.LimitPartyCount.Value)
}
if !dungeon.Metadata.BasisLevel.Set || dungeon.Metadata.BasisLevel.Value < 1 || dungeon.Metadata.BasisLevel.Value > 0xff {
return TournamentDefinition{}, fmt.Errorf("%w: basis_level=%d", ErrTournamentDefinitionInvalid, dungeon.Metadata.BasisLevel.Value)
}
basisLevel := byte(dungeon.Metadata.BasisLevel.Value)
definition := TournamentDefinition{
DungeonID: uint32(dungeon.ID), MapID: uint32(tournamentMap.ID), PartyCount: byte(partyCount),
DifficultyLevel: basisLevel, RoundFatigue: dungeon.Metadata.TournamentRoundFatigue,
GoldRate: dungeon.Metadata.TournamentGoldRate, Experience: make(map[byte]uint32, 5),
ResultCards: make(map[byte]TournamentResultCard, 6),
RewardItems: append([]TournamentRewardItemRate(nil), dungeon.Metadata.TournamentRewardItemRates...),
StartAreas: append([]TournamentStartArea(nil), tournamentMap.TournamentStartAreas...),
}
for _, row := range dungeon.Metadata.TournamentClearRewardExp {
if row.CompletedRounds < 0 || row.CompletedRounds > tournamentRoundCount || row.Experience < 0 || row.Experience > int64(^uint32(0)) {
return TournamentDefinition{}, fmt.Errorf("%w: reward experience row=%+v", ErrTournamentDefinitionInvalid, row)
}
key := byte(row.CompletedRounds)
if _, exists := definition.Experience[key]; exists {
return TournamentDefinition{}, fmt.Errorf("%w: duplicate reward experience round=%d", ErrTournamentDefinitionInvalid, key)
}
definition.Experience[key] = uint32(row.Experience)
}
for round := byte(0); round <= tournamentRoundCount; round++ {
if _, ok := definition.Experience[round]; !ok {
return TournamentDefinition{}, fmt.Errorf("%w: reward experience round=%d is missing", ErrTournamentDefinitionInvalid, round)
}
}
for _, card := range dungeon.Metadata.TournamentResultCards {
if card.ResultKey < 0 || card.ResultKey > 5 || card.GoldWeight < 0 || card.ItemWeight < 0 || card.EmptyWeight < 0 || card.GoldWeight+card.ItemWeight+card.EmptyWeight <= 0 {
return TournamentDefinition{}, fmt.Errorf("%w: result card=%+v", ErrTournamentDefinitionInvalid, card)
}
key := byte(card.ResultKey)
if _, exists := definition.ResultCards[key]; exists {
return TournamentDefinition{}, fmt.Errorf("%w: duplicate result card=%d", ErrTournamentDefinitionInvalid, key)
}
definition.ResultCards[key] = card
}
for key := byte(0); key <= 5; key++ {
if _, ok := definition.ResultCards[key]; !ok {
return TournamentDefinition{}, fmt.Errorf("%w: result card=%d is missing", ErrTournamentDefinitionInvalid, key)
}
}
if len(definition.RewardItems) == 0 || definition.RoundFatigue < 0 || definition.GoldRate <= 0 {
return TournamentDefinition{}, fmt.Errorf("%w: reward metadata is incomplete", ErrTournamentDefinitionInvalid)
}
for _, reward := range definition.RewardItems {
if reward.ItemID <= 0 || reward.Weight <= 0 || reward.Count <= 0 {
return TournamentDefinition{}, fmt.Errorf("%w: reward item=%+v", ErrTournamentDefinitionInvalid, reward)
}
}
for _, candidate := range tournamentMap.TournamentEnemies {
if candidate.PartyCount != int64(partyCount) {
continue
}
if candidate.Code <= 0 || candidate.Code > int64(^uint32(0)) || candidate.Strength < 0 || candidate.Strength > 0xffff || strings.TrimSpace(candidate.Name) == "" {
return TournamentDefinition{}, fmt.Errorf("%w: actor=%+v", ErrTournamentDefinitionInvalid, candidate)
}
actor := TournamentActor{Code: uint32(candidate.Code), Strength: uint16(candidate.Strength), Name: candidate.Name, Level: basisLevel}
switch strings.ToLower(strings.TrimSpace(candidate.Kind)) {
case "[monster]":
actor.Kind = TournamentActorMonster
case "[apc]":
if projector == nil {
return TournamentDefinition{}, fmt.Errorf("%w: APC code=%d requires a projector", ErrTournamentDefinitionInvalid, candidate.Code)
}
level, actorType, err := projector(candidate, basisLevel)
if err != nil {
return TournamentDefinition{}, fmt.Errorf("%w: project APC code=%d: %v", ErrTournamentDefinitionInvalid, candidate.Code, err)
}
actor.Kind, actor.Level, actor.Type = TournamentActorAICharacter, level, actorType
default:
return TournamentDefinition{}, fmt.Errorf("%w: actor kind=%q", ErrTournamentDefinitionInvalid, candidate.Kind)
}
definition.Candidates = append(definition.Candidates, actor)
}
if len(definition.Candidates) < 15*partyCount {
return TournamentDefinition{}, fmt.Errorf("%w: candidates=%d required=%d", ErrTournamentDefinitionInvalid, len(definition.Candidates), 15*partyCount)
}
startCount := 0
for _, area := range definition.StartAreas {
if area.PartyCount == int64(partyCount) {
startCount++
}
}
if startCount < 2 {
return TournamentDefinition{}, fmt.Errorf("%w: start areas=%d required=2", ErrTournamentDefinitionInvalid, startCount)
}
return definition, nil
}
type TournamentRandom func(exclusiveMaximum int) (int, error)
type TournamentTerminalResult byte
const (
TournamentActive TournamentTerminalResult = iota
TournamentChampion
TournamentEliminated
)
type TournamentDeathDisposition byte
const (
TournamentDeathRejected TournamentDeathDisposition = iota
TournamentDeathDuplicate
TournamentDeathAccepted
TournamentRoundCompleted
TournamentCompleted
)
type TournamentDeathTransition struct {
Disposition TournamentDeathDisposition
CompletedRound byte
CurrentRound byte
}
type TournamentRuntime struct {
mu sync.Mutex
Definition TournamentDefinition
Rounds []TournamentRound
PathActors []TournamentActor
firstSequence uint16
currentRound byte
completedRounds byte
terminal TournamentTerminalResult
acceptedEvents map[string]uint16
acceptedSequences map[uint16]struct{}
rewardPlan *TournamentRewardPlan
}
// PrepareRewardPlan freezes the PVF-selected card faces exactly once after a
// tournament becomes terminal. The caller supplies the already-authoritative
// gold/experience amounts because those values belong to the bridge's reward
// and persistence layers, not to the world-map parser.
func (r *TournamentRuntime) PrepareRewardPlan(experience, gold uint32, random TournamentRandom) (TournamentRewardPlan, bool, error) {
if r == nil || random == nil {
return TournamentRewardPlan{}, false, fmt.Errorf("%w: reward random source is missing", ErrTournamentDefinitionInvalid)
}
r.mu.Lock()
defer r.mu.Unlock()
if r.terminal == TournamentActive {
return TournamentRewardPlan{}, false, fmt.Errorf("%w: tournament is not terminal", ErrTournamentDefinitionInvalid)
}
if r.rewardPlan != nil {
return *r.rewardPlan, false, nil
}
plan := TournamentRewardPlan{
CompletedRounds: r.completedRounds,
Experience: experience,
Champion: r.terminal == TournamentChampion,
}
plan.initSelection()
upperKey := r.completedRounds
for index := byte(0); index < tournamentCardsPerType; index++ {
reward, err := tournamentRollReward(r.Definition, upperKey, gold, random)
if err != nil {
return TournamentRewardPlan{}, false, err
}
plan.Upper[index] = reward
}
if plan.Champion {
for index := byte(0); index < tournamentCardsPerType; index++ {
reward, err := tournamentRollReward(r.Definition, 5, gold, random)
if err != nil {
return TournamentRewardPlan{}, false, err
}
plan.Lower[index] = reward
}
}
r.rewardPlan = &plan
return plan, true, nil
}
func (r *TournamentRuntime) RewardPlanSnapshot() (TournamentRewardPlan, bool) {
if r == nil {
return TournamentRewardPlan{}, false
}
r.mu.Lock()
defer r.mu.Unlock()
if r.rewardPlan == nil {
return TournamentRewardPlan{}, false
}
return *r.rewardPlan, true
}
func (r *TournamentRuntime) ReserveRewardSelection(side, index byte) (TournamentReward, bool, bool) {
if r == nil {
return TournamentReward{}, false, false
}
r.mu.Lock()
defer r.mu.Unlock()
if r.rewardPlan == nil {
return TournamentReward{}, false, false
}
return r.rewardPlan.ReserveSelection(side, index)
}
func (r *TournamentRuntime) CommitRewardSelection(side, index byte) bool {
if r == nil {
return false
}
r.mu.Lock()
defer r.mu.Unlock()
return r.rewardPlan != nil && r.rewardPlan.CommitSelection(side, index)
}
func (r *TournamentRuntime) RollbackRewardSelection(side, index byte) bool {
if r == nil {
return false
}
r.mu.Lock()
defer r.mu.Unlock()
return r.rewardPlan != nil && r.rewardPlan.RollbackSelection(side, index)
}
func (r *TournamentRuntime) RewardSelectionComplete() bool {
if r == nil {
return false
}
r.mu.Lock()
defer r.mu.Unlock()
return r.rewardPlan != nil && r.rewardPlan.IsSelectionComplete()
}
// CommitCardlessRewardPlan closes a PVF reward plan whose visible faces are
// all empty. The current client represents this as zero card groups in op388,
// so there is no op458 request that could otherwise cross the delivery gate.
func (r *TournamentRuntime) CommitCardlessRewardPlan() bool {
if r == nil {
return false
}
r.mu.Lock()
defer r.mu.Unlock()
return r.rewardPlan != nil && r.rewardPlan.commitCardless()
}
// RewardSideSelectionComplete reports whether one visible card group has
// already crossed the durable delivery boundary. It is used by timeout
// coordinators to fill only client selections that never arrived.
func (r *TournamentRuntime) RewardSideSelectionComplete(side byte) bool {
if r == nil || side >= tournamentCardTypeCount {
return false
}
r.mu.Lock()
defer r.mu.Unlock()
return r.rewardPlan != nil && r.rewardPlan.delivered[side]
}
func tournamentRollReward(definition TournamentDefinition, resultKey byte, gold uint32, random TournamentRandom) (TournamentReward, error) {
card, ok := definition.ResultCards[resultKey]
if !ok {
return TournamentReward{}, fmt.Errorf("%w: reward result key=%d is missing", ErrTournamentDefinitionInvalid, resultKey)
}
total := card.GoldWeight + card.ItemWeight + card.EmptyWeight
if total <= 0 {
return TournamentReward{}, fmt.Errorf("%w: reward result key=%d has no weight", ErrTournamentDefinitionInvalid, resultKey)
}
roll, err := tournamentRandomIndex(random, int(total))
if err != nil {
return TournamentReward{}, err
}
if int64(roll) < card.GoldWeight {
return TournamentReward{Gold: true, Count: gold}, nil
}
if int64(roll) < card.GoldWeight+card.ItemWeight {
reward, err := tournamentRollRewardItem(definition.RewardItems, random)
if err != nil {
return TournamentReward{}, err
}
return reward, nil
}
return TournamentReward{Empty: true}, nil
}
func tournamentRollRewardItem(items []TournamentRewardItemRate, random TournamentRandom) (TournamentReward, error) {
var total int64
for _, item := range items {
if item.ItemID <= 0 || item.Weight <= 0 || item.Count <= 0 {
continue
}
total += item.Weight
}
if total <= 0 || total > int64(^uint(0)>>1) {
return TournamentReward{}, fmt.Errorf("%w: reward item rates are empty", ErrTournamentDefinitionInvalid)
}
roll, err := tournamentRandomIndex(random, int(total))
if err != nil {
return TournamentReward{}, err
}
var cumulative int64
for _, item := range items {
if item.ItemID <= 0 || item.Weight <= 0 || item.Count <= 0 {
continue
}
cumulative += item.Weight
if int64(roll) < cumulative {
return TournamentReward{ItemID: uint32(item.ItemID), Count: uint32(item.Count)}, nil
}
}
return TournamentReward{}, fmt.Errorf("%w: reward item roll did not resolve", ErrTournamentDefinitionInvalid)
}
func NewTournamentRuntime(definition TournamentDefinition, random TournamentRandom) (*TournamentRuntime, error) {
if random == nil {
return nil, fmt.Errorf("%w: random source is nil", ErrTournamentDefinitionInvalid)
}
partyCount := int(definition.PartyCount)
if partyCount < 1 || partyCount > 2 || len(definition.Candidates) < 15*partyCount {
return nil, fmt.Errorf("%w: party=%d candidates=%d", ErrTournamentDefinitionInvalid, partyCount, len(definition.Candidates))
}
candidates := append([]TournamentActor(nil), definition.Candidates...)
if err := shuffleTournamentActors(candidates, random); err != nil {
return nil, err
}
npcTeams := make([]TournamentTeam, 15)
for index := range npcTeams {
members := append([]TournamentActor(nil), candidates[index*partyCount:(index+1)*partyCount]...)
npcTeams[index] = TournamentTeam{Members: members}
}
for i := 1; i < len(npcTeams); i++ {
for j := i; j > 0 && tournamentTeamStrength(npcTeams[j]) < tournamentTeamStrength(npcTeams[j-1]); j-- {
npcTeams[j], npcTeams[j-1] = npcTeams[j-1], npcTeams[j]
}
}
playerPosition, err := tournamentRandomIndex(random, 16)
if err != nil {
return nil, err
}
opponentPosition := playerPosition ^ 1
firstRound := make([]TournamentTeam, 16)
firstRound[playerPosition] = TournamentTeam{Position: byte(playerPosition), Members: make([]TournamentActor, partyCount), Player: true}
firstRound[opponentPosition] = tournamentTeamAt(npcTeams[0], opponentPosition)
remaining := append([]TournamentTeam(nil), npcTeams[1:]...)
if err := shuffleTournamentTeams(remaining, random); err != nil {
return nil, err
}
remainingIndex := 0
for position := range firstRound {
if position == playerPosition || position == opponentPosition {
continue
}
firstRound[position] = tournamentTeamAt(remaining[remainingIndex], position)
remainingIndex++
}
rounds := []TournamentRound{{Number: 1, Teams: firstRound}}
previous := firstRound
for round := byte(2); round <= tournamentRoundCount; round++ {
nextRound := make([]TournamentTeam, len(previous)/2)
for match := range nextRound {
left, right := previous[match*2], previous[match*2+1]
winner := left
if !left.Player && right.Player {
winner = right
} else if !left.Player && !right.Player {
leftStrength, rightStrength := tournamentTeamStrength(left), tournamentTeamStrength(right)
if rightStrength > leftStrength {
winner = right
} else if rightStrength == leftStrength {
pick, pickErr := tournamentRandomIndex(random, 2)
if pickErr != nil {
return nil, pickErr
}
if pick == 1 {
winner = right
}
}
}
nextRound[match] = tournamentTeamAt(winner, match)
}
rounds = append(rounds, TournamentRound{Number: round, Teams: nextRound})
previous = nextRound
}
pathActors := make([]TournamentActor, 0, tournamentRoundCount*partyCount)
for _, round := range rounds {
playerIndex := -1
for index, team := range round.Teams {
if team.Player {
playerIndex = index
break
}
}
if playerIndex < 0 {
return nil, fmt.Errorf("%w: round=%d has no player", ErrTournamentDefinitionInvalid, round.Number)
}
for _, actor := range round.Teams[playerIndex^1].Members {
if round.Number == tournamentRoundCount {
if actor.Kind == TournamentActorAICharacter {
actor.Type = 8
} else {
actor.Type = 3
}
}
pathActors = append(pathActors, actor)
}
}
return &TournamentRuntime{
Definition: definition, Rounds: rounds, PathActors: pathActors, currentRound: 1,
acceptedEvents: make(map[string]uint16), acceptedSequences: make(map[uint16]struct{}),
}, nil
}
func (r *TournamentRuntime) BindFirstActorSequence(sequence uint16) bool {
if r == nil || sequence == 0 {
return false
}
r.mu.Lock()
defer r.mu.Unlock()
if int(sequence)+len(r.PathActors)-1 > int(^uint16(0)) {
return false
}
if r.firstSequence == 0 {
r.firstSequence = sequence
}
return r.firstSequence == sequence
}
func (r *TournamentRuntime) FirstActorSequence() uint16 {
if r == nil {
return 0
}
r.mu.Lock()
defer r.mu.Unlock()
return r.firstSequence
}
func (r *TournamentRuntime) ActorForSequence(sequence uint16) (TournamentActor, bool) {
if r == nil || sequence == 0 {
return TournamentActor{}, false
}
r.mu.Lock()
defer r.mu.Unlock()
if r.firstSequence == 0 || sequence < r.firstSequence {
return TournamentActor{}, false
}
index := int(sequence - r.firstSequence)
if index < 0 || index >= len(r.PathActors) {
return TournamentActor{}, false
}
return r.PathActors[index], true
}
func (r *TournamentRuntime) Snapshot() (currentRound, completedRounds byte, terminal TournamentTerminalResult) {
if r == nil {
return 0, 0, TournamentEliminated
}
r.mu.Lock()
defer r.mu.Unlock()
return r.currentRound, r.completedRounds, r.terminal
}
func (r *TournamentRuntime) ApplyActorDeath(eventID string, sequence uint16) TournamentDeathTransition {
if r == nil || strings.TrimSpace(eventID) == "" {
return TournamentDeathTransition{Disposition: TournamentDeathRejected}
}
r.mu.Lock()
defer r.mu.Unlock()
if accepted, ok := r.acceptedEvents[eventID]; ok {
disposition := TournamentDeathRejected
if accepted == sequence {
disposition = TournamentDeathDuplicate
}
return TournamentDeathTransition{Disposition: disposition, CurrentRound: r.currentRound}
}
if _, ok := r.acceptedSequences[sequence]; ok {
return TournamentDeathTransition{Disposition: TournamentDeathDuplicate, CurrentRound: r.currentRound}
}
if r.firstSequence == 0 || r.terminal != TournamentActive {
return TournamentDeathTransition{Disposition: TournamentDeathRejected, CurrentRound: r.currentRound}
}
first := int(r.firstSequence) + (int(r.currentRound)-1)*int(r.Definition.PartyCount)
end := first + int(r.Definition.PartyCount)
if int(sequence) < first || int(sequence) >= end {
return TournamentDeathTransition{Disposition: TournamentDeathRejected, CurrentRound: r.currentRound}
}
r.acceptedEvents[eventID] = sequence
r.acceptedSequences[sequence] = struct{}{}
for current := first; current < end; current++ {
if _, ok := r.acceptedSequences[uint16(current)]; !ok {
return TournamentDeathTransition{Disposition: TournamentDeathAccepted, CurrentRound: r.currentRound}
}
}
completed := r.currentRound
r.completedRounds = completed
if completed == tournamentRoundCount {
r.terminal = TournamentChampion
return TournamentDeathTransition{Disposition: TournamentCompleted, CompletedRound: completed, CurrentRound: completed}
}
r.currentRound++
return TournamentDeathTransition{Disposition: TournamentRoundCompleted, CompletedRound: completed, CurrentRound: r.currentRound}
}
func (r *TournamentRuntime) Eliminate() bool {
if r == nil {
return false
}
r.mu.Lock()
defer r.mu.Unlock()
if r.terminal != TournamentActive {
return false
}
r.completedRounds = r.currentRound - 1
r.terminal = TournamentEliminated
return true
}
func tournamentTeamAt(team TournamentTeam, position int) TournamentTeam {
return TournamentTeam{Position: byte(position), Members: append([]TournamentActor(nil), team.Members...), Player: team.Player}
}
func tournamentTeamStrength(team TournamentTeam) uint64 {
var total uint64
for _, actor := range team.Members {
total += uint64(actor.Strength)
}
return total
}
func tournamentRandomIndex(random TournamentRandom, maximum int) (int, error) {
value, err := random(maximum)
if err != nil {
return 0, err
}
if value < 0 || value >= maximum {
return 0, fmt.Errorf("%w: value=%d maximum=%d", ErrTournamentRandomInvalid, value, maximum)
}
return value, nil
}
func shuffleTournamentActors(values []TournamentActor, random TournamentRandom) error {
for index := len(values) - 1; index > 0; index-- {
swap, err := tournamentRandomIndex(random, index+1)
if err != nil {
return err
}
values[index], values[swap] = values[swap], values[index]
}
return nil
}
func shuffleTournamentTeams(values []TournamentTeam, random TournamentRandom) error {
for index := len(values) - 1; index > 0; index-- {
swap, err := tournamentRandomIndex(random, index+1)
if err != nil {
return err
}
values[index], values[swap] = values[swap], values[index]
}
return nil
}