Coverage Summary for Class: Model (it.polimi.ingsw.Model)

Class Class, % Method, % Branch, % Line, %
Model 100% (1/1) 100% (31/31) 77,3% (68/88) 93% (160/172)


1 package it.polimi.ingsw.Model; 2  3 import it.polimi.ingsw.Exceptions.Container.EmptyContainerException; 4 import it.polimi.ingsw.Exceptions.Container.FullContainerException; 5 import it.polimi.ingsw.Exceptions.Container.InvalidContainerIndexException; 6 import it.polimi.ingsw.Logger; 7 import it.polimi.ingsw.Misc.OptionalValue; 8 import it.polimi.ingsw.Model.Enums.*; 9  10 import java.io.*; 11 import java.util.*; 12 import java.util.stream.Collectors; 13  14 /** 15  * As the name suggests, this class is the game's Model. The Model's role is to keep track of the game's state and 16  * provide the tools to modify it. This class is not able to detect changes to the underlying data. If you wish for that 17  * functionality look at {@link ModelWrapper} 18  */ 19 public class Model implements Serializable { 20  @Serial 21  private static final long serialVersionUID = 101L; // convention: 1 for model, (01 -> 99) for objects 22  private final IslandField islandField; 23  private final GameMode gameMode; 24  private final StudentBag studentBag; 25  private final List<PlayerBoard> playerBoards; 26  private final Map<PawnColour, PlayerBoard> teachers; 27  private final TeamMapper teamMap; 28  private final TurnOrder turnOrder; 29  private final EffectTracker effects; 30  private final List<Cloud> clouds; 31  private final List<CharacterCard> characterCards; 32  private int coinReserve; 33  34  /** 35  * Constructs a DEBUG ONLY version of the model, to be used for testing purposes. 36  * 37  * @param islandField provide a reference to an external {@link IslandField} 38  * @param gameMode select the game mode 39  * @param studentBag provide a reference to an external {@link StudentBag} 40  * @param playerBoards provide references to external {@link PlayerBoard}s 41  * @param teachers provide a map from a {@link PawnColour} to one of the external {@link PlayerBoard}s 42  * @param teamMap provide a reference to an external {@link TeamMapper} 43  * @param turnOrder provide a reference to an external {@link TurnOrder} 44  * @param effects provide a reference to an external {@link EffectTracker} 45  * @param clouds provide references to external {@link Cloud}s 46  * @param characterCards provide references to external {@link CharacterCard}s 47  * @param coinReserve select the amount of coins left in the bank 48  * @param coinPerPlayerBoard this number times the amount of players will be subtracted from coinReserve 49  */ 50  public Model( 51  IslandField islandField, 52  GameMode gameMode, 53  StudentBag studentBag, 54  List<PlayerBoard> playerBoards, 55  Map<PawnColour, PlayerBoard> teachers, 56  TeamMapper teamMap, 57  TurnOrder turnOrder, 58  EffectTracker effects, 59  List<Cloud> clouds, 60  List<CharacterCard> characterCards, 61  int coinReserve, 62  int coinPerPlayerBoard 63  ) { 64  this.islandField = islandField; 65  this.gameMode = gameMode; 66  this.studentBag = studentBag; 67  this.playerBoards = playerBoards; 68  this.teachers = teachers; 69  this.teamMap = teamMap; 70  this.turnOrder = turnOrder; 71  this.effects = effects; 72  this.clouds = clouds; 73  this.characterCards = characterCards; 74  this.coinReserve = coinReserve - coinPerPlayerBoard * playerBoards.size(); 75  } 76  77  /** 78  * Construct a Model object 79  * 80  * @param gameMode selects the {@link GameMode} 81  * @param playerNicknames a vararg list of {@link String} representing each player. The first nickname (at index 0) will 82  * receive ID = 0, the second ID = 1, and so on and so forth. 83  */ 84  public Model(GameMode gameMode, String... playerNicknames) { 85  final int nop = playerNicknames.length; 86  this.islandField = new IslandField(); 87  this.gameMode = gameMode; 88  this.studentBag = new StudentBag(24); 89  this.playerBoards = new ArrayList<>(); 90  this.teachers = new EnumMap<>(PawnColour.class); 91  this.coinReserve = 20 - nop; // hp: we assume 20 as amount of available coins just like the real game. 92  93  for (int i = 0; i < nop; i++) { 94  this.playerBoards.add(new PlayerBoard(i, nop, playerNicknames[i], this.studentBag)); 95  } // add generate player based on nickname and store it 96  this.turnOrder = new TurnOrder(playerBoards); 97  this.teamMap = new TeamMapper(this.playerBoards); 98  if (this.gameMode.equals(GameMode.ADVANCED)) { 99  this.characterCards = CharacterDeckGenerator.generateCardSet(this); 100  this.coinReserve = 20 - nop; 101  } else { 102  this.characterCards = null; 103  } 104  this.effects = new EffectTracker(); 105  this.clouds = new ArrayList<>(nop); 106  //2 players: 2 cloud tiles - 3 players: 3 cloud tiles: 4 players: 4 cloud tiles 107  for (int i = 0; i < nop; i++) { 108  clouds.add(new Cloud(i)); 109  } 110  try { 111  refillClouds(); 112  } catch (FullContainerException e) { 113  Logger.severe(e.getMessage()); 114  throw new RuntimeException(e); 115  } 116  } 117  118  /** 119  * Refill the cloud tiles 120  * 121  * @throws FullContainerException if one or more cloud tiles were found full while trying to fill them up 122  */ 123  public void refillClouds() throws FullContainerException { 124  for (Cloud cloud : this.clouds) { 125  cloud.fill(studentBag.multipleExtraction(this.playerBoards.size() == 3 ? 4 : 3)); 126  } 127  } 128  129  /** 130  * Serializes the game model to a new object. 131  * 132  * @return a copy of the GameBoard object or null if the Serialization was not possible <br> 133  * <b>Note:</b> once called, all changes to the original GameBoard object won't be reflected in the instance returned 134  * by this method 135  */ 136  public Model copy() { 137  try { 138  ByteArrayInputStream stream = new ByteArrayInputStream(getSerializedModel()); 139  ObjectInputStream reader = new ObjectInputStream(stream); 140  return (Model) reader.readObject(); 141  } catch (Exception e) { 142  e.printStackTrace(); 143  } 144  return null; // never executed under normal circumstances 145  } 146  147  /** 148  * Serializes the game model to a new de-serializable byte array. 149  * 150  * @return a copy of the GameBoard object. <br> 151  * <b>Note:</b> once called, all changes to the original GameBoard object won't be reflected in the instance returned 152  * by this method 153  */ 154  private byte[] getSerializedModel() throws IOException { 155  ByteArrayOutputStream out = new ByteArrayOutputStream(); 156  ObjectOutputStream writer = new ObjectOutputStream(out); 157  writer.writeObject(this); 158  return out.toByteArray(); 159  } 160  161  /** 162  * @return the amount of coins left in the bank, not yet collected by players 163  */ 164  public int getCoinReserve() { 165  return coinReserve; 166  } 167  168  /** 169  * @return an Unmodifiable {@link List} of the {@link CharacterCard}s currently in the game 170  */ 171  public List<CharacterCard> getCharacterCards() { 172  return List.copyOf(this.characterCards); 173  } 174  175  /** 176  * @return the {@link GameMode} this Model is running 177  */ 178  public GameMode getGameMode() { 179  return gameMode; 180  } 181  182  /** 183  * @return an Unmodifiable {@link Map} from {@link PawnColour} to {@link PlayerBoard}, mapping a teacher's colour to 184  * its owner. If the teacher has no owner, the mapping will be null. 185  */ 186  public Map<PawnColour, PlayerBoard> getTeachers() { 187  return Map.copyOf(this.teachers); 188  } 189  190  /** 191  * Accessing a {@link PlayerBoard} can only be done through its ID since the nicknames can be non-unique 192  * 193  * @param id the ID of the {@link PlayerBoard} to fetch for 194  * @return the fetched {@link PlayerBoard} 195  * @throws InvalidContainerIndexException if no board can be found matching the given ID 196  */ 197  public PlayerBoard getMutablePlayerBoard(int id) throws InvalidContainerIndexException { 198  return playerBoards.stream() 199  .filter(p -> p.getId() == id) 200  .findAny() 201  .orElseThrow(() -> new InvalidContainerIndexException("Playerboards")); 202  } 203  204  /** 205  * @return a reference to the {@link EffectTracker} 206  */ 207  public EffectTracker getMutableEffects() { 208  return effects; 209  } 210  211  /** 212  * @return a non-empty {@link OptionalValue} containing the winners of the game if {@link #isGameOver()} returns true, 213  * else an empty {@link OptionalValue} 214  */ 215  public OptionalValue<List<PlayerBoard>> getWinners() { 216  if (!isGameOver()) { 217  return OptionalValue.empty(); 218  } 219  220  PlayerBoard currentPlayer = this.getMutableTurnOrder().getMutableCurrentPlayer(); 221  222  // immediate win for 3 islands left 223  if (this.getMutableIslandField().getMutableGroups().size() == 3) { 224  return OptionalValue.of(this.getMutablePlayerBoardsByTeamID(this.getTeamMapper().getTeamID(currentPlayer))); 225  } 226  227  // calculate best players depending on tower storage. 228  List<PlayerBoard> sortByTowerAndTeacherCount = this.getMutablePlayerBoards().stream() 229  .sorted((e1, e2) -> { 230  int countE1 = this.getTeamMapper().getMutableTowerStorage(e1).getTowerCount(); 231  int countE2 = this.getTeamMapper().getMutableTowerStorage(e2).getTowerCount(); 232  if (countE1 != countE2) return countE1 - countE2; 233  else { 234  return -this.getOwnTeamTeacherCount(e1) + this.getOwnTeamTeacherCount(e2); 235  } 236  }) 237  .toList(); 238  PlayerBoard firstPlayer = sortByTowerAndTeacherCount.get(0); 239  int firstPlayerTeamTeacherCount = this.getOwnTeamTeacherCount(firstPlayer); 240  int firstPlayerTowerCount = this.getTeamMapper().getMutableTowerStorage(firstPlayer).getTowerCount(); 241  List<PlayerBoard> winners = sortByTowerAndTeacherCount.stream() 242  .takeWhile(e -> this.getTeamMapper().getMutableTowerStorage(e).getTowerCount() == firstPlayerTowerCount) 243  .takeWhile(e -> this.getOwnTeamTeacherCount(e) == firstPlayerTeamTeacherCount) 244  .toList(); 245  246  return OptionalValue.of(winners); 247  } 248  249  /** 250  * Checks to see if the game is over, this function should be called at the end of each complete set of changes to the 251  * Model. 252  * 253  * @return true if the game is over, else false. 254  */ 255  public boolean isGameOver() { 256  // Check if current player has no towers left 257  PlayerBoard currentPlayer = this.getMutableTurnOrder().getMutableCurrentPlayer(); 258  boolean noTowersLeft = this.getTeamMapper().getMutableTowerStorage(currentPlayer).getTowerCount() == 0; 259  // Check if only three island groups remain 260  boolean threeIslandsLeft = this.getMutableIslandField().getMutableGroups().size() <= 3; 261  // Check if all assistant cards have been used 262  boolean allCardsUsed = this.getMutableTurnOrder().getMutableCurrentPlayer() 263  .getMutableAssistantCards().stream() 264  .allMatch(AssistantCard::getUsed); 265  // Check if bag is empty 266  boolean emptyBag = this.getMutableStudentBag().isEmpty(); 267  boolean noViableCloudTiles = this.getClouds().stream().allMatch(cloud -> cloud.getContents().size() == 0); 268  269  return noTowersLeft || 270  threeIslandsLeft || 271  this.getMutableTurnOrder().getGamePhase() == GamePhase.SETUP && 272  (allCardsUsed || (emptyBag && noViableCloudTiles)); 273  } 274  275  /** 276  * @return a reference to the {@link TurnOrder} 277  */ 278  public TurnOrder getMutableTurnOrder() { 279  return turnOrder; 280  } 281  282  /** 283  * @return a reference to the {@link Island} 284  */ 285  public IslandField getMutableIslandField() { 286  return islandField; 287  } 288  289  /** 290  * In case there are 4 players, teams are enabled. 291  * 292  * @param teamID the ID of the team you wish to fetch for 293  * @return an Unmodifiable {@link List} of {@link PlayerBoard}/s that are part of the specified Team 294  */ 295  public List<PlayerBoard> getMutablePlayerBoardsByTeamID(TeamID teamID) { 296  return this.getMutablePlayerBoards().stream() 297  .filter(player -> teamID.equals(this.getTeamMapper().getTeamID(player))) 298  .toList(); 299  } 300  301  /** 302  * @return a reference to the {@link TeamMapper} 303  */ 304  public TeamMapper getTeamMapper() { 305  return teamMap; 306  } 307  308  /** 309  * @return an Unmodifiable {@link List} of the {@link PlayerBoard}s (which can be modified) 310  */ 311  public List<PlayerBoard> getMutablePlayerBoards() { 312  return List.copyOf(playerBoards); 313  } 314  315  /** 316  * @param pb the {@link PlayerBoard} you wish to search the owned teachers of 317  * @return the amount of teachers linked to such player 318  */ 319  private int getOwnTeamTeacherCount(PlayerBoard pb) { 320  return this.getOwnTeamTeacherCount(this.getTeamMapper().getTeamID(pb)); 321  } 322  323  /** 324  * @return a reference to the {@link StudentBag} 325  */ 326  public StudentBag getMutableStudentBag() { 327  return studentBag; 328  } 329  330  /** 331  * @return an Unmodifiable {@link List} of the {@link Cloud}s (which can be modified) 332  */ 333  public List<Cloud> getClouds() { 334  return List.copyOf(this.clouds); 335  } 336  337  /** 338  * @param teamID the {@link TeamID} you wish to search the owned teachers of 339  * @return the amount of teachers linked to such team 340  */ 341  private int getOwnTeamTeacherCount(TeamID teamID) { 342  return this.getMutablePlayerBoardsByTeamID(teamID).stream() 343  .map(this::getOwnTeachers) 344  .mapToInt(List::size) 345  .sum(); 346  } 347  348  /** 349  * @param pb the {@link PlayerBoard} you wish to search the owned teachers of 350  * @return an Unmodifiable {@link List} of the {@link PawnColour}s representing the teachers owned by pb 351  */ 352  public List<PawnColour> getOwnTeachers(PlayerBoard pb) { 353  return this.teachers.entrySet().stream() 354  .filter(e -> e.getValue().equals(pb)) 355  .map(Map.Entry::getKey) 356  .toList(); 357  } 358  359  /** 360  * Mother nature can move to an island and, once moved there must act out its power 361  * 362  * @param steps the steps mother nature will take (can be both positive or negative or zero) 363  */ 364  public void moveAndActMotherNature(int steps) { 365  this.islandField.moveMotherNature(steps); 366  actMotherNaturePower(this.islandField.getMutableMotherNaturePosition()); 367  } 368  369  /** 370  * Acts out mother nature's power, unifying {@link IslandGroup}s adjacent to mnp 371  * 372  * @param mnp an {@link IslandGroup} representing Mother nature's position. 373  */ 374  public void actMotherNaturePower(IslandGroup mnp) { 375  if (mnp.getMutableNoEntryTiles().isEmpty()) { 376  OptionalValue<TeamID> optInfluencer = getInfluencerOf(mnp); 377  if (optInfluencer.isPresent()) { 378  TeamID newInfluencer = optInfluencer.get(); 379  if ( 380  mnp.getTowerColour().isEmpty() || 381  mnp.getTowerColour().get() != TowerColour.fromTeamId(newInfluencer) 382  ) { 383  mnp.swapTower(this.teamMap.getMutableTowerStorage(newInfluencer)); 384  } 385  } 386  this.islandField.joinGroups(); 387  } else { 388  mnp.resetNoEntry(); 389  } 390  } 391  392  /** 393  * @param ig the {@link IslandGroup} to find the influencer of 394  * @return the {@link TeamID} that holds influence over ig, wrapped in a {@link OptionalValue} 395  */ 396  public OptionalValue<TeamID> getInfluencerOf(IslandGroup ig) { 397  Map<PawnColour, Integer> sc = ig.getStudentCount(); 398  Map<TeamID, Integer> ic = new HashMap<>(); // maps the team with the influence count 399  400  for (Map.Entry<PawnColour, Integer> e : sc.entrySet()) { 401  PawnColour colour = e.getKey(); 402  if (teachers.get(colour) == null) { 403  continue; 404  } // 405  int count = e.getValue(); 406  if (effects.isPawnColourDenied()) { 407  if (colour == effects.getDeniedPawnColour().get()) { 408  continue; 409  } 410  } 411  ic.merge(this.teamMap.getTeamID(this.teachers.get(colour)), count, Integer::sum); 412  } 413  414  if (!effects.isTowerInfluenceDenied()) { 415  ig.getTowerColour() 416  .ifPresent(tc -> ic.merge(tc.getTeamID(), ig.getTowerCount(), Integer::sum)); 417  } 418  419  if (effects.isInfluenceIncreased()) { 420  TeamID currentTeam = this.teamMap.getTeamID(turnOrder.getMutableCurrentPlayer()); 421  ic.merge(currentTeam, 2, Integer::sum); 422  } 423  List<Map.Entry<TeamID, Integer>> tbi = ic.entrySet().stream() // tbi is team by influence 424  .sorted(Comparator.comparingInt(Map.Entry::getValue)) 425  .collect(Collectors.toCollection(ArrayList::new)); 426  Collections.reverse(tbi); 427  428  this.effects.reset(); 429  430  switch (tbi.size()) { 431  case 0: 432  return OptionalValue.empty(); 433  case 1: 434  return OptionalValue.of(tbi.get(0).getKey()); 435  default: { 436  if (tbi.get(0).getValue() > tbi.get(1).getValue()) 437  return OptionalValue.of(tbi.get(0).getKey()); 438  else return ig.getTowerColour().flatMap(tc -> OptionalValue.of(tc.getTeamID())); 439  } 440  } 441  } 442  443  /** 444  * Adds a student to the dining room of a player. 445  * 446  * @param student the {@link PawnColour} of the student you wish to add 447  * @param pb the {@link PlayerBoard} you wish to add student to 448  * @throws FullContainerException if the dining room has no more free space to fit the student 449  */ 450  public void addStudentToDiningRoom(PawnColour student, PlayerBoard pb) throws FullContainerException { 451  boolean shouldGiveCoin = pb.unsafeAddStudentToDiningRoom(student); 452  if (shouldGiveCoin && this.coinReserve > 0) { 453  this.coinReserve -= 1; 454  pb.addCoin(); 455  } 456  // trigger calculation of new teacher placements 457  PlayerBoard owner = this.teachers.get(student); 458  if ( 459  owner == null || 460  owner.getDiningRoomCount(student) < pb.getDiningRoomCount(student) || 461  ( 462  this.effects.isAlternativeTeacherAssignmentEnabled() && 463  owner.getDiningRoomCount(student) == pb.getDiningRoomCount(student) 464  ) 465  ) { 466  this.setTeacher(student, pb); 467  } 468  } 469  470  /** 471  * Given a teacher, assigns it to a player 472  * 473  * @param teacher the {@link PawnColour} of the teacher you wish to select 474  * @param player the {@link PlayerBoard} you wish to link teacher to 475  */ 476  protected void setTeacher(PawnColour teacher, PlayerBoard player) { 477  if (player != null) { 478  teachers.put(teacher, player); 479  } else { 480  teachers.remove(teacher); 481  } 482  } 483  484  /** 485  * Removes a student to the dining room of a player. 486  * 487  * @param student the {@link PawnColour} of the student you wish to remove 488  * @param pb the {@link PlayerBoard} you wish to remove student from 489  * @throws EmptyContainerException if the dining room has no more students to remove 490  */ 491  public void removeStudentFromDiningRoom(PawnColour student, PlayerBoard pb) throws EmptyContainerException { 492  pb.unsafeRemoveStudentFromDiningRoom(student); 493  494  // trigger calculation of new teacher placements 495  PlayerBoard owner = this.teachers.get(student); 496  // if we were the owner of teacher, we need to find the correct teacher owner 497  if (owner != null && owner.equals(pb)) { 498  for (PlayerBoard player : this.playerBoards) { 499  if (player.getDiningRoomCount(student) > owner.getDiningRoomCount(student)) owner = player; 500  } 501  if (owner.getDiningRoomCount(student) == 0) { 502  owner = null; 503  } 504  this.setTeacher(student, owner); 505  } 506  } 507  508  /** 509  * @param amount the amount of currency to bring back into the bank 510  */ 511  public void addCoinToReserve(int amount) { 512  this.coinReserve += amount; 513  } 514 }