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 }