diff --git a/game/fight/round.go b/game/fight/round.go index 3b4f9bf..3096dd9 100644 --- a/game/fight/round.go +++ b/game/fight/round.go @@ -16,6 +16,8 @@ type RoundGameOverVerifyHandle[Data RoundData] func(round *Round[Data]) bool // - camps 阵营 // - roundGameOverVerifyHandle 游戏结束验证函数 // - options 选项 +// +// Deprecated: 从 Minotaur 0.2.7 开始,由于设计原因已弃用,请尝试考虑使用 fight.TurnBased 进行代替 func NewRound[Data RoundData](data Data, camps []*RoundCamp, roundGameOverVerifyHandle RoundGameOverVerifyHandle[Data], options ...RoundOption[Data]) *Round[Data] { mark := random.HostName() round := &Round[Data]{ diff --git a/game/fight/turn_based.go b/game/fight/turn_based.go new file mode 100644 index 0000000..6f9110a --- /dev/null +++ b/game/fight/turn_based.go @@ -0,0 +1,184 @@ +package fight + +import ( + "github.com/kercylan98/minotaur/utils/generic" + "sync" + "time" +) + +const ( + signalFinish = 1 + iota // 操作结束信号 + signalStop // 停止回合制信号 +) + +// NewTurnBased 创建一个新的回合制 +// - calcNextTurnDuration 将返回下一次行动时间间隔,适用于按照速度计算下一次行动时间间隔的情况。当返回 0 时,将使用默认的行动超时时间 +func NewTurnBased[CampID, EntityID comparable, Camp generic.IdR[CampID], Entity generic.IdR[EntityID]](calcNextTurnDuration func(Camp, Entity) time.Duration) *TurnBased[CampID, EntityID, Camp, Entity] { + tb := &TurnBased[CampID, EntityID, Camp, Entity]{ + turnBasedEvents: &turnBasedEvents[CampID, EntityID, Camp, Entity]{}, + campRel: make(map[EntityID]Camp), + calcNextTurnDuration: calcNextTurnDuration, + actionTimeoutHandler: func(camp Camp, entity Entity) time.Duration { + return 0 + }, + } + tb.controller = &TurnBasedController[CampID, EntityID, Camp, Entity]{tb: tb} + return tb +} + +// TurnBased 回合制 +type TurnBased[CampID, EntityID comparable, Camp generic.IdR[CampID], Entity generic.IdR[EntityID]] struct { + *turnBasedEvents[CampID, EntityID, Camp, Entity] + controller *TurnBasedController[CampID, EntityID, Camp, Entity] // 控制器 + ticker *time.Ticker // 计时器 + actionWaitTicker *time.Ticker // 行动等待计时器 + actioning bool // 是否正在行动 + actionMutex sync.RWMutex // 行动锁 + entities []Entity // 所有阵容实体顺序 + campRel map[EntityID]Camp // 实体与阵营的关系 + calcNextTurnDuration func(Camp, Entity) time.Duration // 下一次行动时间间隔 + actionTimeoutHandler func(Camp, Entity) time.Duration // 行动超时时间 + + signal chan byte // 信号 + round int // 当前回合数 + currCamp Camp // 当前操作阵营 + currEntity Entity // 当前操作实体 + currActionTimeout time.Duration // 当前行动超时时间 + currStart time.Time // 当前回合开始时间 + + closeMutex sync.RWMutex // 关闭锁 + closed bool +} + +// Close 关闭回合制 +func (slf *TurnBased[CampID, EntityID, Camp, Entity]) Close() { + slf.closeMutex.Lock() + defer slf.closeMutex.Unlock() + slf.closed = true +} + +// AddCamp 添加阵营 +func (slf *TurnBased[CampID, EntityID, Camp, Entity]) AddCamp(camp Camp, entity Entity, entities ...Entity) { + for _, e := range append([]Entity{entity}, entities...) { + slf.entities = append(slf.entities, e) + slf.campRel[e.GetId()] = camp + } +} + +// SetActionTimeout 设置行动超时时间处理函数 +// - 默认情况下行动超时时间函数将始终返回 0 +func (slf *TurnBased[CampID, EntityID, Camp, Entity]) SetActionTimeout(actionTimeoutHandler func(Camp, Entity) time.Duration) { + if actionTimeoutHandler == nil { + panic("actionTimeoutHandler can not be nil") + } + slf.actionTimeoutHandler = actionTimeoutHandler +} + +// Run 运行 +func (slf *TurnBased[CampID, EntityID, Camp, Entity]) Run() { + slf.round = 1 + slf.signal = make(chan byte, 1) + var actionDuration = make(map[EntityID]time.Duration) + var actionSubmit = func() { + slf.actionMutex.Lock() + slf.actioning = false + if slf.actionWaitTicker != nil { + slf.actionWaitTicker.Stop() + } + slf.actionMutex.Unlock() + } + for { + slf.closeMutex.RLock() + if slf.closed { + slf.closeMutex.RUnlock() + break + } + slf.closeMutex.RUnlock() + + var minDuration *time.Duration + var delay time.Duration + for _, entity := range slf.entities { + camp := slf.campRel[entity.GetId()] + next := slf.calcNextTurnDuration(camp, entity) + accumulate := next + actionDuration[entity.GetId()] + if minDuration == nil || accumulate < *minDuration { + minDuration = &accumulate + slf.currEntity = entity + slf.currCamp = camp + delay = next + } + } + if *minDuration == 0 { + *minDuration = 1 // 防止永远是第一对象行动 + } + actionDuration[slf.currEntity.GetId()] = *minDuration + if len(actionDuration) == len(slf.entities) { + for key := range actionDuration { + delete(actionDuration, key) + } + } + + if delay > 0 { + if slf.ticker == nil { + slf.ticker = time.NewTicker(delay) + } else { + slf.ticker.Reset(delay) + } + <-slf.ticker.C + } + + // 进入回合操作阶段 + slf.currActionTimeout = slf.actionTimeoutHandler(slf.currCamp, slf.currEntity) + slf.currStart = time.Now() + slf.actionMutex.Lock() + slf.actioning = true + slf.actionMutex.Unlock() + slf.OnTurnBasedEntitySwitchEvent(slf.controller) + if slf.actionWaitTicker == nil { + slf.actionWaitTicker = time.NewTicker(slf.currActionTimeout) + } else { + slf.actionWaitTicker.Reset(slf.currActionTimeout) + } + breakListen: + for { + select { + case <-slf.actionWaitTicker.C: + actionSubmit() + slf.OnTurnBasedEntityActionTimeoutEvent(slf.controller) + break breakListen + case sign := <-slf.signal: + switch sign { + case signalFinish: + actionSubmit() + slf.OnTurnBasedEntityActionFinishEvent(slf.controller) + break breakListen + } + } + } + slf.OnTurnBasedEntityActionSubmitEvent(slf.controller) + + slf.closeMutex.Lock() + if slf.closed { + if slf.ticker != nil { + slf.ticker.Stop() + slf.ticker = nil + } + if slf.actionWaitTicker != nil { + slf.actionWaitTicker.Stop() + slf.actionWaitTicker = nil + } + if slf.signal != nil { + close(slf.signal) + slf.signal = nil + } + slf.closeMutex.Unlock() + break + } + slf.closeMutex.Unlock() + + if len(actionDuration) == 0 { + slf.round++ + } + + } +} diff --git a/game/fight/turn_based_controller.go b/game/fight/turn_based_controller.go new file mode 100644 index 0000000..174ee70 --- /dev/null +++ b/game/fight/turn_based_controller.go @@ -0,0 +1,79 @@ +package fight + +import ( + "github.com/kercylan98/minotaur/utils/generic" + "time" +) + +type TurnBasedControllerInfo[CampID, EntityID comparable, Camp generic.IdR[CampID], Entity generic.IdR[EntityID]] interface { + // GetRound 获取当前回合数 + GetRound() int + // GetCamp 获取当前操作阵营 + GetCamp() Camp + // GetEntity 获取当前操作实体 + GetEntity() Entity + // GetActionTimeoutDuration 获取当前行动超时时长 + GetActionTimeoutDuration() time.Duration + // GetActionStartTime 获取当前行动开始时间 + GetActionStartTime() time.Time + // GetActionEndTime 获取当前行动结束时间 + GetActionEndTime() time.Time + // Stop 在当前回合执行完毕后停止回合进程 + Stop() +} + +type TurnBasedControllerAction[CampID, EntityID comparable, Camp generic.IdR[CampID], Entity generic.IdR[EntityID]] interface { + TurnBasedControllerInfo[CampID, EntityID, Camp, Entity] + // Finish 结束当前操作,将立即切换到下一个操作实体 + Finish() +} + +// TurnBasedController 回合制控制器 +type TurnBasedController[CampID, EntityID comparable, Camp generic.IdR[CampID], Entity generic.IdR[EntityID]] struct { + tb *TurnBased[CampID, EntityID, Camp, Entity] +} + +// GetRound 获取当前回合数 +func (slf *TurnBasedController[CampID, EntityID, Camp, Entity]) GetRound() int { + return slf.tb.round +} + +// GetCamp 获取当前操作阵营 +func (slf *TurnBasedController[CampID, EntityID, Camp, Entity]) GetCamp() Camp { + return slf.tb.currCamp +} + +// GetEntity 获取当前操作实体 +func (slf *TurnBasedController[CampID, EntityID, Camp, Entity]) GetEntity() Entity { + return slf.tb.currEntity +} + +// GetActionTimeoutDuration 获取当前行动超时时长 +func (slf *TurnBasedController[CampID, EntityID, Camp, Entity]) GetActionTimeoutDuration() time.Duration { + return slf.tb.currActionTimeout +} + +// GetActionStartTime 获取当前行动开始时间 +func (slf *TurnBasedController[CampID, EntityID, Camp, Entity]) GetActionStartTime() time.Time { + return slf.tb.currStart +} + +// GetActionEndTime 获取当前行动结束时间 +func (slf *TurnBasedController[CampID, EntityID, Camp, Entity]) GetActionEndTime() time.Time { + return slf.tb.currStart.Add(slf.tb.currActionTimeout) +} + +// Finish 结束当前操作,将立即切换到下一个操作实体 +func (slf *TurnBasedController[CampID, EntityID, Camp, Entity]) Finish() { + slf.tb.actionMutex.Lock() + defer slf.tb.actionMutex.Unlock() + if slf.tb.actioning { + slf.tb.actioning = false + slf.tb.signal <- signalFinish + } +} + +// Stop 在当前回合执行完毕后停止回合进程 +func (slf *TurnBasedController[CampID, EntityID, Camp, Entity]) Stop() { + slf.tb.Close() +} diff --git a/game/fight/turn_based_events.go b/game/fight/turn_based_events.go new file mode 100644 index 0000000..9e0759f --- /dev/null +++ b/game/fight/turn_based_events.go @@ -0,0 +1,72 @@ +package fight + +import "github.com/kercylan98/minotaur/utils/generic" + +type ( + TurnBasedEntitySwitchEventHandler[CampID, EntityID comparable, Camp generic.IdR[CampID], Entity generic.IdR[EntityID]] func(controller TurnBasedControllerAction[CampID, EntityID, Camp, Entity]) + TurnBasedEntityActionTimeoutEventHandler[CampID, EntityID comparable, Camp generic.IdR[CampID], Entity generic.IdR[EntityID]] func(controller TurnBasedControllerInfo[CampID, EntityID, Camp, Entity]) + TurnBasedEntityActionFinishEventHandler[CampID, EntityID comparable, Camp generic.IdR[CampID], Entity generic.IdR[EntityID]] func(controller TurnBasedControllerInfo[CampID, EntityID, Camp, Entity]) + TurnBasedEntityActionSubmitEventHandler[CampID, EntityID comparable, Camp generic.IdR[CampID], Entity generic.IdR[EntityID]] func(controller TurnBasedControllerInfo[CampID, EntityID, Camp, Entity]) +) + +type turnBasedEvents[CampID, EntityID comparable, Camp generic.IdR[CampID], Entity generic.IdR[EntityID]] struct { + entitySwitchEventHandlers []TurnBasedEntitySwitchEventHandler[CampID, EntityID, Camp, Entity] + actionTimeoutEventHandlers []TurnBasedEntityActionTimeoutEventHandler[CampID, EntityID, Camp, Entity] + actionFinishEventHandlers []TurnBasedEntityActionFinishEventHandler[CampID, EntityID, Camp, Entity] + actionSubmitEventHandlers []TurnBasedEntityActionSubmitEventHandler[CampID, EntityID, Camp, Entity] +} + +// RegTurnBasedEntitySwitchEvent 注册回合制实体切换事件处理函数,该处理函数将在切换到实体切换为操作时机时触发 +// - 刚函数通常仅用于告知当前操作实体已经完成切换,适合做一些前置校验,但不应该在该函数中执行长时间阻塞操作 +// - 操作计时将在该函数执行完毕后开始 +// +// 场景: +// - 回合开始,如果该实体被标记为已死亡,则跳过该实体 +func (slf *turnBasedEvents[CampID, EntityID, Camp, Entity]) RegTurnBasedEntitySwitchEvent(handler TurnBasedEntitySwitchEventHandler[CampID, EntityID, Camp, Entity]) { + slf.entitySwitchEventHandlers = append(slf.entitySwitchEventHandlers, handler) +} + +// OnTurnBasedEntitySwitchEvent 触发回合制实体切换事件 +func (slf *turnBasedEvents[CampID, EntityID, Camp, Entity]) OnTurnBasedEntitySwitchEvent(controller TurnBasedControllerAction[CampID, EntityID, Camp, Entity]) { + for _, handler := range slf.entitySwitchEventHandlers { + handler(controller) + } +} + +// RegTurnBasedEntityActionTimeoutEvent 注册回合制实体行动超时事件处理函数,该处理函数将在实体行动超时时触发 +func (slf *turnBasedEvents[CampID, EntityID, Camp, Entity]) RegTurnBasedEntityActionTimeoutEvent(handler TurnBasedEntityActionTimeoutEventHandler[CampID, EntityID, Camp, Entity]) { + slf.actionTimeoutEventHandlers = append(slf.actionTimeoutEventHandlers, handler) +} + +// OnTurnBasedEntityActionTimeoutEvent 触发回合制实体行动超时事件 +func (slf *turnBasedEvents[CampID, EntityID, Camp, Entity]) OnTurnBasedEntityActionTimeoutEvent(controller TurnBasedControllerInfo[CampID, EntityID, Camp, Entity]) { + for _, handler := range slf.actionTimeoutEventHandlers { + handler(controller) + } +} + +// RegTurnBasedEntityActionFinishEvent 注册回合制实体行动结束事件处理函数,该处理函数将在实体行动结束时触发 +func (slf *turnBasedEvents[CampID, EntityID, Camp, Entity]) RegTurnBasedEntityActionFinishEvent(handler TurnBasedEntityActionFinishEventHandler[CampID, EntityID, Camp, Entity]) { + slf.actionFinishEventHandlers = append(slf.actionFinishEventHandlers, handler) +} + +// OnTurnBasedEntityActionFinishEvent 触发回合制实体行动结束事件 +func (slf *turnBasedEvents[CampID, EntityID, Camp, Entity]) OnTurnBasedEntityActionFinishEvent(controller TurnBasedControllerInfo[CampID, EntityID, Camp, Entity]) { + for _, handler := range slf.actionFinishEventHandlers { + handler(controller) + } +} + +// RegTurnBasedEntityActionSubmitEvent 注册回合制实体行动提交事件处理函数,该处理函数将在实体行动提交时触发 +// - 该事件将在实体以任意方式结束行动时触发,包括正常结束、超时结束等 +// - 该事件会在原本的行动结束事件之后触发 +func (slf *turnBasedEvents[CampID, EntityID, Camp, Entity]) RegTurnBasedEntityActionSubmitEvent(handler TurnBasedEntityActionSubmitEventHandler[CampID, EntityID, Camp, Entity]) { + slf.actionSubmitEventHandlers = append(slf.actionSubmitEventHandlers, handler) +} + +// OnTurnBasedEntityActionSubmitEvent 触发回合制实体行动提交事件 +func (slf *turnBasedEvents[CampID, EntityID, Camp, Entity]) OnTurnBasedEntityActionSubmitEvent(controller TurnBasedControllerInfo[CampID, EntityID, Camp, Entity]) { + for _, handler := range slf.actionSubmitEventHandlers { + handler(controller) + } +} diff --git a/game/fight/turn_based_test.go b/game/fight/turn_based_test.go new file mode 100644 index 0000000..7fda339 --- /dev/null +++ b/game/fight/turn_based_test.go @@ -0,0 +1,56 @@ +package fight_test + +import ( + "github.com/kercylan98/minotaur/game/fight" + "testing" + "time" +) + +type Camp struct { + id string +} + +func (slf *Camp) GetId() string { + return slf.id +} + +type Entity struct { + id string + speed float64 +} + +func (slf *Entity) GetId() string { + return slf.id +} + +func TestTurnBased_Run(t *testing.T) { + tbi := fight.NewTurnBased[string, string, *Camp, *Entity](func(camp *Camp, entity *Entity) time.Duration { + return time.Duration(float64(time.Second) / entity.speed) + }) + + tbi.SetActionTimeout(func(camp *Camp, entity *Entity) time.Duration { + return time.Second * 5 + }) + + tbi.RegTurnBasedEntityActionTimeoutEvent(func(controller fight.TurnBasedControllerInfo[string, string, *Camp, *Entity]) { + t.Log("时间", time.Now().Unix(), "回合", controller.GetRound(), "阵营", controller.GetCamp().GetId(), "实体", controller.GetEntity().GetId(), "超时") + }) + + tbi.RegTurnBasedEntitySwitchEvent(func(controller fight.TurnBasedControllerAction[string, string, *Camp, *Entity]) { + switch controller.GetEntity().GetId() { + case "1": + go func() { + time.Sleep(time.Second * 2) + controller.Finish() + }() + case "2": + controller.Stop() + } + t.Log("时间", time.Now().Unix(), "回合", controller.GetRound(), "阵营", controller.GetCamp().GetId(), "实体", controller.GetEntity().GetId(), "开始行动") + }) + + tbi.AddCamp(&Camp{id: "1"}, &Entity{id: "1", speed: 1}, &Entity{id: "2", speed: 1}) + tbi.AddCamp(&Camp{id: "2"}, &Entity{id: "3", speed: 1}, &Entity{id: "4", speed: 1}) + + tbi.Run() +}