diff --git a/utils/arrangement/area.go b/utils/arrangement/area.go new file mode 100644 index 0000000..7ff2f0d --- /dev/null +++ b/utils/arrangement/area.go @@ -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) +} diff --git a/utils/arrangement/area_options.go b/utils/arrangement/area_options.go new file mode 100644 index 0000000..bdf5fe2 --- /dev/null +++ b/utils/arrangement/area_options.go @@ -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 + } +} diff --git a/utils/arrangement/arrangement.go b/utils/arrangement/arrangement.go new file mode 100644 index 0000000..628d8ce --- /dev/null +++ b/utils/arrangement/arrangement.go @@ -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 +} diff --git a/utils/arrangement/arrangement_test.go b/utils/arrangement/arrangement_test.go new file mode 100644 index 0000000..94df0ec --- /dev/null +++ b/utils/arrangement/arrangement_test.go @@ -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) +} diff --git a/utils/arrangement/item.go b/utils/arrangement/item.go new file mode 100644 index 0000000..a7a801f --- /dev/null +++ b/utils/arrangement/item.go @@ -0,0 +1,9 @@ +package arrangement + +// Item 编排成员 +type Item[ID comparable] interface { + // GetID 获取成员的唯一标识 + GetID() ID + // Equal 比较两个成员是否相等 + Equal(item Item[ID]) bool +} diff --git a/utils/arrangement/item_options.go b/utils/arrangement/item_options.go new file mode 100644 index 0000000..fba7f4b --- /dev/null +++ b/utils/arrangement/item_options.go @@ -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) + } +} diff --git a/utils/arrangement/options.go b/utils/arrangement/options.go new file mode 100644 index 0000000..6766c78 --- /dev/null +++ b/utils/arrangement/options.go @@ -0,0 +1,4 @@ +package arrangement + +// Option 编排选项 +type Option[ID comparable, AreaInfo any] func(arrangement *Arrangement[ID, AreaInfo])