- add GameManager and test

- fix up Guice
This commit is contained in:
Andy Janata 2012-01-16 23:59:56 -08:00
parent e64978a0da
commit 565c17b338
13 changed files with 274 additions and 23 deletions

View File

@ -1,13 +1,23 @@
package net.socialgamer.cah;
import net.socialgamer.cah.data.GameManager;
import net.socialgamer.cah.data.GameManager.GameId;
import net.socialgamer.cah.data.GameManager.MaxGames;
import com.google.inject.AbstractModule;
import com.google.inject.Singleton;
import com.google.inject.Provides;
public class CahModule extends AbstractModule {
@Override
protected void configure() {
bind(Server.class).in(Singleton.class);
bind(Integer.class).annotatedWith(GameId.class).toProvider(GameManager.class);
}
@Provides
@MaxGames
Integer provideMaxGames() {
return 8;
}
}

View File

@ -1,20 +1,28 @@
package net.socialgamer.cah;
import net.socialgamer.cah.data.ConnectedUsers;
import net.socialgamer.cah.data.GameManager;
import com.google.inject.Inject;
import com.google.inject.Singleton;
@Singleton
public class Server {
private final ConnectedUsers users;
private final GameManager gameManager;
public Server() {
users = new ConnectedUsers();
@Inject
public Server(final ConnectedUsers connectedUsers, final GameManager gameManager) {
users = connectedUsers;
this.gameManager = gameManager;
}
// TODO figure out if I can just get this to inject directly
public ConnectedUsers getConnectedUsers() {
return this.users;
}
public GameManager getGames() {
return this.gameManager;
}
}

View File

@ -43,6 +43,6 @@ public class StartupUtils extends GuiceServletContextListener {
@Override
protected Injector getInjector() {
return Guice.createInjector();
return Guice.createInjector(new CahModule());
}
}

View File

@ -12,8 +12,8 @@ public class UserPing extends TimerTask {
private final ConnectedUsers users;
@Inject
public UserPing(final Server server) {
users = server.getConnectedUsers();
public UserPing(final ConnectedUsers users) {
this.users = users;
}
@Override

View File

@ -11,14 +11,16 @@ import net.socialgamer.cah.Constants.LongPollResponse;
import net.socialgamer.cah.Constants.ReturnableData;
import net.socialgamer.cah.data.QueuedMessage.MessageType;
import com.google.inject.Singleton;
/**
* Class that holds all users connected to the server, and provides functions to operate on said
* list.
*
* @author ajanata
*
*/
@Singleton
public class ConnectedUsers {
/**

View File

@ -7,8 +7,11 @@ import java.util.List;
import net.socialgamer.cah.Constants.LongPollResponse;
import net.socialgamer.cah.Constants.ReturnableData;
import net.socialgamer.cah.data.GameManager.GameId;
import net.socialgamer.cah.data.QueuedMessage.MessageType;
import com.google.inject.Inject;
public class Game {
private final int id;
@ -25,7 +28,8 @@ public class Game {
* @param id
* @param connectedUsers
*/
public Game(final int id, final ConnectedUsers connectedUsers) {
@Inject
public Game(@GameId final Integer id, final ConnectedUsers connectedUsers) {
this.id = id;
this.connectedUsers = connectedUsers;
}
@ -93,6 +97,10 @@ public class Game {
return playersToUsers();
}
public int getId() {
return id;
}
private List<User> playersToUsers() {
final List<User> users;
synchronized (players) {

View File

@ -0,0 +1,139 @@
package net.socialgamer.cah.data;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.Map;
import java.util.TreeMap;
import net.socialgamer.cah.data.GameManager.GameId;
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 ajanata
*/
@Singleton
@GameId
public class GameManager implements Provider<Integer> {
private final int maxGames;
private final Map<Integer, Game> games = new TreeMap<Integer, Game>();
private final Provider<Game> gameProvider;
/**
* Potential next game id.
*/
private int nextId = 0;
@Inject
public GameManager(final Provider<Game> gameProvider, @MaxGames final Integer maxGames) {
this.gameProvider = gameProvider;
this.maxGames = maxGames;
}
/**
* Creates a new game, if there are free game slots. Returns 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.
*/
public Game createGame() {
synchronized (games) {
if (games.size() >= maxGames) {
return null;
}
final Game game = gameProvider.get();
assert (game.getId() >= 0);
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.
*/
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;
}
// TODO remove the players from the game
}
}
/**
* 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 -1 if the maximum number of games are in progress.
*/
@Override
public Integer get() {
synchronized (games) {
if (games.size() >= maxGames) {
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);
}
private int candidateGameId(final int skip) {
synchronized (games) {
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;
}
}
Map<Integer, Game> getGames() {
return games;
}
@BindingAnnotation
@Retention(RetentionPolicy.RUNTIME)
public @interface GameId {
}
@BindingAnnotation
@Retention(RetentionPolicy.RUNTIME)
public @interface MaxGames {
}
}

View File

@ -13,7 +13,6 @@ import net.socialgamer.cah.Constants.LongPollResponse;
import net.socialgamer.cah.Constants.ReturnableData;
import net.socialgamer.cah.Constants.SessionAttribute;
import net.socialgamer.cah.RequestWrapper;
import net.socialgamer.cah.Server;
import net.socialgamer.cah.data.ConnectedUsers;
import net.socialgamer.cah.data.QueuedMessage.MessageType;
import net.socialgamer.cah.data.User;
@ -28,8 +27,8 @@ public class ChatHandler extends Handler {
private final ConnectedUsers users;
@Inject
public ChatHandler(final Server server) {
this.users = server.getConnectedUsers();
public ChatHandler(final ConnectedUsers users) {
this.users = users;
}
@Override

View File

@ -10,7 +10,6 @@ import net.socialgamer.cah.Constants.DisconnectReason;
import net.socialgamer.cah.Constants.ReturnableData;
import net.socialgamer.cah.Constants.SessionAttribute;
import net.socialgamer.cah.RequestWrapper;
import net.socialgamer.cah.Server;
import net.socialgamer.cah.data.ConnectedUsers;
import net.socialgamer.cah.data.User;
@ -24,8 +23,8 @@ public class LogoutHandler extends Handler {
private final ConnectedUsers users;
@Inject
public LogoutHandler(final Server server) {
this.users = server.getConnectedUsers();
public LogoutHandler(final ConnectedUsers users) {
this.users = users;
}
@Override

View File

@ -12,7 +12,6 @@ import net.socialgamer.cah.Constants.AjaxOperation;
import net.socialgamer.cah.Constants.AjaxResponse;
import net.socialgamer.cah.Constants.ReturnableData;
import net.socialgamer.cah.RequestWrapper;
import net.socialgamer.cah.Server;
import net.socialgamer.cah.data.ConnectedUsers;
import net.socialgamer.cah.data.User;
@ -26,8 +25,8 @@ public class NamesHandler extends Handler {
private final ConnectedUsers users;
@Inject
public NamesHandler(final Server server) {
this.users = server.getConnectedUsers();
public NamesHandler(final ConnectedUsers users) {
this.users = users;
}
@Override

View File

@ -13,7 +13,6 @@ import net.socialgamer.cah.Constants.ErrorCode;
import net.socialgamer.cah.Constants.ReturnableData;
import net.socialgamer.cah.Constants.SessionAttribute;
import net.socialgamer.cah.RequestWrapper;
import net.socialgamer.cah.Server;
import net.socialgamer.cah.data.ConnectedUsers;
import net.socialgamer.cah.data.User;
@ -35,8 +34,8 @@ public class RegisterHandler extends Handler {
* @param server
*/
@Inject
public RegisterHandler(final Server server) {
this.users = server.getConnectedUsers();
public RegisterHandler(final ConnectedUsers users) {
this.users = users;
}
@Override

View File

@ -0,0 +1,89 @@
package net.socialgamer.cah.data;
import static org.easymock.EasyMock.createMock;
import static org.easymock.EasyMock.replay;
import static org.easymock.EasyMock.verify;
import static org.junit.Assert.assertEquals;
import net.socialgamer.cah.data.GameManager.GameId;
import net.socialgamer.cah.data.GameManager.MaxGames;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import com.google.inject.AbstractModule;
import com.google.inject.Guice;
import com.google.inject.Injector;
import com.google.inject.Provides;
public class GameManagerTest {
private Injector injector;
private GameManager gameManager;
private ConnectedUsers cmMock;
private int gameId;
@Before
public void setUp() throws Exception {
injector = Guice.createInjector(new AbstractModule() {
@Override
protected void configure() {
// pass
}
@SuppressWarnings("unused")
@Provides
@MaxGames
Integer provideMaxGames() {
return 3;
}
@SuppressWarnings("unused")
@Provides
@GameId
Integer provideGameId() {
return gameId;
}
});
gameManager = injector.getInstance(GameManager.class);
cmMock = createMock(ConnectedUsers.class);
replay(cmMock);
}
@After
public void tearDown() {
verify(cmMock);
}
@Test
public void testGetAndDestroyGame() {
// fill it up with 3 games
assertEquals(0, gameManager.get().intValue());
gameManager.getGames().put(0, new Game(0, cmMock));
assertEquals(1, gameManager.get().intValue());
gameManager.getGames().put(1, new Game(1, cmMock));
assertEquals(2, gameManager.get().intValue());
gameManager.getGames().put(2, new Game(2, cmMock));
// make sure it says it can't make any more
assertEquals(-1, gameManager.get().intValue());
// remove game 1 using its own method -- this should be how it always happens in production
gameManager.destroyGame(1);
// make sure it re-uses that id
assertEquals(1, gameManager.get().intValue());
gameManager.getGames().put(1, new Game(1, cmMock));
assertEquals(-1, gameManager.get().intValue());
// remove game 1 out from under it, to make sure it'll fix itself
gameManager.getGames().remove(1);
assertEquals(1, gameManager.get().intValue());
gameManager.getGames().put(1, new Game(1, cmMock));
assertEquals(-1, gameManager.get().intValue());
gameManager.destroyGame(2);
gameManager.destroyGame(0);
assertEquals(2, gameManager.get().intValue());
}
}

View File

@ -50,6 +50,5 @@ public class GameTest {
assertEquals(null, game.getHost());
verify(cmMock);
}
}