feat: 新增 arrangement 包,用于针对多条数据进行合理编排的数据结构

This commit is contained in:
kercylan98 2023-08-03 12:24:09 +08:00
parent 7cfdbb12a4
commit 1f5f95ae6d
7 changed files with 312 additions and 0 deletions

44
utils/arrangement/area.go Normal file
View File

@ -0,0 +1,44 @@
package arrangement
import "github.com/kercylan98/minotaur/utils/hash"
// Area 编排区域
type Area[ID comparable, AreaInfo any] struct {
info AreaInfo
items map[ID]Item[ID]
constraints []AreaConstraintHandle[ID, AreaInfo]
evaluate AreaEvaluateHandle[ID, AreaInfo]
}
// GetAreaInfo 获取编排区域的信息
func (slf *Area[ID, AreaInfo]) GetAreaInfo() AreaInfo {
return slf.info
}
// GetItems 获取编排区域中的所有成员
func (slf *Area[ID, AreaInfo]) GetItems() map[ID]Item[ID] {
return slf.items
}
// IsAllow 检测一个成员是否可以被添加到该编排区域中
func (slf *Area[ID, AreaInfo]) IsAllow(item Item[ID]) (Item[ID], bool) {
for _, constraint := range slf.constraints {
if item, allow := constraint(slf, item); !allow {
return item, false
}
}
return nil, true
}
// GetScore 获取该编排区域的评估分数
// - 当 extra 不为空时,将会将 extra 中的内容添加到 items 中进行评估
func (slf *Area[ID, AreaInfo]) GetScore(extra ...Item[ID]) float64 {
if slf.evaluate == nil {
return 0
}
var items = hash.Copy(slf.items)
for _, item := range extra {
items[item.GetID()] = item
}
return slf.evaluate(slf.GetAreaInfo(), items)
}

View File

@ -0,0 +1,25 @@
package arrangement
// AreaOption 编排区域选项
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)
AreaEvaluateHandle[ID comparable, AreaInfo any] func(areaInfo AreaInfo, items map[ID]Item[ID]) float64
)
// WithAreaConstraint 设置编排区域的约束条件
// - 该约束用于判断一个成员是否可以被添加到该编排区域中
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)
}
}
// WithAreaEvaluate 设置编排区域的评估函数
// - 该评估函数将影响成员被编入区域的优先级
func WithAreaEvaluate[ID comparable, AreaInfo any](evaluate AreaEvaluateHandle[ID, AreaInfo]) AreaOption[ID, AreaInfo] {
return func(area *Area[ID, AreaInfo]) {
area.evaluate = evaluate
}
}

View File

@ -0,0 +1,153 @@
package arrangement
import (
"github.com/kercylan98/minotaur/utils/hash"
"sort"
)
// NewArrangement 创建一个新的编排
func NewArrangement[ID comparable, AreaInfo any](options ...Option[ID, AreaInfo]) *Arrangement[ID, AreaInfo] {
arrangement := &Arrangement[ID, AreaInfo]{
items: map[ID]Item[ID]{},
fixed: map[ID]ItemFixedAreaHandle[AreaInfo]{},
priority: map[ID][]ItemPriorityHandle[ID, AreaInfo]{},
}
for _, option := range options {
option(arrangement)
}
return arrangement
}
// Arrangement 用于针对多条数据进行合理编排的数据结构
// - 我不知道这个数据结构的具体用途,但是我觉得这个数据结构应该是有用的
// - 目前我能想到的用途只有我的过往经历:排课
// - 如果是在游戏领域,或许适用于多人小队匹配编排等类似情况
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] // 成员的优先级函数
}
// AddArea 添加一个编排区域
func (slf *Arrangement[ID, AreaInfo]) AddArea(areaInfo AreaInfo, options ...AreaOption[ID, AreaInfo]) {
area := &Area[ID, AreaInfo]{
info: areaInfo,
items: map[ID]Item[ID]{},
}
for _, option := range options {
option(area)
}
slf.areas = append(slf.areas, area)
}
// AddItem 添加一个成员
func (slf *Arrangement[ID, AreaInfo]) AddItem(item Item[ID]) {
slf.items[item.GetID()] = item
}
// Arrange 编排
func (slf *Arrangement[ID, AreaInfo]) Arrange(threshold int) (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)
// 将固定编排的成员添加到对应的编排区域中,当成员无法添加到对应的编排区域中时,将会被转移至未编排区域
for id, isFixed := range fixed {
var notFoundArea = true
for _, area := range slf.areas {
if isFixed(area.GetAreaInfo()) {
area.items[id] = items[id]
delete(items, id)
notFoundArea = false
break
}
}
if notFoundArea {
delete(fixed, id)
items[id] = slf.items[id]
}
}
// 优先级处理
var priorityInfo = map[ID]float64{} // itemID -> priority
var itemAreaPriority = map[ID]map[int]float64{} // itemID -> areaIndex -> priority
for id, item := range items {
itemAreaPriority[id] = map[int]float64{}
for i, area := range slf.areas {
for _, getPriority := range slf.priority[id] {
priority := getPriority(area.GetAreaInfo(), item)
priorityInfo[id] += priority
itemAreaPriority[id][i] = priority
}
}
}
var pending = hash.ToSlice(items)
sort.Slice(pending, func(i, j int) bool {
return priorityInfo[pending[i].GetID()] > priorityInfo[pending[j].GetID()]
})
var current Item[ID]
var fails []Item[ID]
var retryCount = 0
for len(pending) > 0 {
current = pending[0]
pending = 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 {
maxPriority = priority
area = a
}
}
}
if area == nil { // 无法通过优先级找到合适的编排区域
for i, a := range slf.areas {
if _, exist := itemAreaPriority[current.GetID()][i]; exist {
continue
}
if _, allow := a.IsAllow(current); allow {
area = a
break
}
}
if area == nil {
fails = append(fails, current)
goto end
}
}
area.items[current.GetID()] = current
end:
{
if len(fails) > 0 {
noSolution = map[ID]Item[ID]{}
for _, item := range fails {
noSolution[item.GetID()] = item
}
}
if len(pending) == 0 && len(fails) > 0 {
pending = fails
fails = fails[:0]
retryCount++
if retryCount > threshold {
break
}
}
}
}
return slf.areas, noSolution
}

View File

@ -0,0 +1,54 @@
package arrangement_test
import (
"fmt"
"github.com/kercylan98/minotaur/utils/arrangement"
"testing"
)
type Player struct {
ID int
}
func (slf *Player) GetID() int {
return slf.ID
}
func (slf *Player) Equal(item arrangement.Item[int]) bool {
return item.GetID() == slf.GetID()
}
type Team struct {
ID int
}
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: 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: 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})
for i := 0; i < 10; i++ {
a.AddItem(&Player{ID: i + 1})
}
res, no := a.Arrange(50)
for _, area := range res {
var str = fmt.Sprintf("area %d: ", area.GetAreaInfo().ID)
for id := range area.GetItems() {
str += fmt.Sprintf("%d ", id)
}
fmt.Println(str)
}
var noStr = "no: "
for _, i := range no {
noStr += fmt.Sprintf("%d ", i.GetID())
}
fmt.Println(noStr)
}

View File

@ -0,0 +1,9 @@
package arrangement
// Item 编排成员
type Item[ID comparable] interface {
// GetID 获取成员的唯一标识
GetID() ID
// Equal 比较两个成员是否相等
Equal(item Item[ID]) bool
}

View File

@ -0,0 +1,23 @@
package arrangement
// ItemOption 编排成员选项
type ItemOption[ID comparable, AreaInfo any] func(arrangement *Arrangement[ID, AreaInfo], item Item[ID])
type (
ItemFixedAreaHandle[AreaInfo any] func(areaInfo AreaInfo) bool
ItemPriorityHandle[ID comparable, AreaInfo any] func(areaInfo AreaInfo, item Item[ID]) float64
)
// WithItemFixed 设置成员的固定编排区域
func WithItemFixed[ID comparable, AreaInfo any](matcher ItemFixedAreaHandle[AreaInfo]) ItemOption[ID, AreaInfo] {
return func(arrangement *Arrangement[ID, AreaInfo], item Item[ID]) {
arrangement.fixed[item.GetID()] = matcher
}
}
// WithItemPriority 设置成员的优先级
func WithItemPriority[ID comparable, AreaInfo any](priority ItemPriorityHandle[ID, AreaInfo]) ItemOption[ID, AreaInfo] {
return func(arrangement *Arrangement[ID, AreaInfo], item Item[ID]) {
arrangement.priority[item.GetID()] = append(arrangement.priority[item.GetID()], priority)
}
}

View File

@ -0,0 +1,4 @@
package arrangement
// Option 编排选项
type Option[ID comparable, AreaInfo any] func(arrangement *Arrangement[ID, AreaInfo])