From 84f36eaabaaafa072777c393eafd27d03e6ebf2a Mon Sep 17 00:00:00 2001 From: kercylan98 Date: Thu, 3 Aug 2023 15:27:54 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20arrangement=20=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=E5=86=B2=E7=AA=81=E3=80=81=E5=86=B2=E7=AA=81=E5=A4=84=E7=90=86?= =?UTF-8?q?=E5=87=BD=E6=95=B0=E3=80=81=E7=BA=A6=E6=9D=9F=E5=A4=84=E7=90=86?= =?UTF-8?q?=E5=87=BD=E6=95=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- utils/arrangement/area.go | 51 ++++++++++++-- utils/arrangement/area_options.go | 13 +++- utils/arrangement/arrangement.go | 98 +++++++++++++++++++-------- utils/arrangement/arrangement_test.go | 22 ++++-- utils/arrangement/editor.go | 55 +++++++++++++++ utils/arrangement/options.go | 39 +++++++++++ 6 files changed, 240 insertions(+), 38 deletions(-) create mode 100644 utils/arrangement/editor.go diff --git a/utils/arrangement/area.go b/utils/arrangement/area.go index 7ff2f0d..0283d95 100644 --- a/utils/arrangement/area.go +++ b/utils/arrangement/area.go @@ -7,6 +7,7 @@ type Area[ID comparable, AreaInfo any] struct { info AreaInfo items map[ID]Item[ID] constraints []AreaConstraintHandle[ID, AreaInfo] + conflicts []AreaConflictHandle[ID, AreaInfo] evaluate AreaEvaluateHandle[ID, AreaInfo] } @@ -21,13 +22,55 @@ func (slf *Area[ID, AreaInfo]) GetItems() map[ID]Item[ID] { } // IsAllow 检测一个成员是否可以被添加到该编排区域中 -func (slf *Area[ID, AreaInfo]) IsAllow(item Item[ID]) (Item[ID], bool) { +func (slf *Area[ID, AreaInfo]) IsAllow(item Item[ID]) (constraintErr error, conflictItems map[ID]Item[ID], allow bool) { for _, constraint := range slf.constraints { - if item, allow := constraint(slf, item); !allow { - return item, false + if err := constraint(slf, item); err != nil { + return err, nil, false } } - return nil, true + for _, conflict := range slf.conflicts { + if items := conflict(slf, item); len(items) > 0 { + if conflictItems == nil { + conflictItems = make(map[ID]Item[ID]) + } + for id, item := range items { + conflictItems[id] = item + } + } + } + return nil, conflictItems, len(conflictItems) == 0 +} + +// IsConflict 检测一个成员是否会造成冲突 +func (slf *Area[ID, AreaInfo]) IsConflict(item Item[ID]) bool { + if hash.Exist(slf.items, item.GetID()) { + return false + } + for _, conflict := range slf.conflicts { + if items := conflict(slf, item); len(items) > 0 { + return true + } + } + return false +} + +// GetConflictItems 获取与一个成员产生冲突的所有其他成员 +func (slf *Area[ID, AreaInfo]) GetConflictItems(item Item[ID]) map[ID]Item[ID] { + if hash.Exist(slf.items, item.GetID()) { + return nil + } + var conflictItems map[ID]Item[ID] + for _, conflict := range slf.conflicts { + if items := conflict(slf, item); len(items) > 0 { + if conflictItems == nil { + conflictItems = make(map[ID]Item[ID]) + } + for id, item := range items { + conflictItems[id] = item + } + } + } + return conflictItems } // GetScore 获取该编排区域的评估分数 diff --git a/utils/arrangement/area_options.go b/utils/arrangement/area_options.go index bdf5fe2..8048d57 100644 --- a/utils/arrangement/area_options.go +++ b/utils/arrangement/area_options.go @@ -4,18 +4,29 @@ package arrangement type AreaOption[ID comparable, AreaInfo any] func(area *Area[ID, AreaInfo]) type ( - AreaConstraintHandle[ID comparable, AreaInfo any] func(area *Area[ID, AreaInfo], item Item[ID]) (Item[ID], bool) + AreaConstraintHandle[ID comparable, AreaInfo any] func(area *Area[ID, AreaInfo], item Item[ID]) error + AreaConflictHandle[ID comparable, AreaInfo any] func(area *Area[ID, AreaInfo], item Item[ID]) map[ID]Item[ID] AreaEvaluateHandle[ID comparable, AreaInfo any] func(areaInfo AreaInfo, items map[ID]Item[ID]) float64 ) // WithAreaConstraint 设置编排区域的约束条件 // - 该约束用于判断一个成员是否可以被添加到该编排区域中 +// - 与 WithAreaConflict 不同的是,约束通常用于非成员关系导致的硬性约束,例如:成员的等级过滤、成员的性别等 func WithAreaConstraint[ID comparable, AreaInfo any](constraint AreaConstraintHandle[ID, AreaInfo]) AreaOption[ID, AreaInfo] { return func(area *Area[ID, AreaInfo]) { area.constraints = append(area.constraints, constraint) } } +// WithAreaConflict 设置编排区域的冲突条件,冲突处理函数需要返回造成冲突的成员列表 +// - 该冲突用于判断一个成员是否可以被添加到该编排区域中 +// - 与 WithAreaConstraint 不同的是,冲突通常用于成员关系导致的软性约束,例如:成员的职业唯一性、成员的种族唯一性等 +func WithAreaConflict[ID comparable, AreaInfo any](conflict AreaConflictHandle[ID, AreaInfo]) AreaOption[ID, AreaInfo] { + return func(area *Area[ID, AreaInfo]) { + area.conflicts = append(area.conflicts, conflict) + } +} + // WithAreaEvaluate 设置编排区域的评估函数 // - 该评估函数将影响成员被编入区域的优先级 func WithAreaEvaluate[ID comparable, AreaInfo any](evaluate AreaEvaluateHandle[ID, AreaInfo]) AreaOption[ID, AreaInfo] { diff --git a/utils/arrangement/arrangement.go b/utils/arrangement/arrangement.go index 628d8ce..0f91d5f 100644 --- a/utils/arrangement/arrangement.go +++ b/utils/arrangement/arrangement.go @@ -23,10 +23,14 @@ func NewArrangement[ID comparable, AreaInfo any](options ...Option[ID, AreaInfo] // - 目前我能想到的用途只有我的过往经历:排课 // - 如果是在游戏领域,或许适用于多人小队匹配编排等类似情况 type Arrangement[ID comparable, AreaInfo any] struct { - areas []*Area[ID, AreaInfo] // 所有的编排区域 - items map[ID]Item[ID] // 所有的成员 - fixed map[ID]ItemFixedAreaHandle[AreaInfo] // 固定编排区域的成员 - priority map[ID][]ItemPriorityHandle[ID, AreaInfo] // 成员的优先级函数 + areas []*Area[ID, AreaInfo] // 所有的编排区域 + items map[ID]Item[ID] // 所有的成员 + fixed map[ID]ItemFixedAreaHandle[AreaInfo] // 固定编排区域的成员 + priority map[ID][]ItemPriorityHandle[ID, AreaInfo] // 成员的优先级函数 + threshold int // 重试次数阈值 + + constraintHandles []ConstraintHandle[ID, AreaInfo] + conflictHandles []ConflictHandle[ID, AreaInfo] } // AddArea 添加一个编排区域 @@ -47,13 +51,11 @@ func (slf *Arrangement[ID, AreaInfo]) AddItem(item Item[ID]) { } // Arrange 编排 -func (slf *Arrangement[ID, AreaInfo]) Arrange(threshold int) (areas []*Area[ID, AreaInfo], noSolution map[ID]Item[ID]) { +func (slf *Arrangement[ID, AreaInfo]) Arrange() (areas []*Area[ID, AreaInfo], noSolution map[ID]Item[ID]) { if len(slf.areas) == 0 { return slf.areas, slf.items } - if threshold <= 0 { - threshold = 10 - } + var items = hash.Copy(slf.items) var fixed = hash.Copy(slf.fixed) @@ -87,24 +89,26 @@ func (slf *Arrangement[ID, AreaInfo]) Arrange(threshold int) (areas []*Area[ID, } } } - var pending = hash.ToSlice(items) - sort.Slice(pending, func(i, j int) bool { - return priorityInfo[pending[i].GetID()] > priorityInfo[pending[j].GetID()] + var editor = &Editor[ID, AreaInfo]{ + a: slf, + pending: hash.ToSlice(items), + falls: map[ID]struct{}{}, + } + sort.Slice(editor.pending, func(i, j int) bool { + return priorityInfo[editor.pending[i].GetID()] > priorityInfo[editor.pending[j].GetID()] }) var current Item[ID] - var fails []Item[ID] - var retryCount = 0 - for len(pending) > 0 { - current = pending[0] - pending = pending[1:] + for editor.GetPendingCount() > 0 { + current = editor.pending[0] + editor.pending = editor.pending[1:] var maxPriority = float64(0) var area *Area[ID, AreaInfo] for areaIndex, priority := range itemAreaPriority[current.GetID()] { if priority > maxPriority { a := slf.areas[areaIndex] - if _, allow := a.IsAllow(current); allow { + if slf.try(editor, a, current) { maxPriority = priority area = a } @@ -116,33 +120,33 @@ func (slf *Arrangement[ID, AreaInfo]) Arrange(threshold int) (areas []*Area[ID, if _, exist := itemAreaPriority[current.GetID()][i]; exist { continue } - if _, allow := a.IsAllow(current); allow { + if slf.try(editor, a, current) { area = a - break } } if area == nil { - fails = append(fails, current) + editor.fails = append(editor.fails, current) goto end } } area.items[current.GetID()] = current + editor.falls[current.GetID()] = struct{}{} end: { - if len(fails) > 0 { + if len(editor.fails) > 0 { noSolution = map[ID]Item[ID]{} - for _, item := range fails { + for _, item := range editor.fails { noSolution[item.GetID()] = item } } - if len(pending) == 0 && len(fails) > 0 { - pending = fails - fails = fails[:0] - retryCount++ - if retryCount > threshold { + if len(editor.pending) == 0 && len(editor.fails) > 0 { + editor.pending = editor.fails + editor.fails = editor.fails[:0] + editor.retryCount++ + if editor.retryCount > slf.threshold { break } } @@ -151,3 +155,43 @@ func (slf *Arrangement[ID, AreaInfo]) Arrange(threshold int) (areas []*Area[ID, return slf.areas, noSolution } + +// try 尝试将 current 编排到 a 中 +func (slf *Arrangement[ID, AreaInfo]) try(editor *Editor[ID, AreaInfo], a *Area[ID, AreaInfo], current Item[ID]) bool { + err, conflictItems, allow := a.IsAllow(current) + if !allow { + if err != nil { + var solve = err + for _, handle := range slf.constraintHandles { + if solve = handle(editor, a, current, solve); solve == nil { + err, conflictItems, allow = a.IsAllow(current) + if allow { + break + } else { + // 当 err 依旧不为 nil 时,发生约束处理函数欺骗行为,不做任何处理 + if len(conflictItems) > 0 { + goto conflictHandle + } + } + break + } + } + } + conflictHandle: + { + if err == nil && len(conflictItems) > 0 { // 硬性约束未解决时,不考虑冲突解决 + var solve = conflictItems + for _, handle := range slf.conflictHandles { + if solve = handle(editor, a, current, solve); len(solve) == 0 { + if a.IsConflict(current) { + allow = true + break + } + // 依旧存在冲突时,表明冲突处理函数存在欺骗行为,不做任何处理 + } + } + } + } + } + return allow +} diff --git a/utils/arrangement/arrangement_test.go b/utils/arrangement/arrangement_test.go index 94df0ec..9cce3b7 100644 --- a/utils/arrangement/arrangement_test.go +++ b/utils/arrangement/arrangement_test.go @@ -1,6 +1,7 @@ package arrangement_test import ( + "errors" "fmt" "github.com/kercylan98/minotaur/utils/arrangement" "testing" @@ -24,14 +25,23 @@ type Team struct { func TestArrangement_Arrange(t *testing.T) { var a = arrangement.NewArrangement[int, *Team]() - a.AddArea(&Team{ID: 1}, arrangement.WithAreaConstraint[int, *Team](func(area *arrangement.Area[int, *Team], item arrangement.Item[int]) (arrangement.Item[int], bool) { - return nil, len(area.GetItems()) < 2 + a.AddArea(&Team{ID: 1}, arrangement.WithAreaConstraint[int, *Team](func(area *arrangement.Area[int, *Team], item arrangement.Item[int]) error { + if len(area.GetItems()) >= 2 { + return errors.New("too many") + } + return nil })) - a.AddArea(&Team{ID: 2}, arrangement.WithAreaConstraint[int, *Team](func(area *arrangement.Area[int, *Team], item arrangement.Item[int]) (arrangement.Item[int], bool) { - return nil, len(area.GetItems()) < 1 + a.AddArea(&Team{ID: 2}, arrangement.WithAreaConstraint[int, *Team](func(area *arrangement.Area[int, *Team], item arrangement.Item[int]) error { + if len(area.GetItems()) >= 1 { + return errors.New("too many") + } + return nil })) - a.AddArea(&Team{ID: 3}, arrangement.WithAreaConstraint[int, *Team](func(area *arrangement.Area[int, *Team], item arrangement.Item[int]) (arrangement.Item[int], bool) { - return nil, len(area.GetItems()) < 2 + a.AddArea(&Team{ID: 3}, arrangement.WithAreaConstraint[int, *Team](func(area *arrangement.Area[int, *Team], item arrangement.Item[int]) error { + if len(area.GetItems()) >= 2 { + return errors.New("too many") + } + return nil })) //a.AddArea(&Team{ID: 3}) for i := 0; i < 10; i++ { diff --git a/utils/arrangement/editor.go b/utils/arrangement/editor.go new file mode 100644 index 0000000..1a4e9f2 --- /dev/null +++ b/utils/arrangement/editor.go @@ -0,0 +1,55 @@ +package arrangement + +import ( + "github.com/kercylan98/minotaur/utils/hash" + "github.com/kercylan98/minotaur/utils/slice" +) + +// Editor 编排器 +type Editor[ID comparable, AreaInfo any] struct { + a *Arrangement[ID, AreaInfo] + pending []Item[ID] + fails []Item[ID] + falls map[ID]struct{} + retryCount int +} + +// GetPendingCount 获取待编排的成员数量 +func (slf *Editor[ID, AreaInfo]) GetPendingCount() int { + return len(slf.pending) +} + +// RemoveAreaItem 从编排区域中移除一个成员到待编排队列中,如果该成员不存在于编排区域中,则不进行任何操作 +func (slf *Editor[ID, AreaInfo]) RemoveAreaItem(area *Area[ID, AreaInfo], item Item[ID]) { + target := area.items[item.GetID()] + if target == nil { + return + } + delete(area.items, item.GetID()) + delete(slf.falls, item.GetID()) + slf.pending = append(slf.pending, target) +} + +// AddAreaItem 将一个成员添加到编排区域中,如果该成员已经存在于编排区域中,则不进行任何操作 +func (slf *Editor[ID, AreaInfo]) AddAreaItem(area *Area[ID, AreaInfo], item Item[ID]) { + if hash.Exist(slf.falls, item.GetID()) { + return + } + area.items[item.GetID()] = item + slf.falls[item.GetID()] = struct{}{} +} + +// GetAreas 获取所有的编排区域 +func (slf *Editor[ID, AreaInfo]) GetAreas() []*Area[ID, AreaInfo] { + return slice.Copy(slf.a.areas) +} + +// GetRetryCount 获取重试次数 +func (slf *Editor[ID, AreaInfo]) GetRetryCount() int { + return slf.retryCount +} + +// GetThresholdProgressRate 获取重试次数阈值进度 +func (slf *Editor[ID, AreaInfo]) GetThresholdProgressRate() float64 { + return float64(slf.retryCount) / float64(slf.a.threshold) +} diff --git a/utils/arrangement/options.go b/utils/arrangement/options.go index 6766c78..1ad32c4 100644 --- a/utils/arrangement/options.go +++ b/utils/arrangement/options.go @@ -2,3 +2,42 @@ package arrangement // Option 编排选项 type Option[ID comparable, AreaInfo any] func(arrangement *Arrangement[ID, AreaInfo]) + +type ( + ConstraintHandle[ID comparable, AreaInfo any] func(editor *Editor[ID, AreaInfo], area *Area[ID, AreaInfo], item Item[ID], err error) error + ConflictHandle[ID comparable, AreaInfo any] func(editor *Editor[ID, AreaInfo], area *Area[ID, AreaInfo], item Item[ID], conflictItems map[ID]Item[ID]) map[ID]Item[ID] +) + +// WithRetryThreshold 设置编排时的重试阈值 +// - 当每一轮编排结束任有成员未被编排时,将会进行下一轮编排,直到编排次数达到该阈值 +// - 默认的阈值为 10 次 +func WithRetryThreshold[ID comparable, AreaInfo any](threshold int) Option[ID, AreaInfo] { + return func(arrangement *Arrangement[ID, AreaInfo]) { + if threshold <= 0 { + threshold = 10 + } + arrangement.threshold = threshold + } +} + +// WithConstraintHandle 设置编排时触发约束时的处理函数 +// - 当约束条件触发时,将会调用该函数。如果无法在该函数中处理约束,应该继续返回 err,尝试进行下一层的约束处理 +// - 当该函数的返回值为 nil 时,表示约束已经被处理,将会命中当前的编排区域 +// - 当所有的约束处理函数都无法处理约束时,将会进入下一个编排区域的尝试,如果均无法完成,将会将该成员加入到编排队列的末端,等待下一次编排 +// +// 有意思的是,硬性约束应该永远是无解的,而当需要进行一些打破规则的操作时,则可以透过该函数传入的 editor 进行操作 +func WithConstraintHandle[ID comparable, AreaInfo any](handle ConstraintHandle[ID, AreaInfo]) Option[ID, AreaInfo] { + return func(arrangement *Arrangement[ID, AreaInfo]) { + arrangement.constraintHandles = append(arrangement.constraintHandles, handle) + } +} + +// WithConflictHandle 设置编排时触发冲突时的处理函数 +// - 当冲突条件触发时,将会调用该函数。如果无法在该函数中处理冲突,应该继续返回这一批成员,尝试进行下一层的冲突处理 +// - 当该函数的返回值长度为 0 时,表示冲突已经被处理,将会命中当前的编排区域 +// - 当所有的冲突处理函数都无法处理冲突时,将会进入下一个编排区域的尝试,如果均无法完成,将会将该成员加入到编排队列的末端,等待下一次编排 +func WithConflictHandle[ID comparable, AreaInfo any](handle ConflictHandle[ID, AreaInfo]) Option[ID, AreaInfo] { + return func(arrangement *Arrangement[ID, AreaInfo]) { + arrangement.conflictHandles = append(arrangement.conflictHandles, handle) + } +}