From 5bda9fd452c9c8a0d576e7a98b35f63c949153d7 Mon Sep 17 00:00:00 2001 From: Andy Janata Date: Wed, 18 Jan 2012 16:42:18 -0800 Subject: [PATCH] client can join games. doesn't load the game yet, which is the same thing that happens with creating a game. the same thing should happen for both cases. reworked some tests to be able to hide a method from GameManager. part of this goal was to reduce unnecessary game refresh broadcasts if a client tried to create multiple games. --- WebContent/js/cah.ajax.builder.js | 11 +++ WebContent/js/cah.constants.js | 14 +++- WebContent/js/cah.gamelist.js | 16 +++- src/net/socialgamer/cah/Constants.java | 7 +- src/net/socialgamer/cah/data/Game.java | 26 ++++++- src/net/socialgamer/cah/data/GameManager.java | 23 +++++- src/net/socialgamer/cah/data/Player.java | 2 + src/net/socialgamer/cah/data/User.java | 2 +- .../cah/handlers/CreateGameHandler.java | 2 +- .../socialgamer/cah/handlers/Handlers.java | 1 + .../cah/handlers/JoinGameHandler.java | 77 +++++++++++++++++++ .../socialgamer/cah/data/GameManagerTest.java | 58 ++++++++++---- test/net/socialgamer/cah/data/GameTest.java | 22 ++++-- 13 files changed, 227 insertions(+), 34 deletions(-) create mode 100644 src/net/socialgamer/cah/handlers/JoinGameHandler.java diff --git a/WebContent/js/cah.ajax.builder.js b/WebContent/js/cah.ajax.builder.js index 8bd589b..6e9d67c 100644 --- a/WebContent/js/cah.ajax.builder.js +++ b/WebContent/js/cah.ajax.builder.js @@ -92,6 +92,17 @@ cah.ajax.Builder.prototype.withMessage = function(message) { return this; }; +/** + * @param {number} + * gameId Game id field to use in the request. + * @returns {cah.ajax.Builder} This object. + */ +cah.ajax.Builder.prototype.withGameId = function(gameId) { + this.assertNotExecuted(); + this.data[cah.$.AjaxRequest.GAME_ID] = gameId; + return this; +}; + cah.ajax.Builder.prototype.assertNotExecuted = function() { if (this.run_) { throw "Request already executed."; diff --git a/WebContent/js/cah.constants.js b/WebContent/js/cah.constants.js index 77e9e79..62cd719 100644 --- a/WebContent/js/cah.constants.js +++ b/WebContent/js/cah.constants.js @@ -9,6 +9,7 @@ cah.$.AjaxOperation.prototype.dummy = undefined; cah.$.AjaxOperation.FIRST_LOAD = "firstload"; cah.$.AjaxOperation.LOG_OUT = "logout"; cah.$.AjaxOperation.GAME_LIST = "games"; +cah.$.AjaxOperation.JOIN_GAME = "join_game"; cah.$.AjaxOperation.REGISTER = "register"; cah.$.AjaxOperation.CREATE_GAME = "create_game"; cah.$.AjaxOperation.CHAT = "chat"; @@ -19,6 +20,7 @@ cah.$.AjaxRequest = function() { }; cah.$.AjaxRequest.prototype.dummy = undefined; cah.$.AjaxRequest.MESSAGE = "message"; +cah.$.AjaxRequest.GAME_ID = "game_id"; cah.$.AjaxRequest.SERIAL = "serial"; cah.$.AjaxRequest.OP = "op"; cah.$.AjaxRequest.NICKNAME = "nickname"; @@ -53,28 +55,34 @@ cah.$.ErrorCode.prototype.dummy = undefined; cah.$.ErrorCode.TOO_MANY_GAMES = "too_many_games"; cah.$.ErrorCode.INVALID_NICK = "invalid_nick"; cah.$.ErrorCode.BAD_REQUEST = "bad_req"; +cah.$.ErrorCode.CANNOT_JOIN_ANOTHER_GAME = "cannot_join_another_game"; +cah.$.ErrorCode.NO_GAME_SPECIFIED = "no_game_spec"; cah.$.ErrorCode.SESSION_EXPIRED = "session_expired"; cah.$.ErrorCode.MESSAGE_TOO_LONG = "msg_too_long"; cah.$.ErrorCode.BAD_OP = "bad_op"; cah.$.ErrorCode.NO_SESSION = "no_session"; cah.$.ErrorCode.NOT_REGISTERED = "not_registered"; -cah.$.ErrorCode.CANNOT_JOIN_GAME = "cannot_join_game"; +cah.$.ErrorCode.INVALID_GAME = "invalid_game"; cah.$.ErrorCode.OP_NOT_SPECIFIED = "op_not_spec"; cah.$.ErrorCode.NO_MSG_SPECIFIED = "no_msg_spec"; cah.$.ErrorCode.NICK_IN_USE = "nick_in_use"; +cah.$.ErrorCode.GAME_FULL = "game_full"; cah.$.ErrorCode.NO_NICK_SPECIFIED = "no_nick_spec"; cah.$.ErrorCode_msg = {}; cah.$.ErrorCode_msg['bad_op'] = "Invalid operation."; cah.$.ErrorCode_msg['not_registered'] = "Not registered. Refresh the page."; cah.$.ErrorCode_msg['msg_too_long'] = "Messages cannot be longer than 200 characters."; -cah.$.ErrorCode_msg['cannot_join_game'] = "You cannot join another game."; cah.$.ErrorCode_msg['session_expired'] = "Your session has expired. Refresh the page."; +cah.$.ErrorCode_msg['invalid_game'] = "Invalid game specified."; +cah.$.ErrorCode_msg['no_game_spec'] = "No game specified."; +cah.$.ErrorCode_msg['game_full'] = "That game is full. Join another."; cah.$.ErrorCode_msg['invalid_nick'] = "Nickname must contain only upper and lower case letters, numbers, or underscores, must be 3 to 30 characters long, and must not start with a number."; +cah.$.ErrorCode_msg['too_many_games'] = "There are too many games already in progress. Either join an existing game, or wait for one to become available."; cah.$.ErrorCode_msg['no_nick_spec'] = "No nickname specified."; cah.$.ErrorCode_msg['no_session'] = "Session not detected. Make sure you have cookies enabled."; -cah.$.ErrorCode_msg['too_many_games'] = "There are too many games already in progress. Either join an existing game, or wait for one to become available."; cah.$.ErrorCode_msg['nick_in_use'] = "Nickname is already in use."; cah.$.ErrorCode_msg['bad_req'] = "Bad request."; +cah.$.ErrorCode_msg['cannot_join_another_game'] = "You cannot join another game."; cah.$.ErrorCode_msg['op_not_spec'] = "Operation not specified."; cah.$.ErrorCode_msg['no_msg_spec'] = "No message specified."; diff --git a/WebContent/js/cah.gamelist.js b/WebContent/js/cah.gamelist.js index 302838a..c6a9f30 100644 --- a/WebContent/js/cah.gamelist.js +++ b/WebContent/js/cah.gamelist.js @@ -85,8 +85,22 @@ cah.GameList.prototype.refreshGames = function() { * @constructor */ cah.GameListLobby = function(parentElem, data) { + /** + * The game id represented by this lobby. + * + * @type {number} + * @private + */ this.id_ = data[cah.$.GameInfo.ID]; + + /** + * This game lobby's dom element. + * + * @type {HTMLDivElement} + * @private + */ this.element_ = $("#gamelist_lobby_template").clone()[0]; + this.element_.id = "gamelist_lobby_" + this.id_; parentElem.appendChild(this.element_); $("#gamelist_lobby_" + this.id_ + " .gamelist_lobby_id").text(this.id_); @@ -114,5 +128,5 @@ cah.GameListLobby = function(parentElem, data) { }; cah.GameListLobby.prototype.joinClick = function(e) { - debugger; + cah.Ajax.build(cah.$.AjaxOperation.JOIN_GAME).withGameId(this.id_).run(); }; diff --git a/src/net/socialgamer/cah/Constants.java b/src/net/socialgamer/cah/Constants.java index b0a4442..4c17d9f 100644 --- a/src/net/socialgamer/cah/Constants.java +++ b/src/net/socialgamer/cah/Constants.java @@ -30,6 +30,7 @@ public class Constants { CREATE_GAME("create_game"), FIRST_LOAD("firstload"), GAME_LIST("games"), + JOIN_GAME("join_game"), LOG_OUT("logout"), NAMES("names"), REGISTER("register"); @@ -47,6 +48,7 @@ public class Constants { } public enum AjaxRequest { + GAME_ID("game_id"), MESSAGE("message"), NICKNAME("nickname"), OP("op"), @@ -91,12 +93,15 @@ public class Constants { public enum ErrorCode implements Localizable { BAD_OP("bad_op", "Invalid operation."), BAD_REQUEST("bad_req", "Bad request."), - CANNOT_JOIN_GAME("cannot_join_game", "You cannot join another game."), + CANNOT_JOIN_ANOTHER_GAME("cannot_join_another_game", "You cannot join another game."), + GAME_FULL("game_full", "That game is full. Join another."), + INVALID_GAME("invalid_game", "Invalid game specified."), INVALID_NICK("invalid_nick", "Nickname must contain only upper and lower case letters, " + "numbers, or underscores, must be 3 to 30 characters long, and must not start with a " + "number."), MESSAGE_TOO_LONG("msg_too_long", "Messages cannot be longer than 200 characters."), NICK_IN_USE("nick_in_use", "Nickname is already in use."), + NO_GAME_SPECIFIED("no_game_spec", "No game specified."), NO_MSG_SPECIFIED("no_msg_spec", "No message specified."), NO_NICK_SPECIFIED("no_nick_spec", "No nickname specified."), NO_SESSION("no_session", "Session not detected. Make sure you have cookies enabled."), diff --git a/src/net/socialgamer/cah/data/Game.java b/src/net/socialgamer/cah/data/Game.java index e7b9ca7..7b1ec21 100644 --- a/src/net/socialgamer/cah/data/Game.java +++ b/src/net/socialgamer/cah/data/Game.java @@ -25,6 +25,8 @@ public class Game { private BlackDeck blackDeck; private WhiteDeck whiteDeck; private GameState state; + // TODO make this host-configurable + private final int maxPlayers = 10; /** * TODO Injection here would be much nicer, but that would need a Provider for the id... Too much @@ -43,14 +45,29 @@ public class Game { state = GameState.LOBBY; } - public void addPlayer(final User user) { - final Player player = new Player(user); + /** + * Add a player to the game. + * + * @param user + * Player to add to this game. + * @throws TooManyPlayersException + * Thrown if this game is at its maximum player capacity. + * @throws IllegalStateException + * Thrown if the user is already in a game. + */ + public void addPlayer(final User user) throws TooManyPlayersException, IllegalStateException { synchronized (players) { + if (maxPlayers >= 3 && players.size() >= maxPlayers) { + throw new TooManyPlayersException(); + } + // this will throw IllegalStateException if the user is already in a game, including this one. + user.joinGame(this); + final Player player = new Player(user); players.add(player); if (host == null) { host = player; } - user.joinGame(this); + } final HashMap data = new HashMap(); @@ -146,4 +163,7 @@ public class Game { } return users; } + + public class TooManyPlayersException extends Exception { + } } diff --git a/src/net/socialgamer/cah/data/GameManager.java b/src/net/socialgamer/cah/data/GameManager.java index 9b16051..284f3bb 100644 --- a/src/net/socialgamer/cah/data/GameManager.java +++ b/src/net/socialgamer/cah/data/GameManager.java @@ -11,6 +11,7 @@ import java.util.TreeMap; import net.socialgamer.cah.Constants.LongPollEvent; import net.socialgamer.cah.Constants.LongPollResponse; import net.socialgamer.cah.Constants.ReturnableData; +import net.socialgamer.cah.data.Game.TooManyPlayersException; import net.socialgamer.cah.data.GameManager.GameId; import net.socialgamer.cah.data.QueuedMessage.MessageType; @@ -54,7 +55,7 @@ public class GameManager implements Provider { * * @return Newly created game, or {@code null} if the maximum number of games are in progress. */ - public Game createGame() { + private Game createGame() { synchronized (games) { if (games.size() >= maxGames) { return null; @@ -64,7 +65,6 @@ public class GameManager implements Provider { return null; } games.put(game.getId(), game); - broadcastGameListRefresh(); return game; } } @@ -94,7 +94,11 @@ public class GameManager implements Provider { } 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; } } @@ -122,7 +126,7 @@ public class GameManager implements Provider { } } - private void broadcastGameListRefresh() { + public void broadcastGameListRefresh() { final HashMap broadcastData = new HashMap(); broadcastData.put(LongPollResponse.EVENT, LongPollEvent.GAME_REFRESH.toString()); users.broadcastToAll(MessageType.GAME_EVENT, broadcastData); @@ -182,6 +186,19 @@ public class GameManager implements Provider { } } + /** + * 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); + } + } + Map getGames() { return games; } diff --git a/src/net/socialgamer/cah/data/Player.java b/src/net/socialgamer/cah/data/Player.java index e129baf..495a505 100644 --- a/src/net/socialgamer/cah/data/Player.java +++ b/src/net/socialgamer/cah/data/Player.java @@ -3,6 +3,8 @@ package net.socialgamer.cah.data; public class Player { private final User user; + // TODO add their hand, etc. + public Player(final User user) { this.user = user; } diff --git a/src/net/socialgamer/cah/data/User.java b/src/net/socialgamer/cah/data/User.java index b01b77c..d8f5c39 100644 --- a/src/net/socialgamer/cah/data/User.java +++ b/src/net/socialgamer/cah/data/User.java @@ -133,7 +133,7 @@ public class User { * Thrown if this user is already in another game. */ void joinGame(final Game game) throws IllegalStateException { - if (currentGame != null && currentGame != game) { + if (currentGame != null) { throw new IllegalStateException("User is already in a game."); } currentGame = game; diff --git a/src/net/socialgamer/cah/handlers/CreateGameHandler.java b/src/net/socialgamer/cah/handlers/CreateGameHandler.java index 8c52841..441dd2e 100644 --- a/src/net/socialgamer/cah/handlers/CreateGameHandler.java +++ b/src/net/socialgamer/cah/handlers/CreateGameHandler.java @@ -44,7 +44,7 @@ public class CreateGameHandler extends Handler { try { game = gameManager.createGameWithPlayer(user); } catch (final IllegalStateException ise) { - return error(ErrorCode.CANNOT_JOIN_GAME); + return error(ErrorCode.CANNOT_JOIN_ANOTHER_GAME); } if (game == null) { return error(ErrorCode.TOO_MANY_GAMES); diff --git a/src/net/socialgamer/cah/handlers/Handlers.java b/src/net/socialgamer/cah/handlers/Handlers.java index a4f32bf..e6d2588 100644 --- a/src/net/socialgamer/cah/handlers/Handlers.java +++ b/src/net/socialgamer/cah/handlers/Handlers.java @@ -14,6 +14,7 @@ public class Handlers { LIST.put(CreateGameHandler.OP, CreateGameHandler.class); LIST.put(FirstLoadHandler.OP, FirstLoadHandler.class); LIST.put(GameListHandler.OP, GameListHandler.class); + LIST.put(JoinGameHandler.OP, JoinGameHandler.class); LIST.put(LogoutHandler.OP, LogoutHandler.class); LIST.put(NamesHandler.OP, NamesHandler.class); LIST.put(RegisterHandler.OP, RegisterHandler.class); diff --git a/src/net/socialgamer/cah/handlers/JoinGameHandler.java b/src/net/socialgamer/cah/handlers/JoinGameHandler.java new file mode 100644 index 0000000..a695b92 --- /dev/null +++ b/src/net/socialgamer/cah/handlers/JoinGameHandler.java @@ -0,0 +1,77 @@ +package net.socialgamer.cah.handlers; + +import java.util.HashMap; +import java.util.Map; + +import javax.servlet.http.HttpSession; + +import net.socialgamer.cah.Constants.AjaxOperation; +import net.socialgamer.cah.Constants.AjaxRequest; +import net.socialgamer.cah.Constants.AjaxResponse; +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.data.Game; +import net.socialgamer.cah.data.Game.TooManyPlayersException; +import net.socialgamer.cah.data.GameManager; +import net.socialgamer.cah.data.User; + +import com.google.inject.Inject; + + +public class JoinGameHandler extends Handler { + + public static final String OP = AjaxOperation.JOIN_GAME.toString(); + + private final GameManager gameManager; + + @Inject + public JoinGameHandler(final GameManager gameManager) { + this.gameManager = gameManager; + } + + @Override + public Map handle(final RequestWrapper request, + final HttpSession session) { + final Map data = new HashMap(); + + final User user = (User) session.getAttribute(SessionAttribute.USER); + assert (user != null); + + final int gameId; + + if (request.getParameter(AjaxRequest.GAME_ID) == null) { + return error(ErrorCode.NO_GAME_SPECIFIED); + } + try { + gameId = Integer.parseInt(request.getParameter(AjaxRequest.GAME_ID)); + } catch (final NumberFormatException nfe) { + return error(ErrorCode.INVALID_GAME); + } + + final Game game = gameManager.getGame(gameId); + if (game == null) { + return error(ErrorCode.INVALID_GAME); + } + + assert game.getId() == gameId : "Got a game with id not what we asked for."; + + try { + game.addPlayer(user); + } catch (final IllegalStateException e) { + return error(ErrorCode.CANNOT_JOIN_ANOTHER_GAME); + } catch (final TooManyPlayersException e) { + return error(ErrorCode.GAME_FULL); + } + + // return the game id as a positive result to the client, which will then make another request + // to actually get game data + data.put(AjaxResponse.GAME_ID, game.getId()); + + gameManager.broadcastGameListRefresh(); + + return data; + } + +} diff --git a/test/net/socialgamer/cah/data/GameManagerTest.java b/test/net/socialgamer/cah/data/GameManagerTest.java index 9d617a9..3577a1a 100644 --- a/test/net/socialgamer/cah/data/GameManagerTest.java +++ b/test/net/socialgamer/cah/data/GameManagerTest.java @@ -1,13 +1,21 @@ package net.socialgamer.cah.data; +import static org.easymock.EasyMock.anyObject; import static org.easymock.EasyMock.createMock; +import static org.easymock.EasyMock.eq; +import static org.easymock.EasyMock.expectLastCall; import static org.easymock.EasyMock.replay; import static org.easymock.EasyMock.verify; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; + +import java.util.Collection; +import java.util.HashMap; + import net.socialgamer.cah.data.GameManager.GameId; import net.socialgamer.cah.data.GameManager.MaxGames; +import net.socialgamer.cah.data.QueuedMessage.MessageType; import org.junit.After; import org.junit.Before; @@ -23,15 +31,19 @@ public class GameManagerTest { private Injector injector; private GameManager gameManager; - private ConnectedUsers cmMock; + private ConnectedUsers cuMock; + private User userMock; private int gameId; @Before public void setUp() throws Exception { + cuMock = createMock(ConnectedUsers.class); + userMock = createMock(User.class); + injector = Guice.createInjector(new AbstractModule() { @Override protected void configure() { - // pass + bind(ConnectedUsers.class).toInstance(cuMock); } @SuppressWarnings("unused") @@ -50,24 +62,28 @@ public class GameManagerTest { }); gameManager = injector.getInstance(GameManager.class); - cmMock = createMock(ConnectedUsers.class); - replay(cmMock); } @After public void tearDown() { - verify(cmMock); + verify(cuMock); + verify(userMock); } @Test public void testGetAndDestroyGame() { + cuMock.broadcastToAll(eq(MessageType.GAME_EVENT), anyObject(HashMap.class)); + expectLastCall().times(3); + replay(cuMock); + replay(userMock); + // fill it up with 3 games assertEquals(0, gameManager.get().intValue()); - gameManager.getGames().put(0, new Game(0, cmMock)); + gameManager.getGames().put(0, new Game(0, cuMock, gameManager)); assertEquals(1, gameManager.get().intValue()); - gameManager.getGames().put(1, new Game(1, cmMock)); + gameManager.getGames().put(1, new Game(1, cuMock, gameManager)); assertEquals(2, gameManager.get().intValue()); - gameManager.getGames().put(2, new Game(2, cmMock)); + gameManager.getGames().put(2, new Game(2, cuMock, gameManager)); // make sure it says it can't make any more assertEquals(-1, gameManager.get().intValue()); @@ -75,13 +91,13 @@ public class GameManagerTest { gameManager.destroyGame(1); // make sure it re-uses that id assertEquals(1, gameManager.get().intValue()); - gameManager.getGames().put(1, new Game(1, cmMock)); + gameManager.getGames().put(1, new Game(1, cuMock, gameManager)); 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)); + gameManager.getGames().put(1, new Game(1, cuMock, gameManager)); assertEquals(-1, gameManager.get().intValue()); gameManager.destroyGame(2); @@ -89,18 +105,32 @@ public class GameManagerTest { assertEquals(2, gameManager.get().intValue()); } + @SuppressWarnings("unchecked") @Test public void testCreateGame() { - Game game = gameManager.createGame(); + cuMock.broadcastToAll(eq(MessageType.GAME_EVENT), anyObject(HashMap.class)); + expectLastCall().times(3); + cuMock.broadcastToList(anyObject(Collection.class), eq(MessageType.GAME_PLAYER_EVENT), + anyObject(HashMap.class)); + expectLastCall().times(3); + replay(cuMock); + + userMock.joinGame(anyObject(Game.class)); + expectLastCall().times(3); + userMock.getNickname(); + expectLastCall().andReturn("test").times(3); + replay(userMock); + + Game game = gameManager.createGameWithPlayer(userMock); assertNotNull(game); gameId = 1; - game = gameManager.createGame(); + game = gameManager.createGameWithPlayer(userMock); assertNotNull(game); gameId = 2; - game = gameManager.createGame(); + game = gameManager.createGameWithPlayer(userMock); assertNotNull(game); gameId = -1; - game = gameManager.createGame(); + game = gameManager.createGameWithPlayer(userMock); assertNull(game); } } diff --git a/test/net/socialgamer/cah/data/GameTest.java b/test/net/socialgamer/cah/data/GameTest.java index 0176a9f..7d04cda 100644 --- a/test/net/socialgamer/cah/data/GameTest.java +++ b/test/net/socialgamer/cah/data/GameTest.java @@ -1,5 +1,6 @@ package net.socialgamer.cah.data; +import static org.easymock.EasyMock.anyInt; import static org.easymock.EasyMock.anyObject; import static org.easymock.EasyMock.createMock; import static org.easymock.EasyMock.eq; @@ -13,6 +14,7 @@ import static org.junit.Assert.assertTrue; import java.util.Collection; import java.util.HashMap; +import net.socialgamer.cah.data.Game.TooManyPlayersException; import net.socialgamer.cah.data.QueuedMessage.MessageType; import org.junit.Before; @@ -22,21 +24,26 @@ import org.junit.Test; public class GameTest { private Game game; - private ConnectedUsers cmMock; + private ConnectedUsers cuMock; + private GameManager gmMock; @Before public void setUp() throws Exception { - cmMock = createMock(ConnectedUsers.class); - game = new Game(0, cmMock); + cuMock = createMock(ConnectedUsers.class); + gmMock = createMock(GameManager.class); + game = new Game(0, cuMock, gmMock); } @SuppressWarnings("unchecked") @Test - public void testRemovePlayer() { - cmMock.broadcastToList(anyObject(Collection.class), eq(MessageType.GAME_PLAYER_EVENT), + public void testRemovePlayer() throws IllegalStateException, TooManyPlayersException { + cuMock.broadcastToList(anyObject(Collection.class), eq(MessageType.GAME_PLAYER_EVENT), anyObject(HashMap.class)); expectLastCall().times(4); - replay(cmMock); + replay(cuMock); + gmMock.destroyGame(anyInt()); + expectLastCall().once(); + replay(gmMock); final User user1 = new User("test1"); final User user2 = new User("test2"); @@ -49,6 +56,7 @@ public class GameTest { assertTrue(game.removePlayer(user2)); assertEquals(null, game.getHost()); - verify(cmMock); + verify(cuMock); + verify(gmMock); } }