feat: 新增 arrangement 包,用于针对多条数据进行合理编排的数据结构
This commit is contained in:
parent
7cfdbb12a4
commit
1f5f95ae6d
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
package arrangement
|
||||
|
||||
// Item 编排成员
|
||||
type Item[ID comparable] interface {
|
||||
// GetID 获取成员的唯一标识
|
||||
GetID() ID
|
||||
// Equal 比较两个成员是否相等
|
||||
Equal(item Item[ID]) bool
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
package arrangement
|
||||
|
||||
// Option 编排选项
|
||||
type Option[ID comparable, AreaInfo any] func(arrangement *Arrangement[ID, AreaInfo])
|
Loading…
Reference in New Issue