/** * Copyright (c) 2012, Andy Janata * All rights reserved. * * Redistribution and use in source and binary forms, with or without modification, are permitted * provided that the following conditions are met: * * * Redistributions of source code must retain the above copyright notice, this list of conditions * and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright notice, this list of * conditions and the following disclaimer in the documentation and/or other materials provided * with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY * WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ package net.socialgamer.cah.data; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Map; import java.util.TreeMap; import net.socialgamer.cah.data.Game.TooManyPlayersException; import net.socialgamer.cah.data.GameManager.GameId; import net.socialgamer.cah.task.BroadcastGameListUpdateTask; import org.apache.log4j.Logger; import com.google.inject.BindingAnnotation; import com.google.inject.Inject; import com.google.inject.Provider; import com.google.inject.Singleton; /** * Manage games for the server. * * This is also a Guice provider for game ids. * * @author Andy Janata (ajanata@socialgamer.net) */ @Singleton @GameId public class GameManager implements Provider<Integer> { private static final Logger logger = Logger.getLogger(GameManager.class); private final Provider<Integer> maxGamesProvider; private final Map<Integer, Game> games = new TreeMap<Integer, Game>(); private final Provider<Game> gameProvider; private final BroadcastGameListUpdateTask broadcastUpdate; /** * Potential next game id. */ private int nextId = 0; /** * Create a new game manager. * * @param gameProvider * Provider for new {@code Game} instances. * @param maxGamesProvider * Provider for maximum number of games allowed on the server. * @param users * Connected user manager. */ @Inject public GameManager(final Provider<Game> gameProvider, @MaxGames final Provider<Integer> maxGamesProvider, final BroadcastGameListUpdateTask broadcastUpdate) { this.gameProvider = gameProvider; this.maxGamesProvider = maxGamesProvider; this.broadcastUpdate = broadcastUpdate; } private int getMaxGames() { return maxGamesProvider.get(); } /** * Creates a new game, if there are free game slots. Returns {@code null} if there are already the * maximum number of games in progress. * * @return Newly created game, or {@code null} if the maximum number of games are in progress. */ private Game createGame() { synchronized (games) { if (games.size() >= getMaxGames()) { return null; } final Game game = gameProvider.get(); if (game.getId() < 0) { return null; } games.put(game.getId(), game); return game; } } /** * Creates a new game and puts the specified user into the game, if there are free game slots. * Returns {@code null} if there are already the maximum number of games in progress. * * Creating the game and adding the user are done atomically with respect to another game getting * created, or even getting the list of active games. It is impossible for another user to join * the game before the requesting user. * * @param user * User to place into the game. * @return Newly created game, or {@code null} if the maximum number of games are in progress. * @throws IllegalStateException * If the user is already in a game and cannot join another. */ public Game createGameWithPlayer(final User user) throws IllegalStateException { synchronized (games) { final Game game = createGame(); if (game == null) { return null; } try { game.addPlayer(user); logger.info(String.format("Created new game %d by user %s.", game.getId(), user.toString())); } catch (final IllegalStateException ise) { destroyGame(game.getId()); throw ise; } catch (final TooManyPlayersException tmpe) { // this should never happen -- we just made the game throw new Error("Impossible exception: Too many players in new game.", tmpe); } broadcastGameListRefresh(); return game; } } /** * This probably will not be used very often in the server: Games should normally be deleted when * all players leave it. I'm putting this in if only to help with testing. * * Destroys a game immediately. This will almost certainly cause errors on the client for any * players left in the game. If {@code gameId} isn't valid, this method silently returns. * * @param gameId * ID of game to destroy. */ public void destroyGame(final int gameId) { synchronized (games) { final Game game = games.remove(gameId); if (game == null) { return; } // if the prospective next id isn't valid, set it to the id we just removed if (nextId == -1 || games.containsKey(nextId)) { nextId = gameId; } // remove the players from the game final List<User> usersToRemove = game.getUsers(); for (final User user : usersToRemove) { game.removePlayer(user); game.removeSpectator(user); } logger.info(String.format("Destroyed game %d.", game.getId())); broadcastGameListRefresh(); } } /** * Broadcast an event to all users that they should refresh the game list. */ public void broadcastGameListRefresh() { broadcastUpdate.needsUpdate(); } /** * Get an unused game ID, or -1 if the maximum number of games are in progress. This should not be * called in such a case, though! * * TODO: make this not suck * * @return Next game id, or {@code -1} if the maximum number of games are in progress. */ @Override public Integer get() { synchronized (games) { if (games.size() >= getMaxGames()) { return -1; } if (!games.containsKey(nextId) && nextId >= 0) { final int ret = nextId; nextId = candidateGameId(ret); return ret; } else { final int ret = candidateGameId(); nextId = candidateGameId(ret); return ret; } } } private int candidateGameId() { return candidateGameId(-1); } /** * Try to guess a good candidate for the next game id. * * @param skip * An id to skip over. * @return A guess for the next game id. */ private int candidateGameId(final int skip) { synchronized (games) { final int maxGames = getMaxGames(); if (games.size() >= maxGames) { return -1; } for (int i = 0; i < maxGames; i++) { if (i == skip) { continue; } if (!games.containsKey(i)) { return i; } } return -1; } } /** * @return A copy of the list of all current games. */ public Collection<Game> getGameList() { synchronized (games) { // return a copy return new ArrayList<Game>(games.values()); } } /** * Gets the game with the specified id, or {@code null} if there is no game with that id. * * @param id * Id of game to retrieve. * @return The Game, or {@code null} if there is no game with that id. */ public Game getGame(final int id) { synchronized (games) { return games.get(id); } } // @VisibileForTesting Map<Integer, Game> getGames() { return games; } @BindingAnnotation @Retention(RetentionPolicy.RUNTIME) public @interface GameId { } @BindingAnnotation @Retention(RetentionPolicy.RUNTIME) public @interface MaxGames { } }