世界杯抽签模拟实验

背景

今天凌晨举行了2022卡塔尔世界杯小组抽签仪式,作为球迷,昨天下午我也模拟实现了该过程,今天分享给大家。
编写语言为java8,开发环境为Idea IC。

抽签规则

给非球迷朋友或对世界杯小组赛抽签规则不知道的明确一下抽签规则:

  1. 世界杯32强分为四档,每档8支球队;
  2. 同档不同组,非欧洲球队不同组,欧洲球队最多两支同组;
  3. 没有踢完洲际附加赛的球队直接划分到第四档,且抽签时提前排除该附加赛所涉及的大洲;
  4. 东道主卡塔尔落位A组第1(即A1),第一档其他球队分别落位各小组第1,但具体哪个小组需要抽签决定;
  5. 其余档次球队所在小组和小组位置均需要抽签决定;
  6. 抽签时按照第一档、第二档、第三档和第四档的顺序为球队进行抽签

我对第3条进行一下进一步的解释说明:由于种种原因,截至小组抽签前夕,本届世界杯32强有3个席位还需要通过欧洲附加赛或洲际附加赛来确定,这三个席位的附加赛对阵情况分别如下:

  1. 威尔士 对阵 苏格兰/乌克兰;
  2. 澳大利亚/阿联酋 对阵 秘鲁;
  3. 哥斯达黎加 对阵 新西兰。

第一组均为欧洲球队,小组抽签时适用“欧洲球队最多两支同组”即可;
第二组为亚洲和南美之间的对决,因此小组抽签时直接回避这两个大洲,只为其匹配欧洲、非洲或中北美洲;
第三组为中北美和大洋洲之间的对决,因此因此小组抽签时直接回避这两个大洲,只为其匹配欧洲、非洲、亚洲或南美洲。

实验设计

本次实验有六个类,分别是Team、Group、Rank、GroupPlan、Assigner和WorldCupDraw。
Team类负责封装球队信息,包括球队名称、档位、分组、大洲、以及是否分配小组;
Group类负责封装小组信息,包括组名、包含的球队、欧洲球队数量,并且对球队准入进行检查;
Rank类负责封装档位信息,包括档位(第一档、第二档、第三档和第四档)和该档位包含的球队;
GroupPlan类负责统计各种小组抽签结果及出现的次数;
Assigner类负责进行小组抽签;
WorldCupDraw类为入口类,负责初始化档位、球队和小组信息、启动小组赛抽签、输出结果并写入文件。

代码实现

Team类

这个类很简单,就是球队字段的定义、get/set方法、有参无参构造方法、toString()方法、hashcode()方法和equals()方法,如下所示:

package com.szc.wordCup;

import java.util.Objects;

public class Team {
    private String name; // 球队名称
    private String continent; // 所属大洲
    private Rank rank; // 档位
    private String group = ""; // 小组
    private boolean assigned = false; // 是否已经分配小组

    public Team() {
    }

    public Team(String name, String continent, Rank rank) {
        this.name = name;
        this.continent = continent;
        this.rank = rank;
        this.rank.addTeam(this);
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getContinent() {
        return continent;
    }

    public void setContinent(String continent) {
        this.continent = continent;
    }

    public Rank getRank() {
        return rank;
    }

    public void setRank(Rank rank) {
        this.rank = rank;
        this.rank.addTeam(this);
    }

    public String getGroup() {
        return group;
    }

    public void setGroup(Group group) {
        if (group == null || !group.checkNewTeam(this)) {
            return;
        }
        this.group = group.getGroupName();
        group.addTeam(this);
    }

    public void setAssigned(boolean assigned) {
        this.assigned = assigned;
    }

    public boolean isAssigned() {
        return assigned;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Team team = (Team) o;
        return rank == team.rank && Objects.equals(name, team.name) && Objects.equals(continent, team.continent) && Objects.equals(group, team.group);
    }

    @Override
    public int hashCode() {
        return Objects.hash(name, continent, rank, group);
    }

    @Override
    public String toString() {
        return "Team{" +
                "name='" + name + '\'' +
                ", continent='" + continent + '\'' +
                ", rank=" + rank +
                ", group='" + group + '\'' +
                '}';
    }
}

Group类

首先,该类需要定义三个字段:组名、该组包含的球队和当前组内欧洲球队的数量,如下所示:

public class Group {
    private final List<Team> teamsContained = new ArrayList<>();
    // 该组包含的球队
    private String groupName; // 组名
    private int europeCount = 0; // 当前组内欧洲球队数

	......
}

由于只有组名跟抽签无关,因此针对组名实现get/set方法和有参无参构造方法:

    public Group() {
    }

    public Group(String groupName) {
        this.groupName = groupName;
    }

    public void setGroupName(String groupName) {
        this.groupName = groupName;
    }

    public String getGroupName() {
        return groupName;
    }

为了实现控制球队准入,需要实现抽签规则中的第二条,对应代码如下所示:

    public boolean checkNewTeam(Team newTeam) {
        if (newTeam == null || teamsContained.size() >= 4) {
            return false;
        }

        for (Team currentTeam : teamsContained) {
            if (currentTeam.getRank() == newTeam.getRank()) {
                // 同档不同组
                return false;
            }

			// 对新队伍的所属大洲进行检查
            String newContinent = newTeam.getContinent();
            boolean isNewEurope = newTeam.getContinent().equals("欧洲");
            String oldContinent = currentTeam.getContinent();
            if (!isNewEurope
                    && (newContinent.contains(oldContinent) || oldContinent.contains(newContinent))) {
                // 非欧洲球队,不能同组,同时考虑未完成的洲际附加赛
                return false;
            } else if (isNewEurope && europeCount == 2){
                // 欧洲球队,最多两个同组
                return false;
            }
        }

        return true;
    }

其中,针对未完成的附加赛的考虑,首先是欧洲附加赛,这个不用额外实现,因为该附加赛参赛球队均为第四档欧洲球队,不影响抽签;其次是洲际附加赛,参赛球队档位固定为第四档,所属大洲的字段格式为“大洲1/大洲2/…/大洲n”,比如“亚洲/南美”,因此只需要判断新旧球队中的大洲字段是否彼此包含,即可决定是否存在非欧洲球队同洲冲突的情况。
新球队入组的实现如下,在调用此方法前,需要调用checkNewTeam()进行准入判断:

    public void addTeam(Team team) {
        teamsContained.add(team);
        if (team.getContinent().equals("欧洲")) {
            europeCount++;
        }
    }

再实现一下返回目前包含的球队数和清空组内球队:

    public int getTeamCount() {
        return teamsContained.size();
    }

    public void clearTeam() {
        teamsContained.clear();
        europeCount = 0;
    }

针对抽签规则中对非种子球队组内位置的随机抽取,实现方法randomPositionNonFirstTeams(),代码如下:

    private void randomPositionNonFirstTeams() {
        Random random = new Random();
        Team first = teamsContained.get(0);
        Team[] teamNonFirst = new Team[3]; // 组内非种子队
        int[] teamPicked = new int[3]; // 非种子队是否被选取,0表示未被选取,1表示已被选取
        for (int i = 0; i < 3; i++) {
            int randomIndex = 0;
            do {
                randomIndex = random.nextInt(3); // 非种子队的随机位置
            } while (teamNonFirst[randomIndex] != null && teamPicked[randomIndex] == 1);
            Team team = teamsContained.get(i + 1); // 当前非种子队
            teamNonFirst[randomIndex] = team; // 将非种子队定位到选好的随机位置
            teamPicked[randomIndex] = 1;
        }

		// 按序将种子队和非种子队重新写入teamsContained
        teamsContained.clear();
        teamsContained.add(first);
        teamsContained.addAll(Arrays.asList(teamNonFirst));
    }

该方法适用于小组抽签已经完成,只需要对组内非种子队进行位置混洗的情况,因此适合在toString()输出时调用。
最后,实现toString()方法,用以输出组内球队:

    @Override
    public String toString() {
        randomPositionNonFirstTeams();

        StringBuilder builder = new StringBuilder();

        builder.append("\t\tGroup").append(groupName).append(":\n");
        for (Team team : teamsContained) {
            builder.append("\t\t\t").append(team.getName()).append("\t");
//                    .append("\t").append(team.getContinent()).append("\t\t\t")
//                    .append("\t").append(team.getRank().getRank()).append("\n");
        }

        builder.append("\n\t\t====================================================================================\n");

        return builder.toString();
    }

Group类全部代码如下所示:

package com.szc.wordCup;

import java.util.*;

public class Group {
    private final List<Team> teamsContained = new ArrayList<>();
    private String groupName;
    private int europeCount = 0;

    public Group() {
    }

    public Group(String groupName) {
        this.groupName = groupName;
    }

    public void setGroupName(String groupName) {
        this.groupName = groupName;
    }

    public String getGroupName() {
        return groupName;
    }

    public boolean checkNewTeam(Team newTeam) {
        if (newTeam == null || teamsContained.size() >= 4) {
            return false;
        }

        for (Team currentTeam : teamsContained) {
            if (currentTeam.getRank() == newTeam.getRank()) {
                // 同档不同组
                return false;
            }

            String newContinent = newTeam.getContinent();
            boolean isNewEurope = newTeam.getContinent().equals("欧洲");
            String oldContinent = currentTeam.getContinent();
            if (!isNewEurope
                    && (newContinent.contains(oldContinent) || oldContinent.contains(newContinent))) {
                // 非欧洲球队,不能同组,同时考虑未完成的洲际附加赛
                return false;
            } else if (isNewEurope && europeCount == 2){
                // 欧洲球队,最多两个同组
                return false;
            }
        }

        return true;
    }

    public void addTeam(Team team) {
        teamsContained.add(team);
        if (team.getContinent().equals("欧洲")) {
            europeCount++;
        }
    }

    public int getTeamCount() {
        teamsContained.removeIf(Objects::isNull);
        return teamsContained.size();
    }

    public void clearTeam() {
        teamsContained.clear();
        europeCount = 0;
    }

    @Override
    public String toString() {
        randomPositionNonFirstTeams();

        StringBuilder builder = new StringBuilder();

        builder.append("\t\tGroup").append(groupName).append(":\n");
        for (Team team : teamsContained) {
            builder.append("\t\t\t").append(team.getName()).append("\t");
//                    .append("\t").append(team.getContinent()).append("\t\t\t")
//                    .append("\t").append(team.getRank().getRank()).append("\n");
        }

        builder.append("\n\t\t====================================================================================\n");

        return builder.toString();
    }

    private void randomPositionNonFirstTeams() {
        Random random = new Random();
        Team first = teamsContained.get(0);
        Team[] teamNonFirst = new Team[3];
        int[] teamPicked = new int[3];
        for (int i = 0; i < 3; i++) {
            int randomIndex = 0;
            do {
                randomIndex = random.nextInt(3);
            } while (teamNonFirst[randomIndex] != null && teamPicked[randomIndex] == 1);
            Team team = teamsContained.get(i + 1);
            teamNonFirst[randomIndex] = team;
            teamPicked[randomIndex] = 1;
        }
        teamsContained.clear();
        teamsContained.add(first);
        teamsContained.addAll(Arrays.asList(teamNonFirst));
    }
}

Rank

Rank类负责封装档位和该档位包含的球队,以及向该档位内添加球队,比较简单,代码如下所示:

package com.szc.wordCup;

import java.util.ArrayList;
import java.util.List;

public class Rank {
    private int rank; // 档位
    private final List<Team> teamsContained = new ArrayList<>(); // 该档位包含的球队

    public Rank(int rank) {
        this.rank = rank;
    }

    public void setRank(int rank) {
        this.rank = rank;
    }

    public int getRank() {
        return rank;
    }

    public List<Team> getTeamsContained() {
        return teamsContained;
    }

    public void addTeam(Team team) {
        if (!teamsContained.contains(team)) {
            teamsContained.add(team);
        }
    }
}

GroupPlan

GroupPlan类负责统计各个小组分配情况及出现的次数,以及有序输出。首先,实现单例:

    private static volatile GroupPlan sInstance;
    
    private GroupPlan() {
    }

    public static GroupPlan getInstance() {
        if (sInstance == null) {
            synchronized (GroupPlan.class) {
                if (sInstance == null) {
                    sInstance = new GroupPlan();
                }
            }
        }
        return sInstance;
    }

统计信息保存在映射groupPlan里,键为小组分配情况,值为该情况出现的频率,首先定义该字段:

private HashMap<String, Integer> groupPlan = new HashMap<>();

然后先实现对其的写入:

    public void confirmDrawResult(Group[] groups) {
        if (groups == null || groups.length == 0) {
            return;
        }

        StringBuilder builder = new StringBuilder();

        for (Group group : groups) {
            builder.append(group.toString());
        }
        String currentGroupPlan = builder.toString();

		// 统计信息里不存在该小组,则频率为1;否则频率+1
        if (!groupPlan.containsKey(currentGroupPlan)) {
            groupPlan.put(currentGroupPlan, 1);
        } else {
            groupPlan.put(currentGroupPlan, groupPlan.get(currentGroupPlan) + 1);
        }
    }

最后实现小组分配图的降序输出:

    /**
     * 输出出现次数前20的对阵情况,如果小组对阵图缓存量 <= 20,则输出全部对阵情况
     * @return 全部对阵情况的字符串
     */
    public String printAllGroupPlan() {
        return printAllGroupPlan(20);
    }

    /**
     * 输出出现次数前topN的对阵情况,如果对小组阵图缓存量 <= topN,则输出全部对阵情况
     * @param topN 截取出现次数的前topN名
     * @return 全部对阵情况的字符串
     */
    public String printAllGroupPlan(int topN) {
        StringBuilder builder = new StringBuilder();

        Integer[] matchCountSorted = new Integer[groupPlan.size()];
        int i = 0;
        int totalCount = 0;
        for (String opposite : groupPlan.keySet()) {
            matchCountSorted[i] = groupPlan.get(opposite);
            totalCount += matchCountSorted[i];
            i++;
        }

        Arrays.sort(matchCountSorted, Collections.reverseOrder());

        // 对阵情况的topN截取
        Integer[] matchCountFiltered;
        if (matchCountSorted.length > topN) {
            matchCountFiltered = Arrays.copyOfRange(matchCountSorted, 0, topN);
        } else {
            matchCountFiltered = Arrays.copyOf(matchCountSorted, matchCountSorted.length);
        }

		// 记录某小组图是否完成输出,0为没有输出,1为已经输出
        List<Integer> appended = new ArrayList<Integer>();
        for (int j = 0; j < groupPlan.size(); j++) {
            appended.add(0);
        }

		// 字符串拼接,结果的组装
        builder.append("{ \n");
        for (Integer value : matchCountFiltered) {
            int index = 0;
            for (Map.Entry<String, Integer> entry : groupPlan.entrySet()) {
                if (!groupPlan.get(entry.getKey()).equals(value) || appended.get(index) == 1) {
                    index++;
                    continue;
                }
                builder.append("\t").append("小组对阵情况: {\n")
                        .append(entry.getKey())
                        .append("\t\t出现次数:").append(entry.getValue())
                        .append("\n\t\t出现概率:").append(String.format("%.2f", 100 * entry.getValue() / (float) totalCount))
                        .append("%\n\t},\n");
                appended.set(index, 1);
                index++;
            }
        }

        int lastSplitterIndex = builder.lastIndexOf(",\n");
        if (lastSplitterIndex != -1) {
            builder.delete(lastSplitterIndex, lastSplitterIndex + ",\n".length() - 1);
        }
        builder.append("}");

        return builder.toString();
    }

该类全部代码如下所示:

package com.szc.wordCup;

import java.util.*;

public class GroupPlan {
    private static volatile GroupPlan sInstance;
    private HashMap<String, Integer> groupPlan = new HashMap<>();

    private GroupPlan() {
    }

    public static GroupPlan getInstance() {
        if (sInstance == null) {
            synchronized (GroupPlan.class) {
                if (sInstance == null) {
                    sInstance = new GroupPlan();
                }
            }
        }
        return sInstance;
    }

    public void confirmDrawResult(Group[] groups) {
        if (groups == null || groups.length == 0) {
            return;
        }

        StringBuilder builder = new StringBuilder();

        for (Group group : groups) {
            builder.append(group.toString());
        }
        String currentGroupPlan = builder.toString();

        if (!groupPlan.containsKey(currentGroupPlan)) {
            groupPlan.put(currentGroupPlan, 1);
        } else {
            groupPlan.put(currentGroupPlan, groupPlan.get(currentGroupPlan) + 1);
        }
    }

    /**
     * 输出出现次数前20的对阵情况,如果小组对阵图缓存量 <= 20,则输出全部对阵情况
     * @return 全部对阵情况的字符串
     */
    public String printAllGroupPlan() {
        return printAllGroupPlan(20);
    }

    /**
     * 输出出现次数前topN的对阵情况,如果对小组阵图缓存量 <= topN,则输出全部对阵情况
     * @param topN 截取出现次数的前topN名
     * @return 全部对阵情况的字符串
     */
    public String printAllGroupPlan(int topN) {
        StringBuilder builder = new StringBuilder();

        Integer[] matchCountSorted = new Integer[groupPlan.size()];
        int i = 0;
        int totalCount = 0;
        for (String opposite : groupPlan.keySet()) {
            matchCountSorted[i] = groupPlan.get(opposite);
            totalCount += matchCountSorted[i];
            i++;
        }

        Arrays.sort(matchCountSorted, Collections.reverseOrder());

        // 对阵情况的topN截取
        Integer[] matchCountFiltered;
        if (matchCountSorted.length > topN) {
            matchCountFiltered = Arrays.copyOfRange(matchCountSorted, 0, topN);
        } else {
            matchCountFiltered = Arrays.copyOf(matchCountSorted, matchCountSorted.length);
        }

        List<Integer> appended = new ArrayList<Integer>();
        for (int j = 0; j < groupPlan.size(); j++) {
            appended.add(0);
        }

        builder.append("{ \n");
        for (Integer value : matchCountFiltered) {
            int index = 0;
            for (Map.Entry<String, Integer> entry : groupPlan.entrySet()) {
                if (!groupPlan.get(entry.getKey()).equals(value) || appended.get(index) == 1) {
                    index++;
                    continue;
                }
                builder.append("\t").append("小组对阵情况: {\n")
                        .append(entry.getKey())
                        .append("\t\t出现次数:").append(entry.getValue())
                        .append("\n\t\t出现概率:").append(String.format("%.2f", 100 * entry.getValue() / (float) totalCount))
                        .append("%\n\t},\n");
                appended.set(index, 1);
                index++;
            }
        }

        int lastSplitterIndex = builder.lastIndexOf(",\n");
        if (lastSplitterIndex != -1) {
            builder.delete(lastSplitterIndex, lastSplitterIndex + ",\n".length() - 1);
        }
        builder.append("}");

        return builder.toString();
    }
}

Assigner

Assigner类是抽签的实现类,三个方法实现三个功能:canAssign()判断当前档位剩余球队能否进入所有小组中;tryAssign()尝试对所有档位球队进行一次小组分配;assignGroup()负责控制整个抽签过程。
canAssign()方法代码如下所示,如果有一个队伍没有可供分配的小组,即返回true,否则返回false:

    private static boolean canNotAssign(Group[] allGroups, List<Team> unassignedTeams) {
        // 当前未分配的球队中,是否存在球队不能分到任何小组
        for (Team unassignedTeam : unassignedTeams) {
            for (Group group : allGroups) {
                if (group.checkNewTeam(unassignedTeam)) {
                    return false;
                }
            }
        }
        return true;
    }

tryAssign()方法代码如下所示:

    private static boolean tryAssign(Rank[] allRanks, Group[] allGroups) {
        Random random = new Random();
        for (Rank rank : allRanks) {
            // 遍历所有档位的未分配球队,从而对某个档位进行分组
            List<Team> unassignedTeams = new ArrayList<>(rank.getTeamsContained());
            // 球队若已被分配,则从unassignedTeams中移除
            unassignedTeams.removeIf(Team::isAssigned); 
            
            while (!unassignedTeams.isEmpty()) {
                // 是否存在不能分配的未分配队伍,是就重新开始分组
                if (canNotAssign(allGroups, unassignedTeams)) {
                    return false;
                }

                // 随机抽取球队和小组
                int randomTeamIndex = random.nextInt(unassignedTeams.size());
                int randomGroupIndex = random.nextInt(8);

                Team randomTeam = unassignedTeams.get(randomTeamIndex);
                Group randomGroup = allGroups[randomGroupIndex];

                if (randomGroup.checkNewTeam(randomTeam)) {
                    // 若能将该球队分配到该小组,则进行分配
                    randomGroup.addTeam(randomTeam);
                    randomTeam.setGroup(randomGroup);
                    randomTeam.setAssigned(true);

                    unassignedTeams.remove(randomTeam);
                }
            }
        }
        return true;
    }

核心思想是:对所有档位进行遍历,迭代某个档位时,将该档位所有未分配小组的球队缓存到unassignedTeams中,并开启对unassignedTeams的遍历。遍历unassignedTeams时,如果当前未分配球队不能完成一次小组分配(即canNotAssign()方法返回true),则退出tryAssign()方法,返回false;否则随机抽取球队和小组,进行小组准入判断,若通过,则进行球队小组分配,并从unassignedTeams中移除该球队。
assignGroup()方法代码如下所示:

    public static void assignGroup(Rank[] allRanks, Group[] allGroups) {
        if (allRanks == null || allRanks.length != 4) {
            return;
        }

        if (allGroups == null || allGroups.length != 8) {
            return;
        }

        boolean allGroupFull = false;

        while (!allGroupFull) {
            // 本轮分组失败,重新分组
            if (!tryAssign(allRanks, allGroups)) {
                for (Group group : allGroups) {
                    group.clearTeam();
                }
                for (Rank rank : allRanks) {
                    for (Team team : rank.getTeamsContained()) {
                        team.setAssigned(false);
                        team.setGroup(null);
                    }
                }

                // 设置东道主的位置
                Team hostTeam = allRanks[0].getTeamsContained().get(0);
                hostTeam.setGroup(allGroups[0]);
                hostTeam.setAssigned(true);
                continue;
            }

            // 所有档位分组完毕,检查各小组是否有空位
            boolean hasEmpty = false;
            for (Group group: allGroups) {
                if (group.getTeamCount() < 4) {
                    hasEmpty = true;
                }
            }

            allGroupFull = !hasEmpty;
        }

        // 提交小组分配信息
        GroupPlan.getInstance().confirmDrawResult(allGroups);
    }

核心思想是:先对档位和小组进行合法性判断,然后进行循环分组,直到所有小组均满结束(即allGroupFull为true)。每次迭代时,先通过tryAssign()方法尝试一次小组分配,若失败(返回false),则说明有球队没有合适的小组,需要重新抽签,因此要重置所有小组和所有球队的抽签情况,并且重置东道主的位置,而后进行下一次迭代;否则,即进行了一次成功的小组分配,接下来要判断所有小组的球队数量是否<4,如果存在不满4支球队的小组,则将allGroupFull置为false,表示需要继续抽签。
最后,小组抽签结束后,向GroupPlan提交小组的分配信息。
Assigner类的所有代码如下所示:

package com.szc.wordCup;

import java.util.ArrayList;
import java.util.List;
import java.util.Random;

public class Assigner {
    public static void assignGroup(Rank[] allRanks, Group[] allGroups) {
        if (allRanks == null || allRanks.length != 4) {
            return;
        }

        if (allGroups == null || allGroups.length != 8) {
            return;
        }

        boolean allGroupFull = false;

        while (!allGroupFull) {
            // 本轮分组失败,重新分组
            if (!tryAssign(allRanks, allGroups)) {
                for (Group group : allGroups) {
                    group.clearTeam();
                }
                for (Rank rank : allRanks) {
                    for (Team team : rank.getTeamsContained()) {
                        team.setAssigned(false);
                        team.setGroup(null);
                    }
                }

                // 设置东道主的位置
                Team hostTeam = allRanks[0].getTeamsContained().get(0);
                hostTeam.setGroup(allGroups[0]);
                hostTeam.setAssigned(true);
                continue;
            }

            // 所有档位分组完毕,检查各小组是否有空位
            boolean hasEmpty = false;
            for (Group group: allGroups) {
                if (group.getTeamCount() < 4) {
                    hasEmpty = true;
                }
            }

            allGroupFull = !hasEmpty;
        }

        // 提交小组分配信息
        GroupPlan.getInstance().confirmDrawResult(allGroups);
    }

    private static boolean tryAssign(Rank[] allRanks, Group[] allGroups) {
        Random random = new Random();
        for (Rank rank : allRanks) {
            // 遍历所有档位的未分配球队,从而对某个档位进行分组
            List<Team> unassignedTeams = new ArrayList<>(rank.getTeamsContained());
            unassignedTeams.removeIf(Team::isAssigned);

            while (!unassignedTeams.isEmpty()) {
                // 是否存在不能分配的未分配队伍,是就重新开始分组
                if (canNotAssign(allGroups, unassignedTeams)) {
                    return false;
                }

                // 随机抽取球队和小组
                int randomTeamIndex = random.nextInt(unassignedTeams.size());
                int randomGroupIndex = random.nextInt(8);

                Team randomTeam = unassignedTeams.get(randomTeamIndex);
                Group randomGroup = allGroups[randomGroupIndex];

                if (randomGroup.checkNewTeam(randomTeam)) {
                    // 若能将该球队分配到该小组,则进行分配
                    randomGroup.addTeam(randomTeam);
                    randomTeam.setGroup(randomGroup);
                    randomTeam.setAssigned(true);

                    unassignedTeams.remove(randomTeam);
                }
            }
        }
        return true;
    }

    private static boolean canNotAssign(Group[] allGroups, List<Team> unassignedTeams) {
        // 当前未分配的球队中,是否存在球队不能分到任何小组
        for (Team unassignedTeam : unassignedTeams) {
            for (Group group : allGroups) {
                if (group.checkNewTeam(unassignedTeam)) {
                    return false;
                }
            }
        }
        return true;
    }
}

WorldCupDraw

WorldCupDraw是本次模拟实验的入口类,负责档位、球队、小组等信息的初始化、抽签的启动和结果输出。
首先,初始化档位、球队和小组的信息:

    public void setAllRanks() {
        allRanks[0] = new Rank(1);
        allRanks[1] = new Rank(2);
        allRanks[2] = new Rank(3);
        allRanks[3] = new Rank(4);
    }

    public void setTeams() {
        allTeams[0] = new Team("卡塔尔", "亚洲", allRanks[0]);
        allTeams[1] = new Team("巴西", "南美", allRanks[0]);
        allTeams[2] = new Team("比利时", "欧洲", allRanks[0]);
        allTeams[3] = new Team("法国", "欧洲", allRanks[0]);
        allTeams[4] = new Team("阿根廷", "南美", allRanks[0]);
        allTeams[5] = new Team("英格兰", "欧洲", allRanks[0]);
        allTeams[6] = new Team("西班牙", "欧洲", allRanks[0]);
        allTeams[7] = new Team("葡萄牙", "欧洲", allRanks[0]);
        allTeams[8] = new Team("墨西哥", "中北美", allRanks[1]);
        allTeams[9] = new Team("荷兰", "欧洲", allRanks[1]);
        allTeams[10] = new Team("丹麦", "欧洲", allRanks[1]);
        allTeams[11] = new Team("德国", "欧洲", allRanks[1]);
        allTeams[12] = new Team("乌拉圭", "南美", allRanks[1]);
        allTeams[13] = new Team("瑞士", "欧洲", allRanks[1]);
        allTeams[14] = new Team("美国", "中北美", allRanks[1]);
        allTeams[15] = new Team("克罗地亚", "欧洲", allRanks[1]);
        allTeams[16] = new Team("塞内加尔", "非洲", allRanks[2]);
        allTeams[17] = new Team("伊朗", "亚洲", allRanks[2]);
        allTeams[18] = new Team("日本", "亚洲", allRanks[2]);
        allTeams[19] = new Team("摩洛哥", "非洲", allRanks[2]);
        allTeams[20] = new Team("塞尔维亚", "欧洲", allRanks[2]);
        allTeams[21] = new Team("波兰", "欧洲", allRanks[2]);
        allTeams[22] = new Team("韩国", "亚洲", allRanks[2]);
        allTeams[23] = new Team("突尼斯", "非洲", allRanks[2]);
        allTeams[24] = new Team("喀麦隆", "非洲", allRanks[3]);
        allTeams[25] = new Team("加拿大", "中北美", allRanks[3]);
        allTeams[26] = new Team("厄瓜多尔", "南美", allRanks[3]);
        allTeams[27] = new Team("沙特", "亚洲", allRanks[3]);
        allTeams[28] = new Team("加纳", "非洲", allRanks[3]);
        allTeams[29] = new Team("威尔士/苏格兰/乌克兰", "欧洲", allRanks[3]);
        allTeams[30] = new Team("澳大利亚/阿联酋/秘鲁", "亚洲/南美", allRanks[3]);
        allTeams[31] = new Team("哥斯达黎加/新西兰", "中北美/大洋洲", allRanks[3]);

    }

    public void setGroups() {
        allGroups[0] = new Group("A");
        allGroups[1] = new Group("B");
        allGroups[2] = new Group("C");
        allGroups[3] = new Group("D");
        allGroups[4] = new Group("E");
        allGroups[5] = new Group("F");
        allGroups[6] = new Group("G");
        allGroups[7] = new Group("H");
    }

然后,调用Assigner()的assignGroup()方法进行小组抽签,但要先设置东道主卡塔尔的A1位置:

    public void draw() {
        // 设置东道主的位置
        allTeams[0].setGroup(allGroups[0]);
        allTeams[0].setAssigned(true);

        // 小组抽签
        Assigner.assignGroup(allRanks, allGroups);
    }

一次小组抽签结束后,要重置球队和小组的分配状态:

    public void unset() {
        // 重置球队和小组分配情况
        for (Team team : allTeams) {
            team.setAssigned(false);
            team.setGroup(null);
        }

        for (Group group : allGroups) {
            group.clearTeam();
        }
    }

为了使结果便于阅读,我把结果输出到了文件(项目根目录的WordCupGroupResult.txt)中:

    public void writeResults(String result) {
        // 输出结果到文件
        String filePath = "WordCupGroupResult.txt";
        BufferedOutputStream bos = null;
        try {
            File file = new File(filePath);
            if (!file.exists()) { // 不存在则创建
                file.createNewFile();
            }

            bos = new BufferedOutputStream(new FileOutputStream(file));
            // 输出字节数较少,因此就不用循环了
            bos.write(result.getBytes(StandardCharsets.UTF_8));
            bos.flush();
        } catch (Exception e) {

        } finally {
            if (bos != null) {
                try {
                    bos.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

最后,在main()方法中进行调用:

    public static void main(String[] args) {
        WordCupDraw draw = new WordCupDraw();
        // 初始化档位、球队和小组信息
        draw.setAllRanks(); // 档位信息要先初始化
        draw.setTeams();
        draw.setGroups();

        // 进行10次独立随机抽签
        for (int i = 0; i < 10; i++) {
            draw.draw();
            draw.unset();
        }

        // 获取抽签结果,并打印、写入文件
        String result = GroupPlan.getInstance().printAllGroupPlan(2);
        System.out.println(result);
        draw.writeResults(result);
    }

WordCupDraw类全部代码如下所示:

package com.szc.wordCup;

import java.io.*;
import java.nio.charset.StandardCharsets;

public class WordCupDraw {
    private final Team[] allTeams = new Team[32];
    private final Rank[] allRanks = new Rank[4];
    private final Group[] allGroups = new Group[8];

    public static void main(String[] args) {
        WordCupDraw draw = new WordCupDraw();
        // 初始化档位、球队和小组信息
        draw.setAllRanks();
        draw.setTeams();
        draw.setGroups();

        // 进行10次独立随机抽签
        for (int i = 0; i < 10; i++) {
            draw.draw();
            draw.unset();
        }

        // 获取抽签结果,并打印、写入文件
        String result = GroupPlan.getInstance().printAllGroupPlan(2);
        System.out.println(result);
        draw.writeResults(result);
    }

    public void draw() {
        // 设置东道主的位置
        allTeams[0].setGroup(allGroups[0]);
        allTeams[0].setAssigned(true);

        // 小组抽签
        Assigner.assignGroup(allRanks, allGroups);
    }

    public void unset() {
        // 重置球队和小组分配情况
        for (Team team : allTeams) {
            team.setAssigned(false);
            team.setGroup(null);
        }

        for (Group group : allGroups) {
            group.clearTeam();
        }
    }

    public void writeResults(String result) {
        // 输出结果到文件
        String filePath = "WordCupGroupResult.txt";
        BufferedOutputStream bos = null;
        try {
            File file = new File(filePath);
            if (!file.exists()) {
                file.createNewFile();
            }

            bos = new BufferedOutputStream(new FileOutputStream(file));
            bos.write(result.getBytes(StandardCharsets.UTF_8));
            bos.flush();
        } catch (Exception e) {

        } finally {
            if (bos != null) {
                try {
                    bos.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    public void setAllRanks() {
        allRanks[0] = new Rank(1);
        allRanks[1] = new Rank(2);
        allRanks[2] = new Rank(3);
        allRanks[3] = new Rank(4);
    }

    public void setTeams() {
        allTeams[0] = new Team("卡塔尔", "亚洲", allRanks[0]);
        allTeams[1] = new Team("巴西", "南美", allRanks[0]);
        allTeams[2] = new Team("比利时", "欧洲", allRanks[0]);
        allTeams[3] = new Team("法国", "欧洲", allRanks[0]);
        allTeams[4] = new Team("阿根廷", "南美", allRanks[0]);
        allTeams[5] = new Team("英格兰", "欧洲", allRanks[0]);
        allTeams[6] = new Team("西班牙", "欧洲", allRanks[0]);
        allTeams[7] = new Team("葡萄牙", "欧洲", allRanks[0]);
        allTeams[8] = new Team("墨西哥", "中北美", allRanks[1]);
        allTeams[9] = new Team("荷兰", "欧洲", allRanks[1]);
        allTeams[10] = new Team("丹麦", "欧洲", allRanks[1]);
        allTeams[11] = new Team("德国", "欧洲", allRanks[1]);
        allTeams[12] = new Team("乌拉圭", "南美", allRanks[1]);
        allTeams[13] = new Team("瑞士", "欧洲", allRanks[1]);
        allTeams[14] = new Team("美国", "中北美", allRanks[1]);
        allTeams[15] = new Team("克罗地亚", "欧洲", allRanks[1]);
        allTeams[16] = new Team("塞内加尔", "非洲", allRanks[2]);
        allTeams[17] = new Team("伊朗", "亚洲", allRanks[2]);
        allTeams[18] = new Team("日本", "亚洲", allRanks[2]);
        allTeams[19] = new Team("摩洛哥", "非洲", allRanks[2]);
        allTeams[20] = new Team("塞尔维亚", "欧洲", allRanks[2]);
        allTeams[21] = new Team("波兰", "欧洲", allRanks[2]);
        allTeams[22] = new Team("韩国", "亚洲", allRanks[2]);
        allTeams[23] = new Team("突尼斯", "非洲", allRanks[2]);
        allTeams[24] = new Team("喀麦隆", "非洲", allRanks[3]);
        allTeams[25] = new Team("加拿大", "中北美", allRanks[3]);
        allTeams[26] = new Team("厄瓜多尔", "南美", allRanks[3]);
        allTeams[27] = new Team("沙特", "亚洲", allRanks[3]);
        allTeams[28] = new Team("加纳", "非洲", allRanks[3]);
        allTeams[29] = new Team("威尔士/苏格兰/乌克兰", "欧洲", allRanks[3]);
        allTeams[30] = new Team("澳大利亚/阿联酋/秘鲁", "亚洲/南美", allRanks[3]);
        allTeams[31] = new Team("哥斯达黎加/新西兰", "中北美/大洋洲", allRanks[3]);

    }

    public void setGroups() {
        allGroups[0] = new Group("A");
        allGroups[1] = new Group("B");
        allGroups[2] = new Group("C");
        allGroups[3] = new Group("D");
        allGroups[4] = new Group("E");
        allGroups[5] = new Group("F");
        allGroups[6] = new Group("G");
        allGroups[7] = new Group("H");
    }
}

运行结果

我们直接看WordCupGroupResult.txt中的输出,因为小组抽签的可能结果非常多,我试了试3000次独立抽签、20000次独立抽签,都没有一次出现重复的,因此我就按照先后顺序,截取了前两次的抽签结果,如下所示:

{ 
	小组对阵情况: {
		GroupA:
			卡塔尔				加拿大				荷兰				波兰	
		====================================================================================
		GroupB:
			阿根廷				威尔士/苏格兰/乌克兰				塞内加尔				克罗地亚	
		====================================================================================
		GroupC:
			比利时				乌拉圭				塞尔维亚				哥斯达黎加/新西兰	
		====================================================================================
		GroupD:
			西班牙				丹麦				加纳				日本	
		====================================================================================
		GroupE:
			巴西				墨西哥				沙特				摩洛哥	
		====================================================================================
		GroupF:
			英格兰				伊朗				瑞士				厄瓜多尔	
		====================================================================================
		GroupG:
			法国				美国				韩国				喀麦隆	
		====================================================================================
		GroupH:
			葡萄牙				突尼斯				澳大利亚/阿联酋/秘鲁				德国	
		====================================================================================
		出现次数:1
		出现概率:10.00%
	},
	小组对阵情况: {
		GroupA:
			卡塔尔				厄瓜多尔				波兰				德国	
		====================================================================================
		GroupB:
			葡萄牙				塞尔维亚				澳大利亚/阿联酋/秘鲁				墨西哥	
		====================================================================================
		GroupC:
			法国				丹麦				塞内加尔				沙特	
		====================================================================================
		GroupD:
			比利时				韩国				加纳				美国	
		====================================================================================
		GroupE:
			阿根廷				荷兰				伊朗				喀麦隆	
		====================================================================================
		GroupF:
			英格兰				乌拉圭				威尔士/苏格兰/乌克兰				突尼斯	
		====================================================================================
		GroupG:
			巴西				瑞士				摩洛哥				哥斯达黎加/新西兰	
		====================================================================================
		GroupH:
			西班牙				克罗地亚				加拿大				日本	
		====================================================================================
		出现次数:1
		出现概率:10.00%
	},
	.......
}

看来电脑喜欢给葡萄牙(C罗)搞事儿啊,不是和克星德国同组,就是和预算赛时的冤家塞尔维亚同组,梅西的阿根廷的两次签运也一般,第一次是上届世界杯亚军克罗地亚(该届小组赛,克罗地亚3:0完爆阿根廷),第二次是跟荷兰同组(14年巴西世界杯半决赛,阿根廷和荷兰鏖战120分钟互交白卷,最终通过点球大战淘汰荷兰)。
当然,这是模拟,不能当真。实际情况中,西班牙、德国同分一组;美国伊朗和英格兰小组碰面;东道主卡塔尔遇到橙衣军团荷兰与新晋非洲杯冠军塞内加尔;阿根廷遭遇草帽军团墨西哥与波兰,梅西莱万正面交锋;法国丹麦再聚首、红魔比利时迎战格子军团克罗地亚和枫叶之国加拿大,以及来自北非的摩洛哥;巴西、塞尔维亚和瑞士军刀连续两届同分一组;葡萄牙遇到上届淘汰赛对手乌拉圭,能否报1:2被淘汰的一箭之仇呢?同组的韩国队也是不容小觑,毕竟他们上届小组赛就掀翻了当时的卫冕冠军德国队,该组另一支球队加纳,也是在14年世界杯小组赛上和德国队战成2:2平的球队,同样是一支劲旅。

  • 0
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值