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.
This commit is contained in:
Andy Janata 2012-01-18 16:42:18 -08:00
parent 8187147e74
commit 5bda9fd452
13 changed files with 227 additions and 34 deletions

View File

@ -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.";

View File

@ -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.";

View File

@ -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();
};

View File

@ -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."),

View File

@ -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<ReturnableData, Object> data = new HashMap<ReturnableData, Object>();
@ -146,4 +163,7 @@ public class Game {
}
return users;
}
public class TooManyPlayersException extends Exception {
}
}

View File

@ -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<Integer> {
*
* @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<Integer> {
return null;
}
games.put(game.getId(), game);
broadcastGameListRefresh();
return game;
}
}
@ -94,7 +94,11 @@ public class GameManager implements Provider<Integer> {
} 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<Integer> {
}
}
private void broadcastGameListRefresh() {
public void broadcastGameListRefresh() {
final HashMap<ReturnableData, Object> broadcastData = new HashMap<ReturnableData, Object>();
broadcastData.put(LongPollResponse.EVENT, LongPollEvent.GAME_REFRESH.toString());
users.broadcastToAll(MessageType.GAME_EVENT, broadcastData);
@ -182,6 +186,19 @@ public class GameManager implements Provider<Integer> {
}
}
/**
* 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<Integer, Game> getGames() {
return games;
}

View File

@ -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;
}

View File

@ -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;

View File

@ -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);

View File

@ -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);

View File

@ -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<ReturnableData, Object> handle(final RequestWrapper request,
final HttpSession session) {
final Map<ReturnableData, Object> data = new HashMap<ReturnableData, Object>();
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;
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}