feat: fight 包新增 TurnBased 回合制数据结构,用于替代 fight.Round。解决并发安全问题,并且支持按照速度进行回合切换

This commit is contained in:
kercylan98 2023-10-17 12:09:34 +08:00
parent 42ab52bc66
commit 378f855992
5 changed files with 393 additions and 0 deletions

View File

@ -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]{

184
game/fight/turn_based.go Normal file
View File

@ -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++
}
}
}

View File

@ -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()
}

View File

@ -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)
}
}

View File

@ -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()
}