diff --git a/src/net/socialgamer/cah/CahModule.java b/src/net/socialgamer/cah/CahModule.java index fc89ab7..c9d46c5 100644 --- a/src/net/socialgamer/cah/CahModule.java +++ b/src/net/socialgamer/cah/CahModule.java @@ -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; } } diff --git a/src/net/socialgamer/cah/Server.java b/src/net/socialgamer/cah/Server.java index 10b7333..f41f7ff 100644 --- a/src/net/socialgamer/cah/Server.java +++ b/src/net/socialgamer/cah/Server.java @@ -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; + } } diff --git a/src/net/socialgamer/cah/StartupUtils.java b/src/net/socialgamer/cah/StartupUtils.java index 31882f3..7a053c1 100644 --- a/src/net/socialgamer/cah/StartupUtils.java +++ b/src/net/socialgamer/cah/StartupUtils.java @@ -43,6 +43,6 @@ public class StartupUtils extends GuiceServletContextListener { @Override protected Injector getInjector() { - return Guice.createInjector(); + return Guice.createInjector(new CahModule()); } } diff --git a/src/net/socialgamer/cah/UserPing.java b/src/net/socialgamer/cah/UserPing.java index 87cdcae..d1b6b82 100644 --- a/src/net/socialgamer/cah/UserPing.java +++ b/src/net/socialgamer/cah/UserPing.java @@ -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 diff --git a/src/net/socialgamer/cah/data/ConnectedUsers.java b/src/net/socialgamer/cah/data/ConnectedUsers.java index 43d3ef1..736a75a 100644 --- a/src/net/socialgamer/cah/data/ConnectedUsers.java +++ b/src/net/socialgamer/cah/data/ConnectedUsers.java @@ -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 { /** diff --git a/src/net/socialgamer/cah/data/Game.java b/src/net/socialgamer/cah/data/Game.java index 1b02d17..4bc7a67 100644 --- a/src/net/socialgamer/cah/data/Game.java +++ b/src/net/socialgamer/cah/data/Game.java @@ -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 playersToUsers() { final List users; synchronized (players) { diff --git a/src/net/socialgamer/cah/data/GameManager.java b/src/net/socialgamer/cah/data/GameManager.java new file mode 100644 index 0000000..a95526f --- /dev/null +++ b/src/net/socialgamer/cah/data/GameManager.java @@ -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 { + + private final int maxGames; + private final Map games = new TreeMap(); + private final Provider gameProvider; + /** + * Potential next game id. + */ + private int nextId = 0; + + @Inject + public GameManager(final Provider 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 getGames() { + return games; + } + + @BindingAnnotation + @Retention(RetentionPolicy.RUNTIME) + public @interface GameId { + } + + @BindingAnnotation + @Retention(RetentionPolicy.RUNTIME) + public @interface MaxGames { + } +} diff --git a/src/net/socialgamer/cah/handlers/ChatHandler.java b/src/net/socialgamer/cah/handlers/ChatHandler.java index 1bb1446..f67f370 100644 --- a/src/net/socialgamer/cah/handlers/ChatHandler.java +++ b/src/net/socialgamer/cah/handlers/ChatHandler.java @@ -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 diff --git a/src/net/socialgamer/cah/handlers/LogoutHandler.java b/src/net/socialgamer/cah/handlers/LogoutHandler.java index a36934e..9f8364a 100644 --- a/src/net/socialgamer/cah/handlers/LogoutHandler.java +++ b/src/net/socialgamer/cah/handlers/LogoutHandler.java @@ -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 diff --git a/src/net/socialgamer/cah/handlers/NamesHandler.java b/src/net/socialgamer/cah/handlers/NamesHandler.java index cfad0e1..fc0b2ae 100644 --- a/src/net/socialgamer/cah/handlers/NamesHandler.java +++ b/src/net/socialgamer/cah/handlers/NamesHandler.java @@ -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 diff --git a/src/net/socialgamer/cah/handlers/RegisterHandler.java b/src/net/socialgamer/cah/handlers/RegisterHandler.java index d574639..ba533a9 100644 --- a/src/net/socialgamer/cah/handlers/RegisterHandler.java +++ b/src/net/socialgamer/cah/handlers/RegisterHandler.java @@ -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 diff --git a/test/net/socialgamer/cah/data/GameManagerTest.java b/test/net/socialgamer/cah/data/GameManagerTest.java new file mode 100644 index 0000000..b6074fb --- /dev/null +++ b/test/net/socialgamer/cah/data/GameManagerTest.java @@ -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()); + } +} diff --git a/test/net/socialgamer/cah/data/GameTest.java b/test/net/socialgamer/cah/data/GameTest.java index 0396864..0176a9f 100644 --- a/test/net/socialgamer/cah/data/GameTest.java +++ b/test/net/socialgamer/cah/data/GameTest.java @@ -50,6 +50,5 @@ public class GameTest { assertEquals(null, game.getHost()); verify(cmMock); - } }