본문 바로가기
기타

롤 내전 팀 짜기 프로그램 v1

by Hongwoo 2025. 1. 19.
반응형

LOL Team Creation.zip
1.13MB

목차

    인트로

    이 글은 롤 내전을 할 때 팀을 밸런스 있게 짜는 프로그램을 설명하기 위해 쓴다. 코드는 모두 Java로 작성되었고 이해가 안 되는 부분이나 수정할 부분, 또는 개선할 점이 있으면 댓글로 남겨주길 바란다.

     

    우선 프로그램의 구성은 다음과 같다 (4시간 만에 만들었기 때문에 당연히 프로그램 구성은 개선할 점이 많을 것이므로 더 개선할 수 있는 부분은 꼭 댓글로 남겨주길 바란다):

     

    프로그램 구성:

    • Rank enum
    • Position enum
    • Player class
    • Team class
    • Game class
    • Members.csv
    • Main class

    Rank enum

    우선적으로 나중에 설명할 때 필요한 Rank enum부터 설명을 드리겠다.

     

    public enum RANK {
        IRON_4(1), IRON_3(2), IRON_2(3), IRON_1(4),
        BRONZE_4(5), BRONZE_3(6), BRONZE_2(7), BRONZE_1(8),
        SILVER_4(9), SILVER_3(10), SILVER_2(11), SILVER_1(12),
        GOLD_4(13), GOLD_3(14), GOLD_2(15), GOLD_1(16),
        PLATINUM_4(17), PLATINUM_3(18), PLATINUM_2(19), PLATINUM_1(20),
        EMERALD_4(21), EMERALD_3(22), EMERALD_2(23), EMERALD_1(24),
        DIAMOND_4(25), DIAMOND_3(26), DIAMOND_2(27), DIAMOND_1(28),
        MASTER(30), GRANDMASTER(35), CHALLENGER(40);
    
        private final double points;
    
        RANK(double points) {
            this.points = points;
        }
    
        public double getPoints() {
            return points;
        }
    }

     

    이 Rank enum은 가능한 모든 티어를 저장한다. 그리고 물론 문자열 형태로 저장할 수도 있었겠지만 enum 형태로 저장한 이유는 코드 가독성도 있겠지만 서로 관련 있는 상수들끼리 모아 상수들을 대표할 수 있는 이름으로 타입을 만들고 싶었기 때문에 String형 대신 enum을 사용했다. 

     

    이 enum에 있는 points 변수는 나중에 티어를 점수로 변환하기 위해서이다. 선수의 실력을 티어 및 포지션마다 점수로 변환하여 각 팀에 있는 선수들의 총점수를 비교해 밸런스 있게 팀을 짜기 위해서이다. 예를 들어, 이 enum에서도 볼 수 있듯이 아이언 4는 1점, 아이언 3은 2점 등등으로 변환된다. 

     

    여기 있는 점수를 순차적으로 변환하는 것 말고 티어 별로 더 가중치를 둬서 변환하는 방법도 고려는 해봤지만 아직 실전 테스트나 다양한 의견을 듣지 못해 이렇게 뒀다. 만약에 티어를 점수로 변환할 때 조금 더 좋은 견해가 있다면 댓글로 남겨주길 바란다.

     

    Position Enum

    Position enum은 간단하게 롤에 있는 5개 포지션을 저장한 Enum이다. 한 팀에 포지션이 각각 다른 5명의 선수가 필요하기 때문에 이 Enum을 만들었다. 

     

    public enum POSITION {
        TOP,
        JNG,
        MID,
        BOT,
        SUP;
    }

     

     

    Player class

    Player class는 선수에 대한 정보, 즉 이름과 랭크(티어), 플레이할 수 있는 포지션들, 그리고 만약에 같은 팀에 배정받고 싶은 선수가 있다면 그 선수로 설정할 수 있는 파트너 변수가 있다. 그리고 각 선수마다 플레이할 수 있는 포지션이 다르고 여러 개 포지션을 플레이할 수도 있기 때문에 리스트로 만들었다.

     

    public class Player {
        private String name;
        private RANK rank;
        private List<POSITION> possible_positions;
        private Player partner; //Can be null

     

    선수의 인스턴스를 선언할 때 파트너가 있거나 없거나 할 수 있기 때문에 매개 변수가 다른 생성자 2개가 있다. 이 두 개의 차이점은 파트너가 있냐 없냐의 차이다.

     

    public Player(String name, RANK rank, List<POSITION> possible_positions, Player partner) {
        this.name = name;
        this.rank = rank;
        this.possible_positions = possible_positions;
        this.partner = partner;
    }
    
    public Player(String name, RANK rank, List<POSITION> possible_positions) {
        this.name = name;
        this.rank = rank;
        this.possible_positions = possible_positions;
        this.partner = null;
    }

     

    그리고 이 클래스에 있는 함수들은 getter, setter, equals 등의 함수만 있기 때문에 추가적인 설명은 스킵하겠다.

     

     

    Team Class

    Team class는 팀에 대한 정보를 저장하는 클래스이다. 롤 팀에는 Top, Jungle, Mid, Bottom, Support 이렇게 5명의 선수들이 있다. 추가로 이 클래스에는 이 선수들과 이 선수들의 점수(티어를 점수로 변환한 것)의 합과 그리고 선호하는 포지션으로 몇 명이 배정된 지를 나타내는 변수인 numberOfPreferredPosition 변수가 있다.

     

    public class Team {
        private Player top;
        private Player jng;
        private Player mid;
        private Player bot;
        private Player sup;
        double totalPoints;
        int numberOfPreferredPosition;
    
        public Team(Player top, Player jng, Player mid, Player bot, Player sup) {
            this.top = top;
            this.jng = jng;
            this.mid = mid;
            this.bot = bot;
            this.sup = sup;
            totalPoints = getTotalPoints();
            numberOfPreferredPosition = calculateNumberOfPreferredPositions();
        }

     

    이 클래스에는 getter, setter, equals와 더불어서 validate 함수가 있다. 이 validate 함수는 팀이 유효한지를 확인하는 함수이다. 

     

    이 validate 함수는 다음과 같은 4가지를 확인한다.

    1. 한 선수가 파트너가 있으면 그 파트너가 같은 팀에 있는지,

    2. 선수가 플레이할 수 있는 포지션에 배정되었는지,

    3. A 선수가 파트너 B가 있다면 B의 파트너가 A인지,

    4. 그리고 최종적으로 팀에 중복되게 배정된 선수가 있는지 

     

    정확히 어떻게 구현했는지는 밑에 있는 코드를 보면 되겠다.

    public boolean validate() {
            return checkPartnersSameTeam() && checkPositions()
                    && checkPartnersSymmetry() && checkForDuplicatePlayers();
        }
    
        public boolean checkForDuplicatePlayers() {
            Set<Player> players = new HashSet<>();
            players.add(top);
            players.add(jng);
            players.add(mid);
            players.add(bot);
            players.add(sup);
            return players.size() == 5;
        }
    
        public boolean checkPositions() {
            if (!top.getPossible_positions().contains(POSITION.TOP)) return false;
            if (!jng.getPossible_positions().contains(POSITION.JNG)) return false;
            if (!mid.getPossible_positions().contains(POSITION.MID)) return false;
            if (!bot.getPossible_positions().contains(POSITION.BOT)) return false;
            return sup.getPossible_positions().contains(POSITION.SUP);
        }
    
        public boolean checkPartnersSymmetry() {
            if (top.getPartner() != null) {
                if (top.getPartner().getPartner() == null) return false;
                if (!top.getPartner().getPartner().equals(top)) return false;
            }
            if (jng.getPartner() != null) {
                if (jng.getPartner().getPartner() == null) return false;
                if (!jng.getPartner().getPartner().equals(jng)) return false;
            }
            if (mid.getPartner() != null) {
                if (mid.getPartner().getPartner() == null) return false;
                if (!mid.getPartner().getPartner().equals(mid)) return false;
            }
            if (bot.getPartner() != null) {
                if (bot.getPartner().getPartner() == null) return false;
                if (!bot.getPartner().getPartner().equals(bot)) return false;
            }
            if (sup.getPartner() != null) {
                if (sup.getPartner().getPartner() == null) return false;
                if (!sup.getPartner().getPartner().equals(sup)) return false;
            }
            return true;
        }
    
        private boolean checkPartnersSameTeam() {
            // Check that each player's partner is in the same team
            if (top.getPartner() != null && !top.getPartner().equals(jng) && !top.getPartner().equals(mid) &&
                    !top.getPartner().equals(bot) && !top.getPartner().equals(sup)) {
                return false;
            }
            if (jng.getPartner() != null && !jng.getPartner().equals(top) && !jng.getPartner().equals(mid) &&
                    !jng.getPartner().equals(bot) && !jng.getPartner().equals(sup)) {
                return false;
            }
            if (mid.getPartner() != null && !mid.getPartner().equals(top) && !mid.getPartner().equals(jng) &&
                    !mid.getPartner().equals(bot) && !mid.getPartner().equals(sup)) {
                return false;
            }
            if (bot.getPartner() != null && !bot.getPartner().equals(top) && !bot.getPartner().equals(jng) &&
                    !bot.getPartner().equals(mid) && !bot.getPartner().equals(sup)) {
                return false;
            }
            if (sup.getPartner() != null && !sup.getPartner().equals(top) && !sup.getPartner().equals(jng) &&
                    !sup.getPartner().equals(mid) && !sup.getPartner().equals(bot)) {
                return false;
            }
            return true;
        }

     

    추가로 이 클래스에는 그 팀의 총합 점수를 계산하는 함수 getTotalPoints가 있다. 우리들의 경험상으로는 내전에서 보통 정글이 영향력이 가장 크고 (정글 차이가 심할 때 잘 못하는 정글러 팀이 이기는 경우가 거의 없었다), 미드, 서폿, 원딜, 탑 순이었이므로 이 라인의 순서대로 가중치를 줘서 총합 점수를 계산했다. 이 부분은 토론의 여지가 있으므로 다른 생각이 있다면 댓글 남겨주길 바란다.

     

        public double getTotalPoints() {
            double topPoints = top.getPossible_positions().get(0).equals(POSITION.TOP) ? 1.0 : 0.8 * top.getRank().getPoints();
            double jngPoints = jng.getPossible_positions().get(0).equals(POSITION.JNG) ? 1.0 : 0.8 * jng.getRank().getPoints() * 1.4;
            double midPoints = mid.getPossible_positions().get(0).equals(POSITION.MID) ? 1.0 : 0.8 * mid.getRank().getPoints() * 1.3;
            double botPoints = bot.getPossible_positions().get(0).equals(POSITION.BOT) ? 1.0 : 0.8 * bot.getRank().getPoints() * 1.1;
            double supPoints = sup.getPossible_positions().get(0).equals(POSITION.SUP) ? 1.0 : 0.8 * sup.getRank().getPoints() * 1.2;
            return topPoints + jngPoints + midPoints + botPoints + supPoints;
        }

    Game Class

    Game class는 게임에 대한 변수들을 저장한다. 즉 2개의 팀 (롤에서는 블루 팀, 레드 팀으로 나뉜다), 그리고 팀들의 실력 차이 (티어와 포지션을 참고하며 계산된 점수), 그리고 그 게임에 몇 명의 선수가 선호하는 포지션으로 배정되었는지.

     

    추가로 이 클래스가 Comparable <Game>을 implement 하는 이유는 Main class에서 팀을 만들 때 만들 수 있는 모든 게임을 만드는데, 이 게임들을 밸런스가 맞는 팀들을 먼저 보여줄 수 있게, 즉 point_difference 변수가 가장 작은 것을 먼저 표시하게 정렬을 하기 때문이다. 게임 변수들을 비교하기 위해서 compareTo 함수도 알맞게 만들었다.

    public class Game implements Comparable<Game>{
        private Team team1;
        private Team team2;
        private final int point_difference;
        private final int numberOfPreferredPositions;
    
        public Game(Team team1, Team team2) {
            this.team1 = team1;
            this.team2 = team2;
            point_difference = calculateDifference(team1, team2);
            numberOfPreferredPositions = team1.getNumberOfPreferredPosition() + team2.getNumberOfPreferredPosition();
        }
        
        @Override
        public int compareTo(Game otherGame) {
            return Integer.compare(this.point_difference, otherGame.point_difference);
        }

     

    이 Game class에는 생성자에도 있듯이 point_difference 구하는 함수가 있다. 각 팀에 있는 총점수의 차이를 구하는 함수이다.

        private int calculateDifference(Team team1, Team team2) {
            return (int)Math.abs(team1.totalPoints - team2.totalPoints);
        }

     

     

    Team class에도 있었던 것처럼 이 Game class에도 게임이 유효한지 확인하는 validate 함수가 있다. 이 validate 함수는 다음과 같은 4가지를 확인한다.

    1. Team 1이 유효한지,

    2. Team 2가 유효한지,

    3. 양 팀에 중복된 선수가 없는지, 즉 그 게임에 각각 다른 10명의 선수가 있는지,

    4. 그리고 각 라인마다 실력 차이가 너무 나면 재밌는 게임 진행이 어려우므로 각 라인마다 선수들의 티어 차이가 2단계 이하인지 (예를 들어, 실버와 다이아는 맞라인을 설 수 없다. 최대는 실버 4와 플래 4 이렇게 최대 2단계까지만).

     

    public boolean validate() {
        return team1.validate() && team2.validate() && noSharedPlayers() && laneSimilarLevel();
    }
    
    public boolean laneSimilarLevel() {
        return Math.abs(team1.getTop().getRank().getPoints() - team2.getTop().getRank().getPoints()) <= 8
                && Math.abs(team1.getJng().getRank().getPoints() - team2.getJng().getRank().getPoints()) <= 8
                && Math.abs(team1.getMid().getRank().getPoints() - team2.getMid().getRank().getPoints()) <= 8
                && Math.abs(team1.getBot().getRank().getPoints() - team2.getBot().getRank().getPoints()) <= 8
                && Math.abs(team1.getSup().getRank().getPoints() - team2.getSup().getRank().getPoints()) <= 8;
    }
    
    private boolean noSharedPlayers() {
        Set<Player> players = new HashSet<>();
        players.add(team1.getTop());
        players.add(team1.getJng());
        players.add(team1.getMid());
        players.add(team1.getBot());
        players.add(team1.getSup());
        players.add(team2.getTop());
        players.add(team2.getJng());
        players.add(team2.getMid());
        players.add(team2.getBot());
        players.add(team2.getSup());
        return players.size() == 10;
    }

     

     

    그리고 게임을 출력할 때 보기 편한 포맷으로 출력하기 위해 toString() 함수도 다시 만들었다.

     

    @Override
    public String toString() {
        return "TOP: " + team1.getTop() + " vs " + team2.getTop() + "\n"
                + "JNG: " + team1.getJng() + " vs " + team2.getJng() + "\n"
                + "MID: " + team1.getMid() + " vs " + team2.getMid() + "\n"
                + "BOT: " + team1.getBot() + " vs " + team2.getBot() + "\n"
                + "SUP: " + team1.getSup() + " vs " + team2.getSup();
    }

     

     

     

    Members.csv

    Members.csv 파일에는 게임을 플레이할 10명의 선수들의 정보를 쉼표로 구분한 파일로 저장한다. 

    이 Members.csv 파일에는 다음과 같은 형식으로 선수에 대한 정보를 저장한다.

    이름, 티어, 가능한 포지션들, 파트너 (생략 가능)

     

    예를 들면, Members.csv는 다음과 같이 저장될 수 있다. 

    Player1,EMERALD_4,TOP,BOT
    Player2,DIAMOND_4,JNG,BOT,TOP
    Player3,PLATINUM_3,MID,JNG,SUP,TOP,PPlayer4
    Player4,BRONZE_2,BOT,PPlayer3
    Player5,BRONZE_2,SUP
    Player6,PLATINUM_2,TOP,JNG,SUP,PPlayer9
    Player7,GOLD_3,JNG,SUP,MID,TOP,PPlayer10
    Player8,SILVER_2,MID,SUP
    Player9,GOLD_3,BOT,MID,PPlayer6
    Player10,BRONZE_4,SUP,MID,PPlayer7

     

    그리고 파트너는 구분을 하기 위해서 선수가 파트너가 있다면 P로 시작하고 그 선수 이름을 쓰면 된다.

     

    예를 들어서 민수의 파트너가 철수이면, 다음과 같이 쓸 수 있겠다.

    민수,EMERALD_4,TOP,BOT,P민수
    철수,DIAMOND_4,JNG,BOT,TOP,P철수

     

     

    이 Members.csv 파일은 Main class에서 이 파일을 읽고 이 csv 파일에 있는 정보들을 선수 타입의 인스턴스를 만들고 유효한 게임들을 brute-force 방식으로 만든다.

     

     

    Main Class

    이 Main class의 역할은 Members.csv 파일로부터 선수들에 대한 정보를 읽고 저장하고, 이 10명의 선수들로 만들 수 있는 모든 게임을 만들고 밸런스 있는 팀들 먼저 보여주는 역할을 한다.

     

    이 Main class에는 players와 partners의 변수들이 있다. 각각 HashMap <String, Player>와 HashMap <String, String> 타입으로 만들었다.

    public class Main {
        private static HashMap<String, Player> players;
        private static HashMap<String, String> partners;

     

    Players의 타입이 HashMap <String, Player>인 이유는 키가 '선수 이름', 그리고 밸류가 Player로 하기 위함이다. 마찬가지로, 파트너는 <선수 이름, 선수 이름>, 즉 링크가 되기 위해서 해시 맵으로 만들었다.

     

    그다음에 메인 함수를 보겠다.

     

    public static void main(String[] args) {
            players = new HashMap<>();
            partners = new HashMap<>();
    
            // Read Players from the CSV File
            readPlayersFromCsv();
            linkPartners();
            List<Team> validTeams = generateValidTeams(new ArrayList<>(players.values()));
            List<Game> validGames = generateValidGames(validTeams);
            Collections.sort(validGames);
            for (Game game : validGames) {
                System.out.println(game);
                System.out.println("티어차이: " + game.getPoint_difference() + ", 메인 포지션: " + game.getNumberOfPreferredPositions());
            }
        }

     

    우선 Players와 Partners 해시맵을 초기화해 준다 (initialize variables). 

     

    그다음은 선수들에 대한 정보를 읽고 저장한다.

     

    private static void readPlayersFromCsv() {
            try {
                File file = new File("src/Members.csv");
                BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream(file)));
                String line;
                while ((line = br.readLine()) != null) {
                    String[] tokens = line.split(",");
                    String name = tokens[0];
                    RANK rank = RANK.valueOf(tokens[1]);
                    List<POSITION> positions = new ArrayList<>();
                    for (int i = 2; i < tokens.length; i++) {
                        if (tokens[i].charAt(0) == 'P') {
                            partners.put(name, tokens[i].substring(1));
                        } else {
                            positions.add(POSITION.valueOf(tokens[i]));
                        }
                    }
                    Player player = new Player(name, rank, positions);
                    players.put(name, player);
                }
            } catch (Exception e) {
                System.err.println(e.getMessage());
            }
        }

     

    이 메인 함수에서는 우선 Csv 파일로부터 선수들에 대한 정보를 읽고 저장을 한다. Csv 파일에 선수 이름, 티어, 가능한 포지션들, 파트너 순서로 저장되어 있기 때문에 BufferedReader와 Token을 이용해서 읽고 선수 변수를 만들어주고 Players 해시맵에 저장해 준다. 그리고 파트너가 있으면 파트너의 이름을 Partners 해시맵에 저장해 주고 나중에 Setter를 이용해서 파트너를 설정해 준다.

     

    이 작업은 LinkPartners() 함수에서 실행된다.

     

    private static void linkPartners() {
            // Link partners
            for (String name : partners.keySet()) {
                String partnerName = partners.get(name);
                if (partnerName != null && players.containsKey(partnerName)) {
                    players.get(name).setPartner(players.get(partnerName));
                }
            }
        }

     

    그다음은 Brute force 방식을 이용해 Players에 저장되어 있는 선수들로 유효한 팀을 만든다. 여기서 유효한 팀이란 이 4가지를 충족시키는 팀이다.

    1. 한 선수가 파트너가 있으면 그 파트너가 같은 팀에 있는지,

    2. 선수가 플레이할 수 있는 포지션에 배정되었는지,

    3. A 선수가 파트너 B가 있다면 B의 파트너가 A인지,

    4. 그리고 최종적으로 팀에 중복되게 배정된 선수가 있는지 

     

    private static List<Team> generateValidTeams(List<Player> playerList) {
            List<Team> validTeams = new ArrayList<>();
    
            // Create all possible teams with 5 players
            for (int i = 0; i < playerList.size(); i++) {
                for (int j = 0; j < playerList.size(); j++) {
                    if (i == j) continue;
                    for (int k = 0 ; k < playerList.size(); k++) {
                        if (k == i || k == j) continue;
                        for (int l = 0; l < playerList.size(); l++) {
                            if (l == i || l == j || l == k) continue;
                            for (int m = 0; m < playerList.size(); m++) {
                                if (m == i || m == j || m == k || m == l) continue;
    
                                // Create a team using 5 players
                                Player top = playerList.get(i);
                                Player jng = playerList.get(j);
                                Player mid = playerList.get(k);
                                Player bot = playerList.get(l);
                                Player sup = playerList.get(m);
    
                                Team team = new Team(top, jng, mid, bot, sup);
                                if (team.validate()) {
                                    validTeams.add(team);
                                }
                            }
                        }
                    }
                }
            }
    
            return validTeams;
        }

     

    이 코드가 5중 for loop이므로 시간 복잡도가 최소 O(n^5)인 것도 인지하고 있다. 다만, 선수가 10명밖에 없으므로 퍼포먼스적으로 괜찮다는 판단 하에 5중 for loop를 이용하게 됐다. 만약에 좋은 개선점을 알고 있다면 댓글로 알려주길 바란다.

     

    가능한 모든 유효한 팀을 만든 다음에는 이 유효한 팀들로 유효한 게임을 만든다. 유효한 게임의 기준은 다음과 같다. 

    1. Team 1이 유효한지,

    2. Team 2가 유효한지,

    3. 양 팀에 중복된 선수가 없는지, 즉 그 게임에 각각 다른 10명의 선수가 있는지,

    4. 그리고 각 라인마다 실력 차이가 너무 나면 재밌는 게임 진행이 어려우므로 각 라인마다 선수들의 티어 차이가 2단계 이하인지 (예를 들어, 실버와 다이아는 맞라인을 설 수 없다. 최대는 실버 4와 플래 4 이렇게 최대 2단계까지만).

     

    private static List<Game> generateValidGames(List<Team> validTeams) {
        List<Game> validGames = new ArrayList<>();
    
        // Pair every valid team with another valid team
        for (int i = 0; i < validTeams.size(); i++) {
            for (int j = i + 1; j < validTeams.size(); j++) {
                Team team1 = validTeams.get(i);
                Team team2 = validTeams.get(j);
    
                // Ensure no duplicate players between the two teams
                Game game = new Game(team1, team2);
                if (game.validate()) {
                    validGames.add(game);
                }
            }
        }
    
        return validGames;
    }

     

    이 과정도 Brute force 방식으로 진행한다.

     

    유효한 게임들을 다 만들고 나면 팀의 실력 차이가 가장 적은 팀들을 우선적으로 출력해서 보여준다. 그리고 그 게임에 몇 명의 선수들이 선호하는 포지션으로 가는지도 함께 출력하므로 그걸 보고 팀을 구성할 수가 있겠다.

     

    팀 구성된 것을 출력하면 다음과 같이 나온다.

     

     

     

    결론

    우선 이 프로그램을 3시간 정도 들여 만들었기 때문에 refactoring 혹은 개선할 점이 많은 건 인지하고 있다. 물론 여기에 더불어서 확장할 수 있는 방법들도 많다. 현재 생각하고 있는 확장법은 다음과 같다.

     

    1. 실행 가능한 파일로 만들어서 이용하기 쉽게 만들기 (e.g. exe 파일로 만들기)

    2. 이 프로그램을 더 쉽게 사용하기 위해 UI를 만들기 (롤과 비슷한 UI로 만들어서 팀이 어떻게 구성되는지 보여주기)

    3. 더 확장을 해나간다면 DB를 이용해서 데이터의 영속성 챙기기 (data persistence). 즉, 선수들이 플레이한 게임 정보들도 저장해서 선수들의 게임 통계를 보여준다든지 할 수 있을 거 같다. 예를 들어서, 한 특정 선수의 승률이라던지, 아니면 더 나아가서는 게임 내용을 분석하는 프로그램을 만들어서 라인전 능력, 평균 KDA 등등..

    4. 3번에 기재된 내용을 달성하려면 Riot API를 이용해서 각 게임에 대한 정보를 얻고 그걸 토대로 계산하기.

     

     

    반응형

    댓글