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 }