Skip to content

Method: getState()

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