- create games

- list games
  - automatically refreshes list when game is created or destroyed
  - destroy games after removing all players (including at logout)
This commit is contained in:
Andy Janata 2012-01-17 21:58:09 -08:00
parent 40de91b5ac
commit 979a9c4123
12 changed files with 188 additions and 21 deletions

View File

@ -39,6 +39,7 @@
<div id="menubar">
<div id="menubar_left">
<input type="button" id="refresh_games" value="Refresh Games" />
<input type="button" id="create_game" value="Create Game" />
</div>
<div id="menubar_right">
<input type="button" id="logout" value="Log out" />

View File

@ -20,8 +20,6 @@ $(document).ready(function() {
// have not expressed an interest in being cleared out yet.
// $(window).bind("beforeunload", window_beforeunload);
$("#logout").click(logout_click);
$("#refresh_games").click(refreshgames_click);
});
function nickbox_keyup(e) {
@ -55,7 +53,3 @@ function chatsubmit_click(e) {
function logout_click(e) {
cah.Ajax.build(cah.$.AjaxOperation.LOG_OUT).run();
}
function refreshgames_click(e) {
cah.Ajax.build(cah.$.AjaxOperation.GAME_LIST).run();
}

View File

@ -10,6 +10,7 @@ cah.$.AjaxOperation.FIRST_LOAD = "firstload";
cah.$.AjaxOperation.LOG_OUT = "logout";
cah.$.AjaxOperation.GAME_LIST = "games";
cah.$.AjaxOperation.REGISTER = "register";
cah.$.AjaxOperation.CREATE_GAME = "create_game";
cah.$.AjaxOperation.CHAT = "chat";
cah.$.AjaxOperation.NAMES = "names";
@ -27,6 +28,7 @@ cah.$.AjaxResponse = function() {
};
cah.$.AjaxResponse.prototype.dummy = undefined;
cah.$.AjaxResponse.NEXT = "next";
cah.$.AjaxResponse.GAME_ID = "game_id";
cah.$.AjaxResponse.ERROR = "error";
cah.$.AjaxResponse.ERROR_CODE = "error_code";
cah.$.AjaxResponse.SERIAL = "serial";
@ -48,29 +50,33 @@ cah.$.ErrorCode = function() {
// pass
};
cah.$.ErrorCode.prototype.dummy = undefined;
cah.$.ErrorCode.NO_SESSION = "no_session";
cah.$.ErrorCode.NOT_REGISTERED = "not_registered";
cah.$.ErrorCode.TOO_MANY_GAMES = "too_many_games";
cah.$.ErrorCode.INVALID_NICK = "invalid_nick";
cah.$.ErrorCode.BAD_REQUEST = "bad_req";
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.OP_NOT_SPECIFIED = "op_not_spec";
cah.$.ErrorCode.NO_MSG_SPECIFIED = "no_msg_spec";
cah.$.ErrorCode.NICK_IN_USE = "nick_in_use";
cah.$.ErrorCode.SESSION_EXPIRED = "session_expired";
cah.$.ErrorCode.MESSAGE_TOO_LONG = "msg_too_long";
cah.$.ErrorCode.NO_NICK_SPECIFIED = "no_nick_spec";
cah.$.ErrorCode.BAD_OP = "bad_op";
cah.$.ErrorCode_msg = {};
cah.$.ErrorCode_msg['nick_in_use'] = "Nickname is already in use.";
cah.$.ErrorCode_msg['bad_op'] = "Invalid operation.";
cah.$.ErrorCode_msg['not_registered'] = "Not registered. Refresh the page.";
cah.$.ErrorCode_msg['bad_req'] = "Bad request.";
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_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['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['op_not_spec'] = "Operation not specified.";
cah.$.ErrorCode_msg['no_msg_spec'] = "No message specified.";
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['no_session'] = "Session not detected. Make sure you have cookies enabled.";
cah.$.ErrorCode_msg['no_nick_spec'] = "No nickname specified.";
cah.$.GameInfo = function() {
// pass
@ -96,6 +102,7 @@ cah.$.LongPollEvent = function() {
};
cah.$.LongPollEvent.prototype.dummy = undefined;
cah.$.LongPollEvent.NOOP = "noop";
cah.$.LongPollEvent.GAME_REFRESH = "game_refresh";
cah.$.LongPollEvent.NEW_PLAYER = "new_player";
cah.$.LongPollEvent.PLAYER_LEAVE = "player_leave";
cah.$.LongPollEvent.CHAT = "chat";

View File

@ -15,6 +15,17 @@ cah.GameList = function() {
* @private
*/
this.element_ = $("#game_list")[0];
/**
* Array of all game lobby objects.
*
* @type {Array}
* @private
*/
this.games_ = new Array();
$("#create_game").click(cah.bind(this, this.createGameClick_));
$("#refresh_games").click(cah.bind(this, this.refreshGamesClick_));
};
$(document).ready(function() {
@ -28,11 +39,40 @@ $(document).ready(function() {
* gameData The game data returned by the server.
*/
cah.GameList.prototype.update = function(gameData) {
// TODO clear existing display
while (this.element_.hasChildNodes()) {
this.element_.removeChild(this.element_.firstChild);
}
this.games_ = new Array();
for ( var key in gameData[cah.$.AjaxResponse.GAMES]) {
var game = gameData[cah.$.AjaxResponse.GAMES][key];
var lobby = new cah.GameListLobby(this.element_, game);
this.games_.push(lobby);
}
if (gameData[cah.$.AjaxResponse.GAMES].length < gameData[cah.$.AjaxResponse.MAX_GAMES]) {
$("#create_game").removeAttr("disabled");
} else {
$("#create_game").attr("disabled", "disabled");
}
};
/**
* @private
*/
cah.GameList.prototype.createGameClick_ = function(e) {
cah.Ajax.build(cah.$.AjaxOperation.CREATE_GAME).run();
};
/**
* @private
*/
cah.GameList.prototype.refreshGamesClick_ = function(e) {
this.refreshGames();
};
cah.GameList.prototype.refreshGames = function() {
cah.Ajax.build(cah.$.AjaxOperation.GAME_LIST).run();
};
/**

View File

@ -45,3 +45,7 @@ cah.longpoll.EventHandlers[cah.$.LongPollEvent.CHAT] = function(data) {
cah.log.status("&lt;" + data.from + "&gt; " + data.message);
}
};
cah.longpoll.EventHandlers[cah.$.LongPollEvent.GAME_REFRESH] = function(data) {
cah.GameList.instance.refreshGames();
};

View File

@ -27,6 +27,7 @@ public class Constants {
public enum AjaxOperation {
CHAT("chat"),
CREATE_GAME("create_game"),
FIRST_LOAD("firstload"),
GAME_LIST("games"),
LOG_OUT("logout"),
@ -66,6 +67,7 @@ public class Constants {
public enum AjaxResponse implements ReturnableData {
ERROR("error"),
ERROR_CODE("error_code"),
GAME_ID("game_id"),
GAMES("games"),
IN_PROGRESS("in_progress"),
MAX_GAMES("max_games"),
@ -89,6 +91,7 @@ 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."),
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."),
@ -99,7 +102,9 @@ public class Constants {
NO_SESSION("no_session", "Session not detected. Make sure you have cookies enabled."),
NOT_REGISTERED("not_registered", "Not registered. Refresh the page."),
OP_NOT_SPECIFIED("op_not_spec", "Operation not specified."),
SESSION_EXPIRED("session_expired", "Your session has expired. Refresh the page.");
SESSION_EXPIRED("session_expired", "Your session has expired. Refresh the page."),
TOO_MANY_GAMES("too_many_games", "There are too many games already in progress. Either join " +
"an existing game, or wait for one to become available.");
private final String code;
private final String message;
@ -128,6 +133,7 @@ public class Constants {
public enum LongPollEvent {
CHAT("chat"),
GAME_REFRESH("game_refresh"),
NEW_PLAYER("new_player"),
NOOP("noop"),
PLAYER_LEAVE("player_leave");

View File

@ -20,6 +20,7 @@ public class Game {
private final int id;
private final List<Player> players = new ArrayList<Player>(10);
private final ConnectedUsers connectedUsers;
private final GameManager gameManager;
private Player host;
private BlackDeck blackDeck;
private WhiteDeck whiteDeck;
@ -31,11 +32,14 @@ public class Game {
*
* @param id
* @param connectedUsers
* @param gameManager
*/
@Inject
public Game(@GameId final Integer id, final ConnectedUsers connectedUsers) {
public Game(@GameId final Integer id, final ConnectedUsers connectedUsers,
final GameManager gameManager) {
this.id = id;
this.connectedUsers = connectedUsers;
this.gameManager = gameManager;
state = GameState.LOBBY;
}
@ -84,6 +88,10 @@ public class Game {
break;
}
}
// this seems terrible
if (players.size() == 0) {
gameManager.destroyGame(id);
}
return players.size() == 0;
}
}

View File

@ -4,10 +4,15 @@ import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
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.GameManager.GameId;
import net.socialgamer.cah.data.QueuedMessage.MessageType;
import com.google.inject.BindingAnnotation;
import com.google.inject.Inject;
@ -29,15 +34,18 @@ 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;
private final ConnectedUsers users;
/**
* Potential next game id.
*/
private int nextId = 0;
@Inject
public GameManager(final Provider<Game> gameProvider, @MaxGames final Integer maxGames) {
public GameManager(final Provider<Game> gameProvider, @MaxGames final Integer maxGames,
final ConnectedUsers users) {
this.gameProvider = gameProvider;
this.maxGames = maxGames;
this.users = users;
}
/**
@ -56,6 +64,37 @@ public class GameManager implements Provider<Integer> {
return null;
}
games.put(game.getId(), game);
broadcastGameListRefresh();
return game;
}
}
/**
* Creates a new game and puts the specified user into the game, if there are free game slots.
* Returns 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);
} catch (final IllegalStateException ise) {
destroyGame(game.getId());
throw ise;
}
return game;
}
}
@ -78,9 +117,17 @@ public class GameManager implements Provider<Integer> {
nextId = gameId;
}
// TODO remove the players from the game
broadcastGameListRefresh();
}
}
private 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);
}
/**
* 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!

View File

@ -39,7 +39,7 @@ public class QueuedMessage implements Comparable<QueuedMessage> {
* @author ajanata
*/
public enum MessageType {
PLAYER_EVENT(3), GAME_PLAYER_EVENT(4), CHAT(5);
PLAYER_EVENT(3), GAME_EVENT(3), GAME_PLAYER_EVENT(4), CHAT(5);
private final int weight;

View File

@ -109,6 +109,9 @@ public class User {
* Mark this user as no longer valid, probably because they pinged out.
*/
public void noLongerVaild() {
if (currentGame != null) {
currentGame.removePlayer(this);
}
valid = false;
}

View File

@ -0,0 +1,56 @@
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.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.ConnectedUsers;
import net.socialgamer.cah.data.Game;
import net.socialgamer.cah.data.GameManager;
import net.socialgamer.cah.data.User;
import com.google.inject.Inject;
public class CreateGameHandler extends Handler {
public static final String OP = AjaxOperation.CREATE_GAME.toString();
private final GameManager gameManager;
private final ConnectedUsers users;
@Inject
public CreateGameHandler(final GameManager gameManager, final ConnectedUsers users) {
this.gameManager = gameManager;
this.users = users;
}
@Override
public Map<ReturnableData, Object> handle(final RequestWrapper request,
final HttpSession session) {
final Map<ReturnableData, Object> ret = new HashMap<ReturnableData, Object>();
final User user = (User) session.getAttribute(SessionAttribute.USER);
assert (user != null);
Game game;
try {
game = gameManager.createGameWithPlayer(user);
} catch (final IllegalStateException ise) {
return error(ErrorCode.CANNOT_JOIN_GAME);
}
if (game == null) {
return error(ErrorCode.TOO_MANY_GAMES);
} else {
ret.put(AjaxResponse.GAME_ID, game.getId());
return ret;
}
}
}

View File

@ -11,6 +11,7 @@ public class Handlers {
static {
LIST = new HashMap<String, Class<? extends Handler>>();
LIST.put(ChatHandler.OP, ChatHandler.class);
LIST.put(CreateGameHandler.OP, CreateGameHandler.class);
LIST.put(FirstLoadHandler.OP, FirstLoadHandler.class);
LIST.put(GameListHandler.OP, GameListHandler.class);
LIST.put(LogoutHandler.OP, LogoutHandler.class);