feat: arrangement 新增冲突、冲突处理函数、约束处理函数

This commit is contained in:
kercylan98 2023-08-03 15:27:54 +08:00
parent 978777e36c
commit 84f36eaaba
6 changed files with 240 additions and 38 deletions

View File

@ -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 获取该编排区域的评估分数

View File

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

View File

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

View File

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

View File

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

View File

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