feat: arrangement 新增冲突、冲突处理函数、约束处理函数
This commit is contained in:
parent
978777e36c
commit
84f36eaaba
|
@ -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 获取该编排区域的评估分数
|
||||
|
|
|
@ -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] {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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++ {
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue