Plugin Résolu [JAVA] Système de Teams

Kenda

Architecte en herbe
16 Juillet 2016
292
1
2
125
32
www.youtube.com
Bonjour,

Actuellement en création de mini-jeux pour mon serveur, je cherche à faire un système d'équilibrage de teams.
J'ai pour le moment 3 classes pour géré mes teams. Une class Enum qui gère le nom de l'équipe, la location, le meta de la laine, ainsi que la couleur.
Java:
package fr.kenda.rushranked.teams;

import org.bukkit.Bukkit;
import org.bukkit.DyeColor;
import org.bukkit.Location;

public enum TeamList {

    BLEU("§bEquipe Bleu", new Location(Bukkit.getWorld("world"), 0, 9.5, 38), DyeColor.LIGHT_BLUE, 11),
    ROUGE("§cEquipe Rouge", new Location(Bukkit.getWorld("world"), 0, 9.5, -37), DyeColor.RED, 14);

    private final String name;
    private final Location location;
    private final DyeColor color;
    private final int meta;

    TeamList(String name, Location location, DyeColor color, int meta) {
        this.name = name;
        this.location = location;
        this.color = color;
        this.meta = meta;
    }
    public String getName(){
        return name;
    }
    public Location getLocation(){
        return location;
    }

    public DyeColor getColor() {
        return color;
    }

    public int getMeta() {
        return meta;
    }
}

Ensuite j'ai une classe Teams, qui stock les membres de l'équipe, l'ajout du joueur, les infos de la TeamList avec les return du nom, une méthode pour TP les joueurs à la location de l'équipe et d'autres methodes pas encore utiles.

Java:
package fr.kenda.rushranked.teams;

import org.bukkit.DyeColor;
import org.bukkit.entity.Player;

import java.util.ArrayList;
public class Teams {

    private final TeamList team;
    private final ArrayList<Player> members = new ArrayList<>();

    public Teams(TeamList team) {
        this.team = team;
    }

    public void addPlayerTeam(Player player) {
        members.add(player);
    }

    public void removePlayerTeam(Player player) {
        members.remove(player);
    }

    public String getName() {
        return team.getName();
    }

    public ArrayList<Player> getMembers() {
        return members;
    }

    public void teleportTeam() {
        for (Player player : this.members) {
            player.teleport(team.getLocation());
        }
    }

    public DyeColor getColor(){
        return team.getColor();
    }

}

Puis ma class Main, ou l'enregistrement se fait (avec une boucle qui parcours la class TeamList pour crée la class Team avec la TeamList), ainsi qu'une méthode pour vérifier si le joueur est dans une team.
Java:
@Override
    public void onEnable() {
        instance = this;
        getServer().getConsoleSender().sendMessage(prefix + "§aLancement du plugin avec succès.");
        for (TeamList teamsList : TeamList.values()) {
            teams.add(new Teams(teamsList));
        }

        setStatus(GameStatus.WAITING);
        
        [...]
}

//Vérifie si le joueur est dans une team
public static boolean isInTeam(Player player) {
    boolean isInTeam = false;
    for (Teams teams : teams) {
        isInTeam = teams.getMembers().contains(player);
    }
    return isInTeam;
}

Ensuite viens mon problème. Si la partie se lance, et que toutes les teams sont vides, alors on dois égalisé les teams (4 joueurs pour 4 teams = 1 joueur par équipe, on est d'accord ?)
J'ai fais cette méthode dans la class Main (même si je pourrais la stocker autre part) :

Code:
public static void addRandomTeam(Player player) {
        for (Teams teamChecker : teams) {
            System.out.println("Boucle 1: " + teamChecker.getName());
            for (Teams teamsDetec : teams) {
                System.out.println("Boucle 2 : " + teamsDetec.getName());
                if (teamChecker != teamsDetec) {
                    System.out.println("La team n'est pas la même");
                    System.out.println("teamchecker " + teamChecker.getMembers().size());
                    System.out.println("teams " + teamsDetec.getMembers().size());
                    if (teamChecker.getMembers().size() <= teamsDetec.getMembers().size()) {
                        System.out.println("On l'ajoute dans la team : " + teamChecker);
                        teamChecker.addPlayerTeam(player);
                        System.out.println("Teamchecker list : " + teamChecker.getMembers());
                        System.out.println("teamDetect list : " + teamsDetec.getMembers());
                    }
                }
            }
        }
    }
J'ai donc cette sorti de debug :
Code:
[17:44:31] [Server thread/INFO]: Le joueur Dinasto n'est pas dans une team.
[17:44:31] [Server thread/INFO]: Boucle 1: §bEquipe Bleu
[17:44:31] [Server thread/INFO]: Boucle 2 : §bEquipe Bleu
[17:44:31] [Server thread/INFO]: Boucle 2 : §cEquipe Rouge
[17:44:31] [Server thread/INFO]: La team n'est pas la même
[17:44:31] [Server thread/INFO]: teamchecker 0
[17:44:31] [Server thread/INFO]: teams 0
[17:44:31] [Server thread/INFO]: On l'ajoute dans la team : fr.kenda.rushranked.teams.Teams@5087dd9d
[17:44:31] [Server thread/INFO]: [CraftPlayer{name=Dinasto}]
[17:44:31] [Server thread/INFO]: []
[17:44:31] [Server thread/INFO]: Boucle 1: §cEquipe Rouge
[17:44:31] [Server thread/INFO]: Boucle 2 : §bEquipe Bleu
[17:44:31] [Server thread/INFO]: La team n'est pas la même
[17:44:31] [Server thread/INFO]: teamchecker 0
[17:44:31] [Server thread/INFO]: teams 1
[17:44:31] [Server thread/INFO]: On l'ajoute dans la team : fr.kenda.rushranked.teams.Teams@4fdd1776
[17:44:31] [Server thread/INFO]: [CraftPlayer{name=Dinasto}]
[17:44:31] [Server thread/INFO]: [CraftPlayer{name=Dinasto}]
[17:44:31] [Server thread/INFO]: Boucle 2 : §cEquipe Rouge
[17:44:31] [Server thread/INFO]: Le joueur Kenda_ n'est pas dans une team.
[17:44:31] [Server thread/INFO]: Boucle 1: §bEquipe Bleu
[17:44:31] [Server thread/INFO]: Boucle 2 : §bEquipe Bleu
[17:44:31] [Server thread/INFO]: Boucle 2 : §cEquipe Rouge
[17:44:31] [Server thread/INFO]: La team n'est pas la même
[17:44:31] [Server thread/INFO]: teamchecker 1
[17:44:31] [Server thread/INFO]: teams 1
[17:44:31] [Server thread/INFO]: On l'ajoute dans la team : fr.kenda.rushranked.teams.Teams@5087dd9d
[17:44:31] [Server thread/INFO]: [CraftPlayer{name=Dinasto}, CraftPlayer{name=Kenda_}]
[17:44:31] [Server thread/INFO]: [CraftPlayer{name=Dinasto}]
[17:44:31] [Server thread/INFO]: Boucle 1: §cEquipe Rouge
[17:44:31] [Server thread/INFO]: Boucle 2 : §bEquipe Bleu
[17:44:31] [Server thread/INFO]: La team n'est pas la même
[17:44:31] [Server thread/INFO]: teamchecker 1
[17:44:31] [Server thread/INFO]: teams 2
[17:44:31] [Server thread/INFO]: On l'ajoute dans la team : fr.kenda.rushranked.teams.Teams@4fdd1776
[17:44:31] [Server thread/INFO]: [CraftPlayer{name=Dinasto}, CraftPlayer{name=Kenda_}]
[17:44:31] [Server thread/INFO]: [CraftPlayer{name=Dinasto}, CraftPlayer{name=Kenda_}]
[17:44:31] [Server thread/INFO]: Boucle 2 : §cEquipe Rouge

Et donc tout les joueurs seront dans la même team :confused:

J'aimerai avoir de l'aide, car la je bloque depuis 3 jours, et c'est la première fois que je fais un système de team.
Il existe surement des moyens plus simple, et j'en suis sûr, mais je dois mal faire mes recherches.
Je précise que je suis en paper 1.9 (j'ai toujours aimer cette version).

Merci bien de l'aide précieuse car sans sa, je peux pas avancé :(
 

ShE3py

Enbogueuse
Support
26 Septembre 2015
4 086
157
455
247
21
Mìlhüsa
Bonjour,

Les énumérations sont plutôt pour les listes finies constantes, or le nombre d'équipe dans un jeu risque fortement de varier en fonction du nombre de joueurs, donc ce serait pour moi mieux de passer directement par une classe.

De plus c'est casse-gueule de mettre le nom des énumérations au pluriel :
Java:
public static List<Player> getPlayersIn(TeamList team) {
    // ...
}
On s'attend à ce que ta fonction ait une liste d'équipes en paramètre, pas une seule.

De la même manière je ne comprends pas pourquoi tu fais un mix d'énumération + classe
Java:
public class Teams {
    private final TeamList team;
    private final ArrayList<Player> members = new ArrayList<>();
}

Quand tu pourrais simplement le faire directement dans l'énumération ?
Java:
public enum TeamList {
    PINK("Rose", new Location(Bukkit.getWorld("world_the_end"), 1, 2, 3), DyeColor.PINK);
    
    // les varibles sont immuables donc pas besoin de faire un getFoo()
    public final String name;
    public final DyeColor color;
    
    // muable
    private final Location location;
    private final List<Player> players;
    
    private TeamList(String name, Location location, DyeColor color) {
        this.name = "§bÉquipe " + name; // évite de recopier les suffixes/préfixes + les couleurs,
                                        //   ça facilitera les changements dans le futur (style changer une couleur)
        this.location = location;
        this.color = color;
        this.players = new ArrayList<>();
    }
    
    public Location getLocation() {
        return this.location.clone();
    }
    
//  @UnmodifiableView
    public List<Player> getPlayers() {
        return Collections.unmodifiableList(this.players);
    }
    
    @Deprecated
    public byte getMeta() {
        // ✨ magic number ✨
        return this.color.getDyeDate();
    }
}

De plus si t'arrives à me dire quel endroit est resprésenté par ta location à partir d'uniquement le code source d'énumération c'est qu't'es fort.
Bref :
Java:
public class Team extends ArrayList<Player> {
   public static final List<Team> TEAMS;
   public static final Team BLUE;
   public static final Team RED;

   public final String displayName;
   public final DyeColor color;
   
   private final Location startPoint;
   
   static {
      World world = Bukkit.getWorld("world");
      if(world == null) {
         throw new IllegalStateException("world `world` does not exist");
      }
      
      BLUE = new Team("Bleu", DyeColor.BLUE, new Location(world, 0, 9.5, 38));
      RED = new Team("Red", DyeColor.RED, new Location(world, 0, 9.5, -37));
      
      TEAMS = ImmutableList.of(BLUE, RED);
   }
   
   private Team(String displayName, DyeColor color, Location startPoint) {
      super();
      
      if(displayName == null || displayName.isBlank()) {
         throw new IllegalArgumentException("displayName must not be blank");
      }
      
      if(color == null) {
         throw new NullPointerException("color must not be null");
      }
      
      if(startPoint == null || startPoint.getWorld() == null) {
         throw new IllegalArgumentException("startPoint's world must not be null");
      }
      
      this.displayName = "§bÉquipe " + displayName;
      this.color = color;
      this.startPoint = startPoint;
   }
   
   public static Optional<Team> getTeamOf(Player p) {
      for(Team t : TEAMS) {
         if(t.contains(p)) {
            return Optional.of(t);
         }
      }
      
      return Optional.empty();
   }
   
   public static boolean isInTeam(Player p) {
      return getTeamOf(p).isPresent();
   }
   
   public Location getStartPoint() {
      return startPoint.clone();
   }
   
   public boolean teleportAll() {
      int i = 0;
      List<Location> beforeTeleportation = new ArrayList<>(this.size());
      
      while(i++ < this.size()) {
         Player p = this.get(i);
         beforeTeleportation.add(p.getLocation());
         
         if(!p.teleport(this.startPoint)) {
            logger.severe("Teleportation was denied for player " + p.getName());
            
            // undo all previous teleportation
            while(--i > 0) {
               p = this.get(i);
               
               if(!p.teleport(beforeTeleportation.get(i))) {
                  Location lobby = ...;
                  
                  if(!p.teleport(lobby)) {
                     // TODO: denied in both before-teleportation and lobby places?
                     p.kickPlayer("");
                  }
               }
            }
            
            return false;
         }
      }
      
      return true;
   }
}

C'est très moche mais ça te permet d'avoir des idées, après je te conseille fortement de refaire à ta sauce.


Pour en revenir à ton problème initial de répartir des joueurs entre plusieurs équipes :
Java:
/**
 * Distribue uniformément les joueurs spécifiés dans les équipes spécifiées.
 */
public static void assignPlayersToTeams(List<Player> players, List<Team> teams) {
   if(players == null || teams == null || teams.isEmpty()) {
      throw new IllegalArgumentException("todo");
   }
   
   int nextTeamIndex = 0;
   for(Player p : players) {
      teams.get(nextTeamIndex).add(p);
      nextTeamIndex = (nextTeamIndex + 1) % teams.size();
   }
}

/**
 * Affecte le joueur spécifié de façon à ce que qu'à terme toutes les équipes aient le même nombre
 *  de joueurs.
 *
 *  @return L'équipe affectée au joueur.
 */
public static Team assignPlayerToTeam(Player player, List<Team> teams) {
   if(player == null || teams == null || teams.isEmpty()) {
      throw new IllegalArgumentException("todo");
   }
   
   int playerCount = 0;
   for(Team team : teams) {
      playerCount += team.size();
   }
   
   final int expectedPlayersPerTeam = playerCount / teams.size() + 1;
   
   for(Team team : teams) {
      if(team.size() < expectedPlayersPerTeam) {
         team.add(player);
         
         return team;
      }
   }
   
   // toutes les équipes ont exactement la même taille
   Team team = teams.get(0); // possibilité de prendre un indice aléatoire
   team.add(player);
   
   return team;
}

/**
 * Réparti tous les joueurs de toutes les équipes de façon à ce que toutes les équipes
 *  aient toutes le même nombre de joueurs.
 */
public static void harmonize(List<Team> teams) {
   if(teams == null || teams.isEmpty()) {
      return; // il n'y a rien à faire.
   }
   
   // version alternative avec un Stream<T> (cf. ci-dessus)
   final int playerCount = teams.stream().mapToInt(team -> team.size()).sum();
   
   final int expectedPlayersPerTeam = playerCount / teams.size() + 1;
   
   List<Team> tooManyPlayers = new ArrayList<>(teams.size());
   List<Team> enoughPlayers = new ArrayList<>(teams.size());
   List<Team> notEnoughPlayers = new ArrayList<>(teams.size());
   
   for(Team team : teams) {
      if(team.size() > expectedPlayersPerTeam) {
         tooManyPlayers.add(team);
      }
      else if(team.size() < expectedPlayersPerTeam) {
         notEnoughPlayers.add(team);
      }
      else {
         enoughPlayers.add(team);
      }
   }
   
   // on veut éviter de bouger toujours les mêmes joueurs
   final Random random = new Random();
// Collections.shuffle(tooManyPlayers);  pas besoin de mélanger cette liste
   Collections.shuffle(enoughPlayers);
   Collections.shuffle(notEnoughPlayers);
   
   while(!tooManyPlayers.isEmpty()) {
      Team overpopulated = tooManyPlayers.get(0);
      int overpopulation = overpopulated.size() - expectedPlayersPerTeam;
      
      while(overpopulation-- > 0) {
         Team underpopulated = notEnoughPlayers.get(0);
         
         int moverIndex = random.nextInt(overpopulated.size());
         underpopulated.add(overpopulated.remove(moverIndex));
         
         if(underpopulated.size() == expectedPlayersPerTeam) {
            notEnoughPlayers.remove(0);
            enoughPlayers.add(underpopulated);
         }
      }
      
      if(overpopulated.size() == expectedPlayersPerTeam) {
         tooManyPlayers.remove(0);
         enoughPlayers.add(overpopulated);
      }
   }
   
   // il manque désormais entre 1 et 2 joueurs à certaines équipes, mais aucune équipe
   // n'est surpeuplée, donc on ne déplace des joueurs que dans les équipes dont il manque
   // deux joueurs.
   notEnoughPlayers.removeIf(team -> (expectedPlayersPerTeam - team.size()) == 1);
   while(!notEnoughPlayers.isEmpty()) {
      Team extremlyUnderpopulated = notEnoughPlayers.get(0);
      int underpopulation = expectedPlayersPerTeam - extremlyUnderpopulated.size() - 1; // -1 pour devenir légèrement sous-peuplé (pas assez de joueur pour égaliser parfaitement)
      
      while(underpopulation-- > 0) {
         Team rightlyPopulated = enoughPlayers.remove(0);
         
         int moverIndex = random.nextInt(rightlyPopulated.size());
         extremlyUnderpopulated.add(rightlyPopulated.remove(moverIndex));
      }
      
      // désormais légèrement sous-peuplée
      notEnoughPlayers.remove(0);
   }
}

La première fonction répartie simplement les joueurs un par un aux équipes une par une, pour savoir l'équipe j'utilise une indice et pour le forcer à rester dans les dimensions ma liste j'utilise le modulo (n % n == 0), c'est aussi possible de faire avec
Java:
nextTeamIndex += 1;
if(nextTeamIndex == teams.size()) {
   nextTeamIndex = 0;
}

Mais l'avantage du modulo c'est que ça marche avec à peu près n'importe quel nombre (5 % 5 == 0, 7 % 5 == 2, 11 % 5 == 1, -1 % 5 = 4).
Par contre s'il y a déjà des joueurs avant les équipes seront déséquilibrées, la fonction est plutôt faite quand toutes les équipes sont vide.


La deuxième fonction calcul le nombre de joueur maximal par équipe (par ex. 3 si 7 joueurs) et rajoute un joueur à la première équipe qui n'a pas ce nombre de joueurs, et de cette manière la répartition des joueurs restera équilibrée si les équipes étaient déjà équilibrées avant.


La troisième fonction répartie toutes les équipes de telle sorte à ce qu'il y ait au plus un seul joueur de différence entre deux équipes dans toutes les équipes passées en paramètre. Aucun joueur ne changera d'équipe si les équipes sont déjà équilibrées, cependant si une équipe contient au moins deux joueurs de plus qu'une autre équipe, alors un joueur aléatoire sera transféré à une autre équipe.

Le code assume que les équipes ont une fonction Player remove(int index) { ... } qui supprime puis renvoit l'élément présent à l'indice donné en paramètre.

TL;DR: Utilise assignPlayersToTeams() à la création de la partie lorsque tes équipes sont vides, assignPlayerToTeam() pour ajouter un joueur en cours de partie, et harmonize() pour rééquilibrer toutes les équipes si jamais tu souhaites gérer les joueurs qui se déconnectent.

Cordialement,
ShE3py
 

Kenda

Architecte en herbe
16 Juillet 2016
292
1
2
125
32
www.youtube.com
Bonjour (désolé de la réponse un peu tardive).

Merci pour tout ces conseils, c'est vrai que, quand je regarde mon code, et le tiens, c'est totalement différent, et c'est vrai que c'est un peu fouilli.
Après, pour ma défense, c'est la première fois que je fais un système de Teams, un peu à ma sauce, donc bon, faut expérimenté :(

Je garde le post sous la main, je pense refaire totalement mon système et l'adapter au mieux possible à mon besoin.