diff --git a/game/builtin/ranking_list.go b/game/builtin/ranking_list.go new file mode 100644 index 0000000..d8059a5 --- /dev/null +++ b/game/builtin/ranking_list.go @@ -0,0 +1,207 @@ +package builtin + +import ( + "github.com/kercylan98/minotaur/utils/generic" + "github.com/kercylan98/minotaur/utils/synchronization" +) + +// NewRankingList 创建一个排行榜 +func NewRankingList[CompetitorID comparable, Score generic.Ordered](options ...RankingListOption[CompetitorID, Score]) *RankingList[CompetitorID, Score] { + rankingList := &RankingList[CompetitorID, Score]{ + rankCount: 100, + competitors: synchronization.NewMap[CompetitorID, Score](), + } + for _, option := range options { + option(rankingList) + } + return rankingList +} + +type RankingList[CompetitorID comparable, Score generic.Ordered] struct { + asc bool + rankCount int + competitors *synchronization.Map[CompetitorID, Score] + scores [][2]any // CompetitorID, Score +} + +func (slf *RankingList[CompetitorID, Score]) Competitor(competitorId CompetitorID, score Score) { + v, exist := slf.competitors.GetExist(competitorId) + if exist { + if slf.Cmp(v, score) == 0 { + return + } + rank, err := slf.GetRank(competitorId) + if err != nil { + return + } + slf.scores = append(slf.scores[0:rank], slf.scores[rank+1:]...) + slf.competitors.Delete(competitorId) + if slf.Cmp(score, v) > 0 { + slf.competitor(competitorId, score, 0, rank-1) + } else { + slf.competitor(competitorId, score, rank, len(slf.scores)-1) + } + } else { + if slf.rankCount > 0 && len(slf.scores) >= slf.rankCount { + last := slf.scores[len(slf.scores)-1] + if slf.Cmp(score, last[1]) <= 0 { + return + } + } + slf.competitor(competitorId, score, 0, len(slf.scores)-1) + } +} + +func (slf *RankingList[CompetitorID, Score]) RemoveCompetitor(competitorId CompetitorID) { + if !slf.competitors.Exist(competitorId) { + return + } + rank, err := slf.GetRank(competitorId) + if err != nil { + slf.competitors.Delete(competitorId) + return + } + slf.scores = append(slf.scores[0:rank], slf.scores[rank+1:]...) + slf.competitors.Delete(competitorId) +} + +func (slf *RankingList[CompetitorID, Score]) GetRank(competitorId CompetitorID) (int, error) { + competitorScore, exist := slf.competitors.GetExist(competitorId) + if !exist { + return 0, ErrRankingListNotExistCompetitor + } + + low, high := 0, len(slf.scores)-1 + for low <= high { + mid := (low + high) / 2 + data := slf.scores[mid] + id, score := data[0].(CompetitorID), data[1].(Score) + if id == competitorId { + return mid, nil + } else if slf.Cmp(score, competitorScore) == 0 { + for i := mid + 1; i <= high; i++ { + data := slf.scores[i] + if data[0].(CompetitorID) == competitorId { + return i, nil + } + } + for i := mid - 1; i >= low; i-- { + data := slf.scores[i] + if data[0].(CompetitorID) == competitorId { + return i, nil + } + } + } else if slf.Cmp(score, competitorScore) < 0 { + high = mid - 1 + } else { + low = mid + 1 + } + } + return 0, ErrRankingListIndexErr +} + +func (slf *RankingList[CompetitorID, Score]) GetCompetitor(rank int) (competitorId CompetitorID, err error) { + if rank < 0 || rank >= len(slf.scores) { + return competitorId, ErrRankingListNonexistentRanking + } + return slf.scores[rank][0], nil +} + +func (slf *RankingList[CompetitorID, Score]) GetCompetitorWithRange(start, end int) ([]CompetitorID, error) { + if start < 1 || end < start { + return nil, ErrRankingListNonexistentRanking + } + total := len(slf.scores) + if start > total { + return nil, ErrRankingListNonexistentRanking + } + if end > total { + end = total + } + var ids []CompetitorID + for _, data := range slf.scores[start-1 : end] { + ids = append(ids, data[0]) + } + return ids, nil +} + +func (slf *RankingList[CompetitorID, Score]) GetScore(competitorId CompetitorID) (score Score, err error) { + data, ok := slf.competitors.GetExist(competitorId) + if !ok { + return score, ErrRankingListNotExistCompetitor + } + return data, nil +} + +func (slf *RankingList[CompetitorID, Score]) GetAllCompetitor() []CompetitorID { + return slf.competitors.Keys() +} + +func (slf *RankingList[CompetitorID, Score]) Clear() { + slf.competitors.Clear() + slf.scores = make([][2]any, 0) +} + +func (slf *RankingList[CompetitorID, Score]) Cmp(s1, s2 Score) int { + var result int + if s1 > s2 { + result = 1 + } else if s1 < s2 { + result = -1 + } else { + result = 0 + } + if slf.asc { + return -result + } else { + return result + } +} + +func (slf *RankingList[CompetitorID, Score]) competitor(competitorId CompetitorID, score Score, low, high int) { + for low <= high { + mid := (low + high) / 2 + data := slf.scores[mid] + if slf.Cmp(data[1], score) == 0 { + for low = mid + 1; low <= high; low++ { + if slf.Cmp(slf.scores[low][1], score) != 0 { + break + } + } + } else if slf.Cmp(data[1], score) < 0 { + high = mid - 1 + } else { + low = mid + 1 + } + } + + count := len(slf.scores) + if low == count { + if slf.rankCount > 0 && count >= slf.rankCount { + return + } + + slf.scores = append(slf.scores, [2]any{competitorId, score}) + slf.competitors.Set(competitorId, score) + return + } + + scoreItem := [2]any{competitorId, score} + + //队首 + if low == 0 { + slf.scores = append([][2]any{scoreItem}, slf.scores...) + } else { + tmp := append([][2]any{scoreItem}, slf.scores[low:]...) + slf.scores = append(slf.scores[0:low], tmp...) + } + slf.competitors.Set(competitorId, score) + if slf.rankCount <= 0 || len(slf.scores) <= slf.rankCount { + return + } + + count = len(slf.scores) - 1 + scoreItem = slf.scores[count] + slf.competitors.Delete(scoreItem[0]) + slf.scores = slf.scores[0:count] +} diff --git a/game/builtin/ranking_list_errors.go b/game/builtin/ranking_list_errors.go new file mode 100644 index 0000000..3508086 --- /dev/null +++ b/game/builtin/ranking_list_errors.go @@ -0,0 +1,9 @@ +package builtin + +import "errors" + +var ( + ErrRankingListNotExistCompetitor = errors.New("ranking list not exist competitor") + ErrRankingListIndexErr = errors.New("ranking list index error") + ErrRankingListNonexistentRanking = errors.New("nonexistent ranking") +) diff --git a/game/builtin/ranking_list_options.go b/game/builtin/ranking_list_options.go new file mode 100644 index 0000000..1b5e857 --- /dev/null +++ b/game/builtin/ranking_list_options.go @@ -0,0 +1,24 @@ +package builtin + +import "github.com/kercylan98/minotaur/utils/generic" + +type RankingListOption[CompetitorID comparable, Score generic.Ordered] func(list *RankingList[CompetitorID, Score]) + +// WithRankingListCount 通过限制排行榜竞争者数量来创建排行榜 +// - 默认情况下允许100位竞争者 +func WithRankingListCount[CompetitorID comparable, Score generic.Ordered](rankCount int) RankingListOption[CompetitorID, Score] { + return func(list *RankingList[CompetitorID, Score]) { + if rankCount <= 0 { + rankCount = 1 + } + list.rankCount = rankCount + } +} + +// WithRankingListASC 通过升序的方式创建排行榜 +// - 默认情况下为降序 +func WithRankingListASC[CompetitorID comparable, Score generic.Ordered]() RankingListOption[CompetitorID, Score] { + return func(list *RankingList[CompetitorID, Score]) { + list.asc = true + } +} diff --git a/game/ranking_list.go b/game/ranking_list.go new file mode 100644 index 0000000..fa3365b --- /dev/null +++ b/game/ranking_list.go @@ -0,0 +1,23 @@ +package game + +// RankingList 排行榜 +type RankingList[CompetitorID comparable, Score comparable] interface { + // Competitor 声明排行榜竞争者 + // - 如果竞争者存在的情况下,会更新已有成绩,否则新增竞争者 + Competitor(competitorId CompetitorID, score Score) + // RemoveCompetitor 删除特定竞争者 + RemoveCompetitor(competitorId CompetitorID) + // GetRank 获取竞争者排名 + GetRank(competitorId CompetitorID) (int, error) + // GetCompetitor 获取特定排名的竞争者 + GetCompetitor(rank int) (CompetitorID, error) + // GetCompetitorWithRange 获取第start名到第end名竞争者 + GetCompetitorWithRange(start, end int) ([]CompetitorID, error) + // GetScore 获取竞争者成绩 + GetScore(competitorId CompetitorID) (Score, error) + // GetAllCompetitor 获取所有竞争者ID + // - 结果为名次有序的 + GetAllCompetitor() []CompetitorID + // Clear 清空排行榜 + Clear() +}