package com.lyndir.omicron.api; import com.google.common.base.Preconditions; import com.google.common.collect.*; import com.lyndir.lhunath.opal.system.error.InternalInconsistencyException; import com.lyndir.lhunath.opal.system.logging.Logger; import com.lyndir.lhunath.opal.system.util.*; import com.lyndir.omicron.api.error.NotAuthenticatedException; import com.lyndir.omicron.api.view.PlayerGameInfo; import java.lang.reflect.*; import java.util.*; import java.util.stream.Stream; import javax.annotation.Nonnull; import javax.annotation.Nullable; public class GameController implements IGameController { @SuppressWarnings("UnusedDeclaration") private static final Logger logger = Logger.get( GameController.class ); private final Game game; private final Map<GameListener, Player> gameListeners = Collections.synchronizedMap( Maps.<GameListener, Player>newLinkedHashMap() ); GameController(final Game game) { this.game = game; for (final Player player : game.getPlayers()) player.getController().setGameController( this ); } @Override public int hashCode() { return game.hashCode(); } @Override public boolean equals(final Object obj) { return obj instanceof IGameController && Objects.equals( game, ((IGameController) obj).getGame() ); } @Override public Game getGame() { return game; } void addInternalGameListener(final GameListener gameListener) { gameListeners.put( gameListener, null ); } void addGameListeners(final Map<GameListener, Player> newGameListeners) { gameListeners.putAll( newGameListeners ); } @Override public void addGameListener(final GameListener gameListener) throws NotAuthenticatedException { gameListeners.put( gameListener, Security.currentPlayer() ); } /** * Retrieve information on a given player. * * @param player The player whose information is being requested. * * @return Information visible to the current player about the given player. */ @Override public PlayerGameInfo getPlayerGameInfo(final IPlayer player) throws NotAuthenticatedException { if (player.observableTiles().iterator().hasNext()) return PlayerGameInfo.discovered( player, player.getScore() ); return PlayerGameInfo.undiscovered( player ); } @Override public ImmutableCollection<PlayerGameInfo> listPlayerGameInfo() throws NotAuthenticatedException { ImmutableList.Builder<PlayerGameInfo> playerGameInfoBuilder = ImmutableList.builder(); for (final IPlayer player : game.getPlayers()) playerGameInfoBuilder.add( getPlayerGameInfo( player ) ); return playerGameInfoBuilder.build(); } /** * Indicate that the current player is ready with his turn. * * @return true if this action has caused a new turn to begin. */ @Override public boolean setReady() throws NotAuthenticatedException { return setReady( Player.cast( Security.currentPlayer() ) ); } /** * Indicate that the given player is ready with his turn. * * NOTE: The player must be key-less or be the currently authenticated player. * * @return true if this action has caused a new turn to begin. */ @SuppressWarnings("SynchronizationOnLocalVariableOrMethodParameter") boolean setReady(final Player player) throws NotAuthenticatedException { if (!player.isKeyLess()) Preconditions.checkState( player.equals( Security.currentPlayer() ), "Cannot set protected player ready: not authenticated. First authenticate using Security.authenticate()." ); boolean allReady = game.setReady( player ); fire().onPlayerReady( player ); if (allReady) Security.godRun( this::fireNewTurn ); return allReady; } private void start() { Preconditions.checkState( !game.isRunning(), "The game cannot be started: It is already running." ); game.setRunning( true ); fire().onGameStarted( game ); } void end(final VictoryConditionType victoryCondition, @Nullable final IPlayer victor) { Preconditions.checkState( game.isRunning(), "The game cannot end: It isn't running yet." ); game.setRunning( false ); fire().onGameEnded( game, victoryCondition.pub(), victor ); } private void fireNewTurn() { onNewTurn(); fire().onNewTurn( game.getTurns().getLast() ); } protected void onNewTurn() { game.newTurn(); if (!game.isRunning()) start(); for (final Player player : game.getPlayers()) Security.playerRun( player, () -> { player.getController().fireReset(); player.getController().fireNewTurn(); } ); } /** * Get a game listener proxy to call an event on that should be fired for all game listeners. */ GameListener fire() { return TypeUtils.newProxyInstance( GameListener.class, (proxy, method, args) -> { synchronized (gameListeners) { if (method.getDeclaringClass() == Object.class) return Void.TYPE; logger.dbg( "%s: %s", method.getName(), ObjectUtils.describe( args ) ); for (final Map.Entry<GameListener, Player> gameListenerEntry : gameListeners.entrySet()) { Player gameListenerOwner = gameListenerEntry.getValue(); if (gameListenerOwner == null) Security.godRun( newGameListenerJob( gameListenerEntry.getKey(), method, args ) ); else Security.playerRun( gameListenerOwner, newGameListenerJob( gameListenerEntry.getKey(), method, args ) ); } return Void.TYPE; } } ); } /** * Get a game listener proxy to call an event that should be fired for all game listeners that are either internal or registered by * players that pass the playerCondition. * * @param playerCondition The predicate that should hold true for all players eligible to receive the notification. */ GameListener fireIfPlayer(@Nonnull final PredicateNN<IPlayer> playerCondition) { return TypeUtils.newProxyInstance( GameListener.class, (proxy, method, args) -> { synchronized (gameListeners) { if (method.getDeclaringClass() == Object.class) return Void.TYPE; logger.dbg( "%s: %s", method.getName(), ObjectUtils.describe( args ) ); for (final Map.Entry<GameListener, Player> gameListenerEntry : gameListeners.entrySet()) { Player gameListenerOwner = gameListenerEntry.getValue(); if (gameListenerOwner == null) Security.godRun( newGameListenerJob( gameListenerEntry.getKey(), method, args ) ); else if (playerCondition.apply( gameListenerOwner )) Security.playerRun( gameListenerOwner, newGameListenerJob( gameListenerEntry.getKey(), method, args ) ); } return Void.TYPE; } } ); } /** * Get a game listener proxy to call an event on that should be fired for all game listeners that are either internal or registered by * players that can observe the given location. * * @param location The location that should be observable. */ GameListener fireIfObservable(@Nonnull final ITile location) { return fireIfPlayer( player -> Security.godRun( () -> player.canObserve( location ).isTrue() ) ); } /** * Get a game listener proxy to call an event on that should be fired for all game listeners that are either internal or registered by * players that can observe the given object. * * @param gameObject The game object that should be observable. */ GameListener fireIfObservable(@Nonnull final IGameObject gameObject) { return fireIfPlayer( player -> Security.godRun( () -> player.canObserve( gameObject ).isTrue() ) ); } private static Runnable newGameListenerJob(final GameListener gameListener, final Method method, final Object[] args) { return () -> { try { //noinspection unchecked if (method.getDeclaringClass() == GameListener.class) method.invoke( gameListener, args ); } catch (IllegalAccessException | InvocationTargetException e) { throw new InternalInconsistencyException( "Fix: " + gameListener, e ); } }; } }