Skip to content

Package: DefaultGame

DefaultGame

nameinstructionbranchcomplexitylinemethod
DefaultGame(int, State, Map, MoveChecker, ObserverFactoryProvider)
M: 9 C: 0
0%
M: 0 C: 0
100%
M: 1 C: 0
0%
M: 2 C: 0
0%
M: 1 C: 0
0%
DefaultGame(int, State, Map, long, MoveChecker, ObserverFactoryProvider)
M: 0 C: 63
100%
M: 0 C: 2
100%
M: 0 C: 2
100%
M: 0 C: 15
100%
M: 0 C: 1
100%
abortRequested()
M: 18 C: 0
0%
M: 2 C: 0
0%
M: 2 C: 0
0%
M: 4 C: 0
0%
M: 1 C: 0
0%
addObserver(Observer)
M: 6 C: 0
0%
M: 0 C: 0
100%
M: 1 C: 0
0%
M: 2 C: 0
0%
M: 1 C: 0
0%
applyMoveIfPossible(Player, Move)
M: 0 C: 18
100%
M: 0 C: 0
100%
M: 0 C: 1
100%
M: 0 C: 5
100%
M: 0 C: 1
100%
applyNextPossibleMove(CompletionService, Map)
M: 4 C: 56
93%
M: 1 C: 9
90%
M: 1 C: 5
83%
M: 0 C: 15
100%
M: 0 C: 1
100%
callObservers(ConsumerE)
M: 0 C: 21
100%
M: 0 C: 2
100%
M: 0 C: 2
100%
M: 0 C: 5
100%
M: 0 C: 1
100%
checkAndAdjustPlayerStatesIfNecessary()
M: 3 C: 59
95%
M: 1 C: 7
88%
M: 1 C: 4
80%
M: 1 C: 17
94%
M: 0 C: 1
100%
checkMove(Move)
M: 0 C: 18
100%
M: 0 C: 2
100%
M: 0 C: 2
100%
M: 0 C: 3
100%
M: 0 C: 1
100%
close()
M: 0 C: 4
100%
M: 0 C: 0
100%
M: 0 C: 1
100%
M: 0 C: 2
100%
M: 0 C: 1
100%
determineNextAvailableMove(Future)
M: 0 C: 16
100%
M: 0 C: 0
100%
M: 0 C: 1
100%
M: 0 C: 4
100%
M: 0 C: 1
100%
gameToString()
M: 15 C: 0
0%
M: 0 C: 0
100%
M: 1 C: 0
0%
M: 1 C: 0
0%
M: 1 C: 0
0%
getId()
M: 0 C: 3
100%
M: 0 C: 0
100%
M: 0 C: 1
100%
M: 0 C: 1
100%
M: 0 C: 1
100%
getPlayers()
M: 0 C: 4
100%
M: 0 C: 0
100%
M: 0 C: 1
100%
M: 0 C: 1
100%
M: 0 C: 1
100%
getState()
M: 4 C: 0
0%
M: 0 C: 0
100%
M: 1 C: 0
0%
M: 1 C: 0
0%
M: 1 C: 0
0%
getStrategies()
M: 3 C: 0
0%
M: 0 C: 0
100%
M: 1 C: 0
0%
M: 1 C: 0
0%
M: 1 C: 0
0%
handleIllegalMove(Player, Optional, String)
M: 0 C: 20
100%
M: 0 C: 0
100%
M: 0 C: 1
100%
M: 0 C: 4
100%
M: 0 C: 1
100%
handleOverdueMove(Player, Optional)
M: 0 C: 13
100%
M: 0 C: 0
100%
M: 0 C: 1
100%
M: 0 C: 3
100%
M: 0 C: 1
100%
isFinished()
M: 0 C: 13
100%
M: 0 C: 0
100%
M: 0 C: 1
100%
M: 0 C: 3
100%
M: 0 C: 1
100%
isStarted()
M: 0 C: 3
100%
M: 0 C: 0
100%
M: 0 C: 1
100%
M: 0 C: 1
100%
M: 0 C: 1
100%
lambda$applyMoveIfPossible$9(Player, Move, Observer)
M: 0 C: 9
100%
M: 0 C: 0
100%
M: 0 C: 1
100%
M: 0 C: 1
100%
M: 0 C: 1
100%
lambda$applyNextPossibleMove$5(Player, Player, Observer)
M: 0 C: 9
100%
M: 0 C: 0
100%
M: 0 C: 1
100%
M: 0 C: 2
100%
M: 0 C: 1
100%
lambda$checkAndAdjustPlayerStatesIfNecessary$10(Player)
M: 0 C: 5
100%
M: 0 C: 0
100%
M: 0 C: 1
100%
M: 0 C: 1
100%
M: 0 C: 1
100%
lambda$checkAndAdjustPlayerStatesIfNecessary$11(Player)
M: 0 C: 5
100%
M: 0 C: 0
100%
M: 0 C: 1
100%
M: 0 C: 1
100%
M: 0 C: 1
100%
lambda$checkAndAdjustPlayerStatesIfNecessary$12(Player)
M: 0 C: 7
100%
M: 0 C: 0
100%
M: 0 C: 1
100%
M: 0 C: 1
100%
M: 0 C: 1
100%
lambda$checkAndAdjustPlayerStatesIfNecessary$13(Observer)
M: 0 C: 7
100%
M: 0 C: 0
100%
M: 0 C: 1
100%
M: 0 C: 1
100%
M: 0 C: 1
100%
lambda$handleIllegalMove$7(Player, Optional, String, Observer)
M: 0 C: 10
100%
M: 0 C: 0
100%
M: 0 C: 1
100%
M: 0 C: 1
100%
M: 0 C: 1
100%
lambda$handleOverdueMove$8(Player, Optional, Observer)
M: 0 C: 9
100%
M: 0 C: 0
100%
M: 0 C: 1
100%
M: 0 C: 1
100%
M: 0 C: 1
100%
lambda$isFinished$3(Player)
M: 0 C: 5
100%
M: 0 C: 0
100%
M: 0 C: 1
100%
M: 0 C: 1
100%
M: 0 C: 1
100%
lambda$makeMove$1(Set, Observer)
M: 0 C: 8
100%
M: 0 C: 0
100%
M: 0 C: 1
100%
M: 0 C: 1
100%
M: 0 C: 1
100%
lambda$makeMove$2(Observer)
M: 7 C: 0
0%
M: 0 C: 0
100%
M: 1 C: 0
0%
M: 1 C: 0
0%
M: 1 C: 0
0%
lambda$start$0(Observer)
M: 0 C: 7
100%
M: 0 C: 0
100%
M: 0 C: 1
100%
M: 0 C: 1
100%
M: 0 C: 1
100%
lambda$submitMoveComputingRequests$4(Strategy, Player)
M: 0 C: 9
100%
M: 0 C: 0
100%
M: 0 C: 1
100%
M: 0 C: 1
100%
M: 0 C: 1
100%
lambda$tryToApplyNextPossibleMove$6(Player, Observer)
M: 0 C: 8
100%
M: 0 C: 0
100%
M: 0 C: 1
100%
M: 0 C: 1
100%
M: 0 C: 1
100%
makeMove()
M: 5 C: 49
91%
M: 1 C: 5
83%
M: 1 C: 3
75%
M: 2 C: 13
87%
M: 0 C: 1
100%
removeObserver(Observer)
M: 6 C: 0
0%
M: 0 C: 0
100%
M: 1 C: 0
0%
M: 2 C: 0
0%
M: 1 C: 0
0%
start()
M: 0 C: 23
100%
M: 0 C: 2
100%
M: 0 C: 2
100%
M: 0 C: 6
100%
M: 0 C: 1
100%
static {...}
M: 0 C: 1
100%
M: 0 C: 0
100%
M: 0 C: 1
100%
M: 0 C: 1
100%
M: 0 C: 1
100%
submitMoveComputingRequests(Set, CompletionService)
M: 12 C: 42
78%
M: 1 C: 3
75%
M: 1 C: 2
67%
M: 1 C: 8
89%
M: 0 C: 1
100%
tryToApplyNextPossibleMove(CompletionService, Map)
M: 10 C: 102
91%
M: 2 C: 8
80%
M: 2 C: 4
67%
M: 4 C: 27
87%
M: 0 C: 1
100%

Coverage

1: /*
2: * Copyright © 2020 Fachhochschule für die Wirtschaft (FHDW) Hannover
3: *
4: * This file is part of gaming-core.
5: *
6: * Gaming-core is free software: you can redistribute it and/or modify it under
7: * the terms of the GNU General Public License as published by the Free Software
8: * Foundation, either version 3 of the License, or (at your option) any later
9: * version.
10: *
11: * Gaming-core is distributed in the hope that it will be useful, but WITHOUT
12: * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
13: * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
14: * details.
15: *
16: * You should have received a copy of the GNU General Public License along with
17: * gaming-core. If not, see <http://www.gnu.org/licenses/>.
18: */
19: package de.fhdw.gaming.core.domain;
20:
21: import java.util.ArrayList;
22: import java.util.Collection;
23: import java.util.Collections;
24: import java.util.LinkedHashMap;
25: import java.util.List;
26: import java.util.Map;
27: import java.util.Objects;
28: import java.util.Optional;
29: import java.util.Set;
30: import java.util.concurrent.CompletionService;
31: import java.util.concurrent.ExecutionException;
32: import java.util.concurrent.ExecutorCompletionService;
33: import java.util.concurrent.ExecutorService;
34: import java.util.concurrent.Executors;
35: import java.util.concurrent.Future;
36: import java.util.concurrent.TimeUnit;
37: import java.util.stream.Collectors;
38:
39: import de.fhdw.gaming.core.domain.util.ConsumerE;
40:
41: /**
42: * Implements {@link Game}.
43: *
44: * @param <S> The type of game states.
45: * @param <M> The type of game moves.
46: * @param <P> The type of game players.
47: * @param <ST> The type of game strategies.
48: */
49: @SuppressWarnings("PMD.GodClass")
50: public abstract class DefaultGame<P extends Player, S extends State<P, S>, M extends Move<P, S>,
51: ST extends Strategy<P, S, M>> implements Game<P, S, M, ST> {
52:
53: /**
54: * The ID of this game.
55: */
56: private final int id;
57: /**
58: * The game state.
59: */
60: private S state;
61: /**
62: * The players of the game together with their strategies.
63: */
64: private final Map<String, ST> strategies;
65: /**
66: * The maximum computation time per move in seconds.
67: */
68: private final long maxComputationTimePerMove;
69: /**
70: * The move checker.
71: */
72: private final MoveChecker<P, S, M> moveChecker;
73: /**
74: * The registered observers.
75: */
76: private final List<Observer> observers;
77: /**
78: * The executor for submitting tasks for choosing a move.
79: */
80: private final ExecutorService executorService;
81: /**
82: * {@code true} if the game has been started, else {@code false}.
83: */
84: private boolean started;
85:
86: /**
87: * Creates a game. Uses {@link GameBuilder#DEFAULT_MAX_COMPUTATION_TIME_PER_MOVE} as maximum computation time per
88: * move.
89: *
90: * @param id The ID of this game.
91: * @param initialState The initial state of the game.
92: * @param strategies The players' strategies.
93: * @param moveChecker The move checker.
94: * @param observerFactoryProvider The {@link ObserverFactoryProvider}.
95: * @throws IllegalArgumentException if the player sets do not match.
96: * @throws InterruptedException if creating the game has been interrupted.
97: */
98: public DefaultGame(final int id, final S initialState, final Map<String, ST> strategies,
99: final MoveChecker<P, S, M> moveChecker, final ObserverFactoryProvider observerFactoryProvider)
100: throws IllegalArgumentException, InterruptedException {
101:
102: this(
103: id,
104: initialState,
105: strategies,
106: GameBuilder.DEFAULT_MAX_COMPUTATION_TIME_PER_MOVE,
107: moveChecker,
108: observerFactoryProvider);
109: }
110:
111: /**
112: * Creates a game.
113: *
114: * @param id The ID of this game.
115: * @param initialState The initial state of the game.
116: * @param strategies The players' strategies.
117: * @param maxComputationTimePerMove The maximum computation time per move in seconds.
118: * @param moveChecker The move checker.
119: * @param observerFactoryProvider The {@link ObserverFactoryProvider}.
120: * @throws IllegalArgumentException if the player sets do not match.
121: * @throws InterruptedException if creating the game has been interrupted.
122: */
123: public DefaultGame(final int id, final S initialState, final Map<String, ST> strategies,
124: final long maxComputationTimePerMove, final MoveChecker<P, S, M> moveChecker,
125: final ObserverFactoryProvider observerFactoryProvider)
126: throws IllegalArgumentException, InterruptedException {
127:
128: this.id = id;
129: this.state = Objects.requireNonNull(initialState, "initialState").deepCopy();
130: this.strategies = new LinkedHashMap<>(Objects.requireNonNull(strategies, "players"));
131: this.maxComputationTimePerMove = maxComputationTimePerMove;
132: this.moveChecker = Objects.requireNonNull(moveChecker, "moveChecker");
133: this.executorService = Executors.newCachedThreadPool();
134: this.started = false;
135:
136:• if (!strategies.keySet().equals(this.state.getPlayers().keySet())) {
137: throw new IllegalArgumentException(
138: "The set of players defined by the game state must match the set of players "
139: + "associated with strategies.");
140: }
141:
142: this.observers = Collections.synchronizedList(
143: observerFactoryProvider.getObserverFactories().stream().map(ObserverFactory::createObserver)
144: .collect(Collectors.toList()));
145: this.checkAndAdjustPlayerStatesIfNecessary();
146: }
147:
148: /**
149: * Returns a string representing the state of the game.
150: */
151: protected final String gameToString() {
152: return String.format("state=%s, strategies=%s", this.state, this.strategies);
153: }
154:
155: @Override
156: public final int getId() {
157: return this.id;
158: }
159:
160: @Override
161: public final Map<String, P> getPlayers() {
162: return this.state.getPlayers();
163: }
164:
165: @Override
166: public final Map<String, ST> getStrategies() {
167: return this.strategies;
168: }
169:
170: @Override
171: public final S getState() {
172: return this.state.deepCopy();
173: }
174:
175: @Override
176: public final void addObserver(final Observer observer) {
177: this.observers.add(observer);
178: }
179:
180: @Override
181: public final void removeObserver(final Observer observer) {
182: this.observers.remove(observer);
183: }
184:
185: @Override
186: public final void start() throws InterruptedException {
187:• for (final ST strategy : this.strategies.values()) {
188: strategy.reset();
189: }
190: this.callObservers((final Observer o) -> o.started(this, this.state.deepCopy()));
191: this.started = true;
192: }
193:
194: /**
195: * Runs all observers.
196: *
197: * @param call Called with the observer as argument.
198: */
199: private void callObservers(final ConsumerE<Observer, InterruptedException> call) throws InterruptedException {
200: final ArrayList<Observer> copyOfObserverList = new ArrayList<>(this.observers);
201:• for (final Observer observer : copyOfObserverList) {
202: call.accept(observer);
203: }
204: }
205:
206: @Override
207: public final void makeMove() throws IllegalStateException, InterruptedException {
208:• if (!this.isStarted()) {
209: throw new IllegalStateException("Trying to make a move although the game has not been started yet.");
210: }
211:• if (this.isFinished()) {
212: throw new IllegalStateException("Trying to make a move although the game is already over.");
213: }
214:
215: final Set<P> nextPlayers = this.state.computeNextPlayers();
216: this.callObservers((final Observer o) -> o.nextPlayersComputed(this, this.state.deepCopy(), nextPlayers));
217:
218:• if (nextPlayers.isEmpty()) {
219: // no active players -> game over
220: this.callObservers((final Observer o) -> o.finished(this, this.state.deepCopy()));
221: return;
222: }
223:
224: final CompletionService<Optional<M>> completionService = new ExecutorCompletionService<>(this.executorService);
225: final Map<Future<Optional<M>>, P> futures = this.submitMoveComputingRequests(nextPlayers, completionService);
226: try {
227: this.applyNextPossibleMove(completionService, futures);
228: } finally {
229: this.state.nextTurn();
230: this.checkAndAdjustPlayerStatesIfNecessary();
231: }
232: }
233:
234: @Override
235: public void abortRequested() {
236:• for (final ST strategy : this.strategies.values()) {
237: strategy.abortRequested(this.id);
238: }
239: }
240:
241: @Override
242: public final boolean isStarted() {
243: return this.started;
244: }
245:
246: @Override
247: public final boolean isFinished() {
248: final List<P> playersPlaying = this.getPlayers().values().stream()
249: .filter((final P player) -> player.getState().equals(PlayerState.PLAYING)).collect(Collectors.toList());
250: return playersPlaying.isEmpty();
251: }
252:
253: @Override
254: public final void close() {
255: this.executorService.shutdown();
256: }
257:
258: /**
259: * Places a move choosing task for each active player.
260: *
261: * @param nextPlayers The active players.
262: * @param completionService The completion service.
263: * @return The futures receiving the computation results.
264: */
265: private Map<Future<Optional<M>>, P> submitMoveComputingRequests(final Set<P> nextPlayers,
266: final CompletionService<Optional<M>> completionService) {
267:
268: final Map<Future<Optional<M>>, P> futures = new LinkedHashMap<>(nextPlayers.size());
269:• for (final P nextPlayer : nextPlayers) {
270:• if (!this.strategies.containsKey(nextPlayer.getName())) {
271: throw new IllegalStateException(String.format("State computed unknown next player %s.", nextPlayer));
272: }
273:
274: final Strategy<P, S, M> strategy = this.strategies.get(nextPlayer.getName());
275: futures.put(
276: completionService
277: .submit(() -> strategy.computeNextMove(this.id, nextPlayer, this.state.deepCopy())),
278: nextPlayer);
279: }
280: return futures;
281: }
282:
283: /**
284: * Applies the next available move of the "fastest" player. If some move can be successfully applied,
285: * {@link Observer#legalMoveApplied(Game, Player, Move)} will be called, otherwise
286: * {@link Observer#illegalMoveRejected(Game, Player, Move, String)} will be invoked for each illegal move returned
287: * until a legal move has been applied. Pending moves or moves computed after a legal move of some player has been
288: * applied are discarded without generating an event.
289: *
290: * @param completionService The completion service.
291: * @param futures The futures receiving the computation results.
292: */
293: private void applyNextPossibleMove(final CompletionService<Optional<M>> completionService,
294: final Map<Future<Optional<M>>, P> futures) throws InterruptedException {
295:
296: Optional<P> playerDoingTheMove = Optional.empty();
297:
298: try {
299:• while (!futures.isEmpty()) {
300: playerDoingTheMove = this.tryToApplyNextPossibleMove(completionService, futures);
301:• if (playerDoingTheMove.isPresent()) {
302: return;
303: }
304: }
305: } finally {
306:• if (!futures.isEmpty()) {
307:• assert playerDoingTheMove.isPresent();
308:
309:• for (final Map.Entry<Future<Optional<M>>, P> entry : futures.entrySet()) {
310: final Future<Optional<M>> future = entry.getKey();
311: future.cancel(true);
312:
313: final P overtakingPlayer = playerDoingTheMove.orElseThrow();
314: final P overtakenPlayer = entry.getValue();
315: this.callObservers(
316: (final Observer o) -> o
317: .playerOvertaken(this, this.state.deepCopy(), overtakenPlayer, overtakingPlayer));
318: }
319: }
320: }
321: }
322:
323: /**
324: * Tries to apply the next available move of the "fastest" player. If some move can be successfully applied,
325: * {@link Observer#legalMoveApplied(Game, Player, Move)} will be called, otherwise
326: * {@link Observer#illegalMoveRejected(Game, Player, Move, String)} will be invoked for such an illegal move.
327: *
328: * @param completionService The completion service.
329: * @param futures The futures receiving the computation results.
330: * @return The player for whom a legal move has been applied successfully (if any). If no legal move could be
331: * applied, an empty Optional is returned.
332: */
333: private Optional<P> tryToApplyNextPossibleMove(final CompletionService<Optional<M>> completionService,
334: final Map<Future<Optional<M>>, P> futures) throws InterruptedException {
335:
336: final Future<Optional<M>> future = completionService.poll(this.maxComputationTimePerMove, TimeUnit.SECONDS);
337:• if (future == null) {
338: // no strategy succeeded in finding a legal move within the configured time window; choosing random moves
339:• for (final Map.Entry<Future<Optional<M>>, P> entry : futures.entrySet()) {
340: final P player = entry.getValue();
341: final Optional<M> chosenMove = this.chooseRandomMove(player, this.state.deepCopy());
342:• if (chosenMove.isEmpty()) {
343: // No move available; this can happen if a previously chosen random move has won the game.
344: // In this case, we let the following strategies unpunished and do nothing.
345: continue;
346: }
347:
348: this.handleOverdueMove(player, chosenMove);
349: try {
350: this.applyMoveIfPossible(player, chosenMove.get());
351: } catch (final GameException e) {
352: // the game itself did not succeed in finding a legal random move?!?
353: this.handleIllegalMove(player, chosenMove, e.getMessage());
354: }
355: }
356:
357: futures.clear();
358: return Optional.empty();
359: }
360:
361: final P playerDoingTheMove = futures.remove(future);
362: Optional<M> move = Optional.empty();
363: try {
364: move = this.determineNextAvailableMove(future);
365:• if (move == null) {
366: // strategy returned null which is not allowed, but we are going to condone it for now
367: move = Optional.empty();
368: }
369:• if (move.isPresent()) {
370: // check if strategy attempts to cheat by returning an unsupported custom move
371: this.checkMove(move.get());
372:
373: this.applyMoveIfPossible(playerDoingTheMove, move.get());
374: return Optional.of(playerDoingTheMove); // some legal move has been found and applied
375: } else {
376: // the player resigned the game
377: this.state.setPlayerState(playerDoingTheMove.getName(), PlayerState.RESIGNED);
378: final P player = playerDoingTheMove;
379: this.callObservers((final Observer o) -> o.playerResigned(this, this.state.deepCopy(), player));
380: }
381: } catch (final GameException e) {
382: // the strategy did not succeed in finding a legal move (or tried to cheat)
383: this.handleIllegalMove(playerDoingTheMove, move, e.getMessage());
384: }
385:
386: return Optional.empty();
387: }
388:
389: /**
390: * Handles an illegal move.
391: *
392: * @param player The player.
393: * @param move The move if present.
394: * @param reason The reason why the move is illegal.
395: */
396: private void handleIllegalMove(final P player, final Optional<M> move, final String reason)
397: throws InterruptedException {
398: this.state.setPlayerState(player.getName(), PlayerState.LOST);
399: final Optional<Move<?, ?>> moveTried = Optional.ofNullable(move.orElse(null));
400: this.callObservers(
401: (final Observer o) -> o.illegalMoveRejected(this, this.state.deepCopy(), player, moveTried, reason));
402: }
403:
404: /**
405: * Handles an overdue move.
406: *
407: * @param player The player.
408: * @param chosenMove The move that has been chosen.
409: */
410: private void handleOverdueMove(final P player, final Optional<M> chosenMove) throws InterruptedException {
411: final Optional<Move<?, ?>> moveChosen = Optional.ofNullable(chosenMove.orElse(null));
412: this.callObservers(
413: (final Observer o) -> o.overdueMoveRejected(this, this.state.deepCopy(), player, moveChosen));
414: }
415:
416: /**
417: * Checks if the move is supported.
418: *
419: * @param move The move to check.
420: * @throws GameException if the move is not supported.
421: */
422: private void checkMove(final M move) throws GameException {
423:• if (!this.moveChecker.check(move)) {
424: throw new GameException(String.format("Unsupported move: %s.", move));
425: }
426: }
427:
428: /**
429: * Returns the next available move from a {@link Future}.
430: *
431: * @param future The future.
432: * @return The next available move returned by the strategy.
433: * @throws GameException if the strategy caused an exception to be thrown.
434: */
435: private Optional<M> determineNextAvailableMove(final Future<Optional<M>> future)
436: throws GameException, InterruptedException {
437: try {
438: return future.get();
439: } catch (final ExecutionException e) {
440: final Throwable cause = e.getCause();
441: throw new GameException("The strategy did not succeed in finding a valid move: " + cause.getMessage(), e);
442: }
443: }
444:
445: /**
446: * Applies a move for a given player to the current game state if possible. If the move can be successfully applied,
447: * {@link Observer#legalMoveApplied(Game, Player, Move)} will be called, otherwise
448: * {@link Observer#illegalMoveRejected(Game, Player, Move, String)} will be invoked for each illegal move returned.
449: *
450: * @param player The current player.
451: * @param move The move to apply.
452: * @throws GameException if the move could not be applied to the current game state for some reason.
453: */
454: private void applyMoveIfPossible(final P player, final M move) throws GameException, InterruptedException {
455: final S newState = this.state.deepCopy();
456: move.applyTo(newState, player);
457: this.state = newState;
458: this.callObservers((final Observer o) -> o.legalMoveApplied(this, this.state.deepCopy(), player, move));
459: }
460:
461: /**
462: * Checks and adjusts the states of the players if necessary.
463: */
464: private void checkAndAdjustPlayerStatesIfNecessary() throws InterruptedException {
465: final Collection<P> players = this.getPlayers().values();
466: final List<P> playersPlaying = players.stream()
467: .filter((final P player) -> player.getState().equals(PlayerState.PLAYING)).collect(Collectors.toList());
468: final List<P> playersWon = players.stream()
469: .filter((final P player) -> player.getState().equals(PlayerState.WON)).collect(Collectors.toList());
470:
471: final boolean gameOver;
472:• if (playersPlaying.isEmpty()) {
473: // all players have stopped playing
474: gameOver = true;
475:• } else if (!playersWon.isEmpty()) {
476: // at least one player has won the game -- no time for losers (Queen)
477: playersPlaying.stream()
478: .forEach((final P player) -> this.state.setPlayerState(player.getName(), PlayerState.LOST));
479: gameOver = true;
480:• } else if (playersPlaying.size() == 1) {
481: // one player remains -- the winner takes them all (ABBA)
482: this.state.setPlayerState(playersPlaying.get(0).getName(), PlayerState.WON);
483: gameOver = true;
484: } else {
485: // there are at least two players participating at the game
486: gameOver = false;
487: }
488:
489:• if (gameOver) {
490: this.callObservers((final Observer o) -> o.finished(this, this.state.deepCopy()));
491: }
492: }
493: }