Skip to content

Method: toString()

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