最近利用业余时间开发一个支持多人对战游戏的天梯匹配系统,纯粹练手之用。该天梯系统需要满足以下要求
1. 有单人对战和多人对战模式,例如从1v1到5v5
2. 每个人都有两个天梯分,分别是1v1的天梯分,和2v2或以上对战的天梯分
3. 每局匹配的最高分和最低分玩家分差不能超过设定值
4. 每局匹配双方间分差不能超过设定值
5. 每个人可和一名或多名好友组队共同参与天梯匹配,组队后系统将计算组内天梯值平均分并适量加成作为匹配依据
6. 成功的匹配必须在对战双方同时包含相同的黑店数量。
7. 玩家选择好对战模式后,开始参与天梯匹配,如果匹配成功则返回对阵双方的匹配结果,否则返回空
实现思路概要
1. 匹配池按照<分数,参与玩家/组队列表>的键值对建立红黑树
2. 每一个新玩家/组队参与进来,将会根据对战模式在天梯池寻找合适玩家
3. 匹配过程首先会在天梯池中,选择预选玩家数量 = 对战所需玩家数量 + 一定buffer的玩家数量,选取规则为以当前新玩家分值为起点,分别向高低分数交替选择剩余玩家,直到满足预选玩家数量或者最高分和最低分玩家分差超过设定值
4. 如果预选玩家数量小于对战所需玩家数量 ,则把已选取的玩家和新玩家重新放回匹配池并退出
5. 对预选玩家进行排序,排序依据为队伍大小和新玩家/组队一样,则优先。否则按分数接近程度排列
6. 根据排序结果依次把新玩家和预选玩家交替放入匹配结果的两队中
7. 如果没有在对战双方匹配到的对等的黑店数量,则视为匹配失败
8. 最后根据对战双方天梯分差进行有必要的位置对调,使得两队天梯分总分差最小。
最新代码可从https://github.com/loveisasea/dmatch.git下载
项目采用web形式,可通过http的json请求测试,postman的url为地址。当然也可使用JUnit等工具测试,这就不一一列出了。
示例
其中匹配核心算法如下
package com.fym.match; import com.fym.core.err.OpException; import com.fym.core.err.OpResult; import com.fym.game.enm.GameType; import com.fym.match.obj.IUnit; import com.fym.match.obj.Match; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.InitializingBean; import org.springframework.stereotype.Component; import java.util.*; import java.util.concurrent.LinkedBlockingQueue; /** * Created by fengy on 2016/5/25. */ @Component public class MatchEngine implements InitializingBean { private static final Logger LOGGER = LoggerFactory.getLogger(MatchEngine.class); private Map<GameType, TreeMap<Integer, Queue<IUnit>>> matchPools = new HashMap<>(); private final static int Max_Score_Diff = 100; private final static int Buffer_Match = 4; public synchronized Match tryMatch(final IUnit mUnit, Integer gameTypeKey) throws OpException { return this.tryMatch(mUnit, GameType.get(gameTypeKey)); } /** * 匹配函数 * 思路: * 1. 先从参赛者分数震荡向高低两边取未匹配玩家和组队,需要符合最大分数差,而且选取数量带有一定buffer * 2. 把第一步选取出来的玩家按照规模大小接近和分值接近排序 * 3. 从结果集中 * @param mUnit * @param gameType * @return * @throws OpException */ public synchronized Match tryMatch(final IUnit mUnit, GameType gameType) throws OpException { TreeMap<Integer, Queue<IUnit>> matchpool = this.matchPools.get(gameType); if (matchpool == null) { throw new OpException(OpResult.INVALID, "找不到该比赛类型" + gameType); } if (mUnit == null || mUnit.getSize() == 0) { throw new OpException(OpResult.FAIL, "匹配玩家或队伍不能为空"); } if (gameType.pcnt < mUnit.getSize()) { throw new OpException(OpResult.FAIL, "队伍人数超过游戏类型人数限制"); } int restCnt = gameType.pcnt * 2 - mUnit.getSize() + Buffer_Match; //需要挑出来匹配的玩家总数量 List<IUnit> results = new ArrayList<>(restCnt); //初步挑出来匹配的玩家 //上下分数匹配 { Map.Entry<Integer, Queue<IUnit>> hEntry = matchpool.ceilingEntry(mUnit.getScore()); Map.Entry<Integer, Queue<IUnit>> lEntry = matchpool.floorEntry(mUnit.getScore()); //每次循环各取高低一个单位 while (restCnt > 0) { if (hEntry == null && lEntry == null) { break; } //寻找分高的玩家 while (hEntry != null && hEntry.getValue().size() == 0) { hEntry = matchpool.higherEntry(hEntry.getKey()); if (hEntry == null) { break; } } if (hEntry != null) { if (Math.abs(hEntry.getKey() - mUnit.getScore()) > Max_Score_Diff) { hEntry = null; } else { IUnit pickUnit = hEntry.getValue().poll(); if (pickUnit != null) { results.add(pickUnit); restCnt = restCnt - pickUnit.getSize(); } } } //寻找分低的玩家 while (lEntry != null && lEntry.getValue().size() == 0) { lEntry = matchpool.lowerEntry(lEntry.getKey()); if (lEntry == null) { break; } } if (lEntry != null) { if (Math.abs(lEntry.getKey() - mUnit.getScore()) > Max_Score_Diff) { lEntry = null; } else { IUnit pickUnit = lEntry.getValue().poll(); if (pickUnit != null) { results.add(pickUnit); restCnt = restCnt - pickUnit.getSize(); } } } } } Match match = new Match(gameType); List<IUnit> toReturn = results; //有足够的玩家进行二次挑选 if (restCnt <= Buffer_Match) { Collections.sort(results, new Comparator<IUnit>() { //玩家选择顺序规则 @Override public int compare(IUnit o1, IUnit o2) { int absSize1 = Math.abs(mUnit.getSize() - o1.getSize()); int absSize2 = Math.abs(mUnit.getSize() - o2.getSize()); if (absSize1 == absSize2) { return -1; } else { if (o1.getScore() < o2.getScore()) { return -1; } else if (o1.getScore() > o2.getScore()) { return 1; } else { return 0; } } } }); //开始进行二次挑选 List<IUnit> results2 = new ArrayList<>(results); match.team1.add(mUnit); IUnit lastTeam1 = mUnit; IUnit lastTeam2 = null; while (true) { //挑选team2,需要满足unit的大小等于team1 Iterator<IUnit> iter2 = results2.iterator(); lastTeam2 = null; while (match.team2size() < gameType.pcnt && iter2.hasNext()) { IUnit pickUnit = iter2.next(); if (pickUnit.getSize() == lastTeam1.getSize()) { lastTeam2 = pickUnit; match.team2.add(lastTeam2); iter2.remove(); break; } } //找不到team2,退出 if (lastTeam2 == null) { break; } //挑选team1 Iterator<IUnit> iter1 = results2.iterator(); lastTeam1 = null; while (match.team1size() < gameType.pcnt && iter1.hasNext()) { IUnit pickUnit = iter1.next(); if (pickUnit.getSize() + match.team1size() <= gameType.pcnt) { lastTeam1 = pickUnit; match.team1.add(lastTeam1); iter1.remove(); break; } } //找不到team1,退出 if (lastTeam1 == null) { break; } } if (match.team1size() == gameType.pcnt && match.team2size() == gameType.pcnt) { toReturn = results2; } else { toReturn.add(mUnit); //需要把当前玩家也加进去 match = null; } } else { toReturn.add(mUnit); //需要把当前玩家也加进去 match = null; } //还原 Iterator<IUnit> iReturn = toReturn.iterator(); while (iReturn.hasNext()) { IUnit picked = iReturn.next(); Queue<IUnit> queue = matchpool.get(picked.getScore()); if (queue == null) { queue = new LinkedBlockingQueue<>(); matchpool.put(picked.getScore(), queue); } queue.add(picked); } if (match == null) { return null; } else { //平衡team1和team2的分数 Match matchret = new Match(match.gameType); for (int i = 0; i < match.team1.size(); i++) { boolean team1higher = matchret.scoreDiff() > 0; boolean team2higher = (match.team2.get(i).getScore() - match.team1.get(i).getScore()) > 0; if (team1higher ^ team2higher) { matchret.team1.add(match.team2.get(i)); matchret.team2.add(match.team1.get(i)); } else { matchret.team1.add(match.team1.get(i)); matchret.team2.add(match.team2.get(i)); } } return match; } } public synchronized Map<GameType, Map<Integer, List<IUnit>>> getMatchPools() { Map ret = new HashMap(); for (Map.Entry<GameType, TreeMap<Integer, Queue<IUnit>>> mapEntry : this.matchPools.entrySet()) { HashMap<Integer, List<IUnit>> retEntry = new HashMap(); for (Map.Entry<Integer, Queue<IUnit>> entry : mapEntry.getValue().entrySet()) { retEntry.put(entry.getKey(), new ArrayList<IUnit>(entry.getValue())); } ret.put(mapEntry.getKey(), retEntry); } return ret; } public synchronized Map<Integer, List<IUnit>> getMatchPool(Integer gameTypeKey) throws OpException { TreeMap<Integer, Queue<IUnit>> matchpool = this.matchPools.get(GameType.get(gameTypeKey)); if (matchpool == null) { throw new OpException(OpResult.INVALID, "找不到该比赛类型" + gameTypeKey); } HashMap<Integer, List<IUnit>> ret = new HashMap(); for (Map.Entry<Integer, Queue<IUnit>> entry : matchpool.entrySet()) { ret.put(entry.getKey(), new ArrayList<IUnit>(entry.getValue())); } return ret; } public synchronized void cleanMatchPool(Integer gameTypeKey) throws OpException { TreeMap<Integer, Queue<IUnit>> matchpool = this.matchPools.get(GameType.get(gameTypeKey)); if (matchpool == null) { throw new OpException(OpResult.INVALID, "找不到该比赛类型" + gameTypeKey); } matchpool.clear(); } @Override public void afterPropertiesSet() throws Exception { for (GameType gameType : GameType.getall().values()) { matchPools.put(gameType, new TreeMap<Integer, Queue<IUnit>>()); } } }
数据结构
IUnit
package com.fym.match.obj; /** * * Created by fengy on 2016/5/25. */ public interface IUnit { int getScore(); int getSize(); }
Person
package com.fym.match.obj; /** * * Created by fengy on 2016/5/25. */ public class Person implements IUnit { /** * 玩家id */ private int pid; /** * 分值 */ private int score; public Person(int pid, int score) { this.pid = pid; this.score = score; } @Override public int getScore() { return this.score; } @Override public int getSize() { return 1; } }
Group
package com.fym.match.obj; import java.util.ArrayList; import java.util.Collection; import java.util.List; /** * * Created by fengy on 2016/5/25. */ public class Group implements IUnit { /** * 分数 */ private int score; /** * 玩家id */ private List<Person> persons; public Group(Collection<Person> persons) { this.persons = new ArrayList<>(persons); int sum = 0; for (Person person : this.persons) { sum += person.getScore(); } this.score = sum / persons.size() + persons.size() * 10; } @Override public int getScore() { return this.score; } @Override public int getSize() { return this.persons.size(); } }
Match
package com.fym.match.obj; import com.fym.game.enm.GameType; import java.util.ArrayList; import java.util.List; /** * * Created by fengy on 2016/5/25. */ public class Match { public GameType gameType; public List<IUnit> team1; public List<IUnit> team2; public Match(GameType gameType) { this.gameType = gameType; this.team1 = new ArrayList<>(gameType.pcnt); this.team2 = new ArrayList<>(gameType.pcnt); } public int team1size() { return this.teamsizeCore(this.team1); } public int team2size() { return this.teamsizeCore(this.team2); } private static int teamsizeCore(List<IUnit> team) { int size = 0; for (IUnit iUnit : team) { size = size + iUnit.getSize(); } return size; } public int team1score() { return this.teamscoreCore(this.team1); } public int team2score() { return this.teamscoreCore(this.team2); } public int scoreDiff() { return (this.team1score() - this.team2score()); } private static int teamscoreCore(List<IUnit> team) { int score = 0; for (IUnit iUnit : team) { score = score + iUnit.getSize(); } return score; } }