Merge branch 'spectator' of https://github.com/uecasm/PretendYoureXyzzy into uecasm-spectator

This commit is contained in:
Andy Janata 2013-11-29 02:08:13 +00:00
commit 4c686010c2
15 changed files with 367 additions and 20 deletions

View File

@ -137,7 +137,17 @@ h2,h3,h4 {
.gamelist_lobby_join { .gamelist_lobby_join {
width: 100%; width: 100%;
height: 100%; height: 75%;
border: 1px solid #aaa;
background: linear-gradient(#fff, #ddd) #ddd;
border-radius: .25em;
text-transform: uppercase;
font-weight: 700;
}
.gamelist_lobby_spectate {
width: 100%;
height: 25%;
border: 1px solid #aaa; border: 1px solid #aaa;
background: linear-gradient(#fff, #ddd) #ddd; background: linear-gradient(#fff, #ddd) #ddd;
border-radius: .25em; border-radius: .25em;

View File

@ -183,13 +183,18 @@ HttpSession hSession = request.getSession(true);
<div class="gamelist_lobby_left"> <div class="gamelist_lobby_left">
<h3> <h3>
<span class="gamelist_lobby_host">host</span>'s Game <span class="gamelist_lobby_host">host</span>'s Game
(<span class="gamelist_lobby_player_count"></span>/<span class="gamelist_lobby_max_players"></span>) (<span class="gamelist_lobby_player_count"></span>/<span class="gamelist_lobby_max_players"></span>,
<span class="gamelist_lobby_spectator_count"></span>/<span class="gamelist_lobby_max_spectators"></span>)
<span class="gamelist_lobby_status">status</span> <span class="gamelist_lobby_status">status</span>
</h3> </h3>
<div> <div>
<strong>Players:</strong> <strong>Players:</strong>
<span class="gamelist_lobby_players">host, player1, player2</span> <span class="gamelist_lobby_players">host, player1, player2</span>
</div> </div>
<div>
<strong>Spectators:</strong>
<span class="gamelist_lobby_spectators">spectator1</span>
</div>
<div><strong>Goal:</strong> <span class="gamelist_lobby_goal"></span></div> <div><strong>Goal:</strong> <span class="gamelist_lobby_goal"></span></div>
<div> <div>
<strong>Cards:</strong> <span class="gamelist_lobby_cardset"></span> <strong>Cards:</strong> <span class="gamelist_lobby_cardset"></span>
@ -198,6 +203,7 @@ HttpSession hSession = request.getSession(true);
</div> </div>
<div class="gamelist_lobby_right"> <div class="gamelist_lobby_right">
<input type="button" class="gamelist_lobby_join" value="Join" /> <input type="button" class="gamelist_lobby_join" value="Join" />
<input type="button" class="gamelist_lobby_spectate" value="View" />
</div> </div>
</div> </div>
</div> </div>
@ -317,7 +323,7 @@ HttpSession hSession = request.getSession(true);
<div id="scorecard_template" class="scorecard" tabindex="0"> <div id="scorecard_template" class="scorecard" tabindex="0">
<span class="scorecard_player">PlayerName</span> <span class="scorecard_player">PlayerName</span>
<div class="clear"></div> <div class="clear"></div>
<span class="scorecard_score">0</span> <span class="scorecard_point_title">Awesome Point<span class="scorecard_s">s</span></span> <span class="scorecard_points"><span class="scorecard_score">0</span> <span class="scorecard_point_title">Awesome Point<span class="scorecard_s">s</span></span></span>
<span class="scorecard_status">Status</span> <span class="scorecard_status">Status</span>
</div> </div>
</div> </div>
@ -360,6 +366,15 @@ HttpSession hSession = request.getSession(true);
</select> </select>
Having more than 10 players may get cramped! Having more than 10 players may get cramped!
<br/> <br/>
<label id="spectator_limit_template_label" for="spectator_limit_template">Spectator limit:</label>
<select id="spectator_limit_template" class="spectator_limit"
aria-label="Spectator limit.">
<% int defaultSpectatorLimit = 10; for (int i = 0; i <= 20; i++) { %>
<option <%= i == defaultSpectatorLimit ? "selected='selected' " : "" %>value="<%= i %>"><%= i %></option>
<% } %>
</select>
Spectators can watch and chat, but not actually play. Not even as Czar.
<br/>
<input type="checkbox" checked="checked" id="use_timer_template" class="use_timer" <input type="checkbox" checked="checked" id="use_timer_template" class="use_timer"
title="Players will be skipped if they have not played within a reasonable amount of time." title="Players will be skipped if they have not played within a reasonable amount of time."
aria-label="Use idle timer. Players will be skipped if they have not played within a reasonable amount of time."/> aria-label="Use idle timer. Players will be skipped if they have not played within a reasonable amount of time."/>

View File

@ -155,6 +155,17 @@ cah.ajax.Builder.prototype.withPlayerLimit = function(playerLimit) {
return this; return this;
}; };
/**
* @param {number}
* spectatorLimit Spectator limit field to use in the request.
* @returns {cah.ajax.Builder} This object.
*/
cah.ajax.Builder.prototype.withSpectatorLimit = function(spectatorLimit) {
this.assertNotExecuted_();
this.data[cah.$.AjaxRequest.SPECTATOR_LIMIT] = spectatorLimit;
return this;
};
/** /**
* @param {number} * @param {number}
* scoreLimit Score limit field to use in the request. * scoreLimit Score limit field to use in the request.

View File

@ -151,6 +151,10 @@ cah.ajax.SuccessHandlers[cah.$.AjaxOperation.JOIN_GAME] = function(data, req) {
cah.Game.joinGame(req[cah.$.AjaxRequest.GAME_ID]); cah.Game.joinGame(req[cah.$.AjaxRequest.GAME_ID]);
}; };
cah.ajax.SuccessHandlers[cah.$.AjaxOperation.SPECTATE_GAME] = function(data, req) {
cah.Game.joinGame(req[cah.$.AjaxRequest.GAME_ID]);
};
cah.ajax.SuccessHandlers[cah.$.AjaxOperation.CREATE_GAME] = function(data) { cah.ajax.SuccessHandlers[cah.$.AjaxOperation.CREATE_GAME] = function(data) {
cah.Game.joinGame(data[cah.$.AjaxResponse.GAME_ID]); cah.Game.joinGame(data[cah.$.AjaxResponse.GAME_ID]);
}; };

View File

@ -8,9 +8,10 @@ cah.$.AjaxOperation = function() {
cah.$.AjaxOperation.prototype.dummyForAutocomplete = undefined; cah.$.AjaxOperation.prototype.dummyForAutocomplete = undefined;
cah.$.AjaxOperation.START_GAME = "sg"; cah.$.AjaxOperation.START_GAME = "sg";
cah.$.AjaxOperation.FIRST_LOAD = "fl"; cah.$.AjaxOperation.FIRST_LOAD = "fl";
cah.$.AjaxOperation.SPECTATE_GAME = "vg";
cah.$.AjaxOperation.LOG_OUT = "lo"; cah.$.AjaxOperation.LOG_OUT = "lo";
cah.$.AjaxOperation.BAN = "b";
cah.$.AjaxOperation.JUDGE_SELECT = "js"; cah.$.AjaxOperation.JUDGE_SELECT = "js";
cah.$.AjaxOperation.BAN = "b";
cah.$.AjaxOperation.GAME_LIST = "ggl"; cah.$.AjaxOperation.GAME_LIST = "ggl";
cah.$.AjaxOperation.CHANGE_GAME_OPTIONS = "cgo"; cah.$.AjaxOperation.CHANGE_GAME_OPTIONS = "cgo";
cah.$.AjaxOperation.GET_GAME_INFO = "ggi"; cah.$.AjaxOperation.GET_GAME_INFO = "ggi";
@ -31,17 +32,18 @@ cah.$.AjaxRequest = function() {
}; };
cah.$.AjaxRequest.prototype.dummyForAutocomplete = undefined; cah.$.AjaxRequest.prototype.dummyForAutocomplete = undefined;
cah.$.AjaxRequest.WALL = "wall"; cah.$.AjaxRequest.WALL = "wall";
cah.$.AjaxRequest.MESSAGE = "m";
cah.$.AjaxRequest.CARD_ID = "cid";
cah.$.AjaxRequest.USE_TIMER = "ut"; cah.$.AjaxRequest.USE_TIMER = "ut";
cah.$.AjaxRequest.GAME_ID = "gid";
cah.$.AjaxRequest.CARD_SETS = "css"; cah.$.AjaxRequest.CARD_SETS = "css";
cah.$.AjaxRequest.SERIAL = "s"; cah.$.AjaxRequest.GAME_ID = "gid";
cah.$.AjaxRequest.PLAYER_LIMIT = "pL";
cah.$.AjaxRequest.PASSWORD = "pw";
cah.$.AjaxRequest.OP = "o"; cah.$.AjaxRequest.OP = "o";
cah.$.AjaxRequest.SCORE_LIMIT = "sl"; cah.$.AjaxRequest.PLAYER_LIMIT = "pL";
cah.$.AjaxRequest.NICKNAME = "n"; cah.$.AjaxRequest.NICKNAME = "n";
cah.$.AjaxRequest.SCORE_LIMIT = "sl";
cah.$.AjaxRequest.CARD_ID = "cid";
cah.$.AjaxRequest.MESSAGE = "m";
cah.$.AjaxRequest.SPECTATOR_LIMIT = "vL";
cah.$.AjaxRequest.SERIAL = "s";
cah.$.AjaxRequest.PASSWORD = "pw";
cah.$.AjaxResponse = function() { cah.$.AjaxResponse = function() {
// Dummy constructor to make Eclipse auto-complete. // Dummy constructor to make Eclipse auto-complete.
@ -179,6 +181,8 @@ cah.$.GameInfo.STATE = "S";
cah.$.GameInfo.PLAYERS = "P"; cah.$.GameInfo.PLAYERS = "P";
cah.$.GameInfo.USE_TIMER = "ut"; cah.$.GameInfo.USE_TIMER = "ut";
cah.$.GameInfo.CARD_SETS = "css"; cah.$.GameInfo.CARD_SETS = "css";
cah.$.GameInfo.SPECTATORS = "V";
cah.$.GameInfo.SPECTATOR_LIMIT = "vL";
cah.$.GameInfo.ID = "gid"; cah.$.GameInfo.ID = "gid";
cah.$.GameInfo.PLAYER_LIMIT = "pL"; cah.$.GameInfo.PLAYER_LIMIT = "pL";
cah.$.GameInfo.PASSWORD = "pw"; cah.$.GameInfo.PASSWORD = "pw";
@ -197,6 +201,7 @@ cah.$.GamePlayerStatus = function() {
// Dummy constructor to make Eclipse auto-complete. // Dummy constructor to make Eclipse auto-complete.
}; };
cah.$.GamePlayerStatus.prototype.dummyForAutocomplete = undefined; cah.$.GamePlayerStatus.prototype.dummyForAutocomplete = undefined;
cah.$.GamePlayerStatus.SPECTATOR = "sv";
cah.$.GamePlayerStatus.HOST = "sh"; cah.$.GamePlayerStatus.HOST = "sh";
cah.$.GamePlayerStatus.IDLE = "si"; cah.$.GamePlayerStatus.IDLE = "si";
cah.$.GamePlayerStatus.WINNER = "sw"; cah.$.GamePlayerStatus.WINNER = "sw";
@ -205,6 +210,7 @@ cah.$.GamePlayerStatus.JUDGE = "sj";
cah.$.GamePlayerStatus.JUDGING = "sjj"; cah.$.GamePlayerStatus.JUDGING = "sjj";
cah.$.GamePlayerStatus_msg = {}; cah.$.GamePlayerStatus_msg = {};
cah.$.GamePlayerStatus_msg['sp'] = "Playing"; cah.$.GamePlayerStatus_msg['sp'] = "Playing";
cah.$.GamePlayerStatus_msg['sv'] = "Spectator";
cah.$.GamePlayerStatus_msg['sh'] = "Host"; cah.$.GamePlayerStatus_msg['sh'] = "Host";
cah.$.GamePlayerStatus_msg['sw'] = "Winner!"; cah.$.GamePlayerStatus_msg['sw'] = "Winner!";
cah.$.GamePlayerStatus_msg['sj'] = "Card Czar"; cah.$.GamePlayerStatus_msg['sj'] = "Card Czar";
@ -212,6 +218,7 @@ cah.$.GamePlayerStatus_msg['sjj'] = "Selecting";
cah.$.GamePlayerStatus_msg['si'] = ""; cah.$.GamePlayerStatus_msg['si'] = "";
cah.$.GamePlayerStatus_msg_2 = {}; cah.$.GamePlayerStatus_msg_2 = {};
cah.$.GamePlayerStatus_msg_2['sp'] = "Select a card to play."; cah.$.GamePlayerStatus_msg_2['sp'] = "Select a card to play.";
cah.$.GamePlayerStatus_msg_2['sv'] = "You are just spectating.";
cah.$.GamePlayerStatus_msg_2['sh'] = "Wait for players then click Start Game."; cah.$.GamePlayerStatus_msg_2['sh'] = "Wait for players then click Start Game.";
cah.$.GamePlayerStatus_msg_2['sw'] = "You have won!"; cah.$.GamePlayerStatus_msg_2['sw'] = "You have won!";
cah.$.GamePlayerStatus_msg_2['sj'] = "You are the Card Czar."; cah.$.GamePlayerStatus_msg_2['sj'] = "You are the Card Czar.";
@ -241,10 +248,12 @@ cah.$.LongPollEvent.prototype.dummyForAutocomplete = undefined;
cah.$.LongPollEvent.BANNED = "B&"; cah.$.LongPollEvent.BANNED = "B&";
cah.$.LongPollEvent.KICKED = "k"; cah.$.LongPollEvent.KICKED = "k";
cah.$.LongPollEvent.HURRY_UP = "hu"; cah.$.LongPollEvent.HURRY_UP = "hu";
cah.$.LongPollEvent.GAME_SPECTATOR_LEAVE = "gvl";
cah.$.LongPollEvent.KICKED_FROM_GAME_IDLE = "kfgi"; cah.$.LongPollEvent.KICKED_FROM_GAME_IDLE = "kfgi";
cah.$.LongPollEvent.GAME_JUDGE_SKIPPED = "gjs"; cah.$.LongPollEvent.GAME_JUDGE_SKIPPED = "gjs";
cah.$.LongPollEvent.GAME_PLAYER_LEAVE = "gpl"; cah.$.LongPollEvent.GAME_PLAYER_LEAVE = "gpl";
cah.$.LongPollEvent.NEW_PLAYER = "np"; cah.$.LongPollEvent.NEW_PLAYER = "np";
cah.$.LongPollEvent.GAME_SPECTATOR_JOIN = "gvj";
cah.$.LongPollEvent.GAME_PLAYER_JOIN = "gpj"; cah.$.LongPollEvent.GAME_PLAYER_JOIN = "gpj";
cah.$.LongPollEvent.GAME_LIST_REFRESH = "glr"; cah.$.LongPollEvent.GAME_LIST_REFRESH = "glr";
cah.$.LongPollEvent.GAME_ROUND_COMPLETE = "grc"; cah.$.LongPollEvent.GAME_ROUND_COMPLETE = "grc";

View File

@ -57,6 +57,14 @@ cah.Game = function(id) {
this.scoreboardElement_ = $("#scoreboard_template").clone()[0]; this.scoreboardElement_ = $("#scoreboard_template").clone()[0];
this.scoreboardElement_.id = "scoreboard_" + id; this.scoreboardElement_.id = "scoreboard_" + id;
$(this.scoreboardElement_).removeClass("hide"); $(this.scoreboardElement_).removeClass("hide");
/**
* The first spectator element within the scoreboard.
*
* @type {HTMLDivElement}
* @private
*/
this.firstSpectatorElement_ = null;
/** /**
* The element for the chat room for this game * The element for the chat room for this game
@ -82,6 +90,7 @@ cah.Game = function(id) {
// TODO: It looks like I'm not changing the id on the label elements... // TODO: It looks like I'm not changing the id on the label elements...
$("#score_limit_template_label", this.optionsElement_).attr("for", "score_limit_" + id); $("#score_limit_template_label", this.optionsElement_).attr("for", "score_limit_" + id);
$("#player_limit_template_label", this.optionsElement_).attr("for", "player_limit_" + id); $("#player_limit_template_label", this.optionsElement_).attr("for", "player_limit_" + id);
$("#spectator_limit_template_label", this.optionsElement_).attr("for", "spectator_limit_" + id);
$("#card_set_template_label", this.optionsElement_).attr("for", "card_set_" + id); $("#card_set_template_label", this.optionsElement_).attr("for", "card_set_" + id);
$("#game_password_template_label", this.optionsElement_).attr("for", "game_password_" + id); $("#game_password_template_label", this.optionsElement_).attr("for", "game_password_" + id);
$("#game_hide_password_template_label", this.optionsElement_).attr("for", $("#game_hide_password_template_label", this.optionsElement_).attr("for",
@ -90,6 +99,7 @@ cah.Game = function(id) {
$("#score_limit_template", this.optionsElement_).attr("id", "score_limit_" + id); $("#score_limit_template", this.optionsElement_).attr("id", "score_limit_" + id);
$("#player_limit_template", this.optionsElement_).attr("id", "player_limit_" + id); $("#player_limit_template", this.optionsElement_).attr("id", "player_limit_" + id);
$("#spectator_limit_template", this.optionsElement_).attr("id", "spectator_limit_" + id);
$("#card_set_template", this.optionsElement_).attr("id", "card_set_" + id); $("#card_set_template", this.optionsElement_).attr("id", "card_set_" + id);
$("#game_password_template", this.optionsElement_).attr("id", "game_password_" + id); $("#game_password_template", this.optionsElement_).attr("id", "game_password_" + id);
$("#game_fake_password_template", this.optionsElement_).attr("id", "game_fake_password_" + id); $("#game_fake_password_template", this.optionsElement_).attr("id", "game_fake_password_" + id);
@ -748,6 +758,7 @@ cah.Game.prototype.updateGameStatus = function(data) {
$(".score_limit", this.optionsElement_).val(gameInfo[cah.$.GameInfo.SCORE_LIMIT]); $(".score_limit", this.optionsElement_).val(gameInfo[cah.$.GameInfo.SCORE_LIMIT]);
$(".player_limit", this.optionsElement_).val(gameInfo[cah.$.GameInfo.PLAYER_LIMIT]); $(".player_limit", this.optionsElement_).val(gameInfo[cah.$.GameInfo.PLAYER_LIMIT]);
$(".spectator_limit", this.optionsElement_).val(gameInfo[cah.$.GameInfo.SPECTATOR_LIMIT]);
$(".game_password", this.optionsElement_).val(gameInfo[cah.$.GameInfo.PASSWORD]); $(".game_password", this.optionsElement_).val(gameInfo[cah.$.GameInfo.PASSWORD]);
if (gameInfo[cah.$.GameInfo.USE_TIMER]) { if (gameInfo[cah.$.GameInfo.USE_TIMER]) {
$(".use_timer", this.optionsElement_).attr("checked", "checked"); $(".use_timer", this.optionsElement_).attr("checked", "checked");
@ -765,6 +776,11 @@ cah.Game.prototype.updateGameStatus = function(data) {
for ( var index in playerInfos) { for ( var index in playerInfos) {
this.updateUserStatus(playerInfos[index]); this.updateUserStatus(playerInfos[index]);
} }
var spectators = gameInfo[cah.$.GameInfo.SPECTATORS];
for ( var index in spectators) {
this.updateSpectator(spectators[index]);
}
}; };
/** /**
@ -780,7 +796,11 @@ cah.Game.prototype.updateUserStatus = function(playerInfo) {
if (!panel) { if (!panel) {
// new score panel // new score panel
panel = new cah.GameScorePanel(playerName); panel = new cah.GameScorePanel(playerName);
$(this.scoreboardElement_).append(panel.getElement()); if (this.firstSpectatorElement_) {
$(this.firstSpectatorElement_).before(panel.getElement());
} else {
$(this.scoreboardElement_).append(panel.getElement());
}
this.scoreCards_[playerName] = panel; this.scoreCards_[playerName] = panel;
} }
var oldStatus = panel.getStatus(); var oldStatus = panel.getStatus();
@ -838,6 +858,32 @@ cah.Game.prototype.updateUserStatus = function(playerInfo) {
} }
}; };
/**
* Update a single spectator's info.
*
* @param {String}
* spectator The spectator name.
*/
cah.Game.prototype.updateSpectator = function(spectator) {
var panel = this.scoreCards_[spectator];
if (!panel) {
// new score panel
panel = new cah.GameScorePanel(spectator);
$(this.scoreboardElement_).append(panel.getElement());
this.scoreCards_[spectator] = panel;
if (!this.firstSpectatorElement_) {
this.firstSpectatorElement_ = panel.getElement();
}
}
panel.update(-1, cah.$.GamePlayerStatus.SPECTATOR);
if (spectator == cah.nickname) {
$(".game_message", this.element_).text(cah.$.GamePlayerStatus_msg_2[cah.$.GamePlayerStatus.SPECTATOR]);
$(".confirm_card", this.element_).attr("disabled", "disabled");
}
};
/** /**
* Round has completed. Update display of round cards to show winner. * Round has completed. Update display of round cards to show winner.
* *
@ -1140,6 +1186,45 @@ cah.Game.prototype.playerLeave = function(player) {
delete this.scoreCards_[player]; delete this.scoreCards_[player];
}; };
/**
* A spectator has joined the game.
*
* @param {String}
* spectator Spectator that joined.
*/
cah.Game.prototype.spectatorJoin = function(spectator) {
if (spectator != cah.nickname) {
cah.log.status_with_game(this, spectator + " has started spectating the game.");
this.refreshGameStatus();
} else {
cah.log.status_with_game(this, "You have started spectating the game.");
}
this.updateSpectator(spectator);
};
/**
* A spectator has left the game.
*
* @param {String}
* spectator Spectator that left.
*/
cah.Game.prototype.spectatorLeave = function(spectator) {
if (spectator != cah.nickname) {
cah.log.status_with_game(this, spectator + " has stopped spectating the game.");
this.refreshGameStatus();
} else {
cah.log.status_with_game(this, "You have stopped spectating the game.");
}
var scorecard = this.scoreCards_[spectator];
if (scorecard) {
if (this.firstSpectatorElement_ == scorecard.getElement()) {
this.firstSpectatorElement_ = this.firstSpectatorElement_.nextSibling;
}
$(scorecard.getElement()).remove();
}
delete this.scoreCards_[spectator];
};
/** /**
* Refresh game scoreboard, etc. * Refresh game scoreboard, etc.
*/ */
@ -1249,7 +1334,8 @@ cah.Game.prototype.optionChanged_ = function(e) {
} }
cah.Ajax.build(cah.$.AjaxOperation.CHANGE_GAME_OPTIONS).withGameId(this.id_).withScoreLimit( cah.Ajax.build(cah.$.AjaxOperation.CHANGE_GAME_OPTIONS).withGameId(this.id_).withScoreLimit(
$(".score_limit", this.optionsElement_).val()).withPlayerLimit( $(".score_limit", this.optionsElement_).val()).withPlayerLimit(
$(".player_limit", this.optionsElement_).val()).withCardSets(cardSetIds).withPassword( $(".player_limit", this.optionsElement_).val()).withSpectatorLimit(
$(".spectator_limit", this.optionsElement_).val()).withCardSets(cardSetIds).withPassword(
$(".game_password", this.optionsElement_).val()).withUseTimer( $(".game_password", this.optionsElement_).val()).withUseTimer(
!!$('.use_timer', this.optionsElement_).attr('checked')).run(); !!$('.use_timer', this.optionsElement_).attr('checked')).run();
}; };
@ -1337,10 +1423,17 @@ cah.GameScorePanel.prototype.update = function(score, status) {
$(".scorecard_score", this.element_).text(score); $(".scorecard_score", this.element_).text(score);
$(".scorecard_status", this.element_).text(cah.$.GamePlayerStatus_msg[status]); $(".scorecard_status", this.element_).text(cah.$.GamePlayerStatus_msg[status]);
$(".scorecard_s", this.element_).text(score == 1 ? "" : "s"); $(".scorecard_s", this.element_).text(score == 1 ? "" : "s");
$(this.element_).attr( if (score < 0) {
"aria-label", $(".scorecard_points", this.element_).addClass("hide");
this.player_ + " has " + score + " Awesome Point" + (score == 1 ? "" : "s") + ". " $(this.element_).attr("aria-label", this.player_ + ". "
+ cah.$.GamePlayerStatus_msg[status]); + cah.$.GamePlayerStatus_msg[status]);
} else {
$(".scorecard_points", this.element_).removeClass("hide");
$(this.element_).attr(
"aria-label",
this.player_ + " has " + score + " Awesome Point" + (score == 1 ? "" : "s") + ". "
+ cah.$.GamePlayerStatus_msg[status]);
}
}; };
/** /**

View File

@ -211,11 +211,15 @@ cah.GameListLobby = function(parentElem, data) {
$(".gamelist_lobby_id", this.element_).text(this.id_); $(".gamelist_lobby_id", this.element_).text(this.id_);
$(".gamelist_lobby_host", this.element_).text(data[cah.$.GameInfo.HOST]); $(".gamelist_lobby_host", this.element_).text(data[cah.$.GameInfo.HOST]);
$(".gamelist_lobby_players", this.element_).text(data[cah.$.GameInfo.PLAYERS].join(", ")); $(".gamelist_lobby_players", this.element_).text(data[cah.$.GameInfo.PLAYERS].join(", "));
$(".gamelist_lobby_spectators", this.element_).text(data[cah.$.GameInfo.SPECTATORS].join(", "));
var statusMessage = cah.$.GameState_msg[data[cah.$.GameInfo.STATE]]; var statusMessage = cah.$.GameState_msg[data[cah.$.GameInfo.STATE]];
$(".gamelist_lobby_status", this.element_).text(statusMessage); $(".gamelist_lobby_status", this.element_).text(statusMessage);
$(".gamelist_lobby_join", this.element_).click(cah.bind(this, this.joinClick)); $(".gamelist_lobby_join", this.element_).click(cah.bind(this, this.joinClick));
$(".gamelist_lobby_spectate", this.element_).click(cah.bind(this, this.spectateClick));
$(".gamelist_lobby_player_count", this.element_).text(data[cah.$.GameInfo.PLAYERS].length); $(".gamelist_lobby_player_count", this.element_).text(data[cah.$.GameInfo.PLAYERS].length);
$(".gamelist_lobby_max_players", this.element_).text(data[cah.$.GameInfo.PLAYER_LIMIT]); $(".gamelist_lobby_max_players", this.element_).text(data[cah.$.GameInfo.PLAYER_LIMIT]);
$(".gamelist_lobby_spectator_count", this.element_).text(data[cah.$.GameInfo.SPECTATORS].length);
$(".gamelist_lobby_max_spectators", this.element_).text(data[cah.$.GameInfo.SPECTATOR_LIMIT]);
$(".gamelist_lobby_goal", this.element_).text(data[cah.$.GameInfo.SCORE_LIMIT]); $(".gamelist_lobby_goal", this.element_).text(data[cah.$.GameInfo.SCORE_LIMIT]);
var cardSetNames = []; var cardSetNames = [];
data[cah.$.GameInfo.CARD_SETS].sort(); data[cah.$.GameInfo.CARD_SETS].sort();
@ -232,7 +236,8 @@ cah.GameListLobby = function(parentElem, data) {
$(this.element_).attr( $(this.element_).attr(
"aria-label", "aria-label",
data[cah.$.GameInfo.HOST] + "'s game, with " + data[cah.$.GameInfo.PLAYERS].length + " of " data[cah.$.GameInfo.HOST] + "'s game, with " + data[cah.$.GameInfo.PLAYERS].length + " of "
+ data[cah.$.GameInfo.PLAYER_LIMIT] + " players. " + statusMessage + ". Goal is " + data[cah.$.GameInfo.PLAYER_LIMIT] + " players, and " + data[cah.$.GameInfo.SPECTATORS].length
+ " of " + data[cah.$.GameInfo.SPECTATOR_LIMIT] + "spectators. " + statusMessage + ". Goal is "
+ data[cah.$.GameInfo.SCORE_LIMIT] + " Awesome Points. Using " + cardSetNames.length + data[cah.$.GameInfo.SCORE_LIMIT] + " Awesome Points. Using " + cardSetNames.length
+ " card set" + (cardSetNames.length == 1 ? "" : "s") + ". " + " card set" + (cardSetNames.length == 1 ? "" : "s") + ". "
+ (data[cah.$.GameInfo.HAS_PASSWORD] ? "Has" : "Does not have") + " a password."); + (data[cah.$.GameInfo.HAS_PASSWORD] ? "Has" : "Does not have") + " a password.");
@ -259,10 +264,25 @@ cah.GameListLobby.prototype.join = function() {
$('.gamelist_lobby_join', this.element_).click(); $('.gamelist_lobby_join', this.element_).click();
}; };
/**
* Event handler for clicking the View button in a game lobby.
*/
cah.GameListLobby.prototype.spectateClick = function() {
var password = "";
if (this.data_[cah.$.GameInfo.HAS_PASSWORD]) {
password = prompt("Enter the game's password.");
if (password == null) {
password = "";
}
}
cah.Ajax.build(cah.$.AjaxOperation.SPECTATE_GAME).withGameId(this.id_).withPassword(password).run();
};
/** /**
* Remove the game lobby from the document and free up resources. * Remove the game lobby from the document and free up resources.
*/ */
cah.GameListLobby.prototype.dispose = function() { cah.GameListLobby.prototype.dispose = function() {
this.parentElem_.removeChild(this.element_); this.parentElem_.removeChild(this.element_);
$(".gamelist_lobby_join", this.element_).unbind(); $(".gamelist_lobby_join", this.element_).unbind();
$(".gamelist_lobby_spectate", this.element_).unbind();
}; };

View File

@ -128,6 +128,17 @@ cah.longpoll.EventHandlers[cah.$.LongPollEvent.GAME_PLAYER_LEAVE] = function(dat
data[cah.$.LongPollResponse.NICKNAME], "player leave"); data[cah.$.LongPollResponse.NICKNAME], "player leave");
}; };
cah.longpoll.EventHandlers[cah.$.LongPollEvent.GAME_SPECTATOR_JOIN] = function(data) {
cah.longpoll.EventHandlers.__gameEvent(data, cah.Game.prototype.spectatorJoin,
data[cah.$.LongPollResponse.NICKNAME],
"spectator join (if you just joined a game this may be OK)");
};
cah.longpoll.EventHandlers[cah.$.LongPollEvent.GAME_SPECTATOR_LEAVE] = function(data) {
cah.longpoll.EventHandlers.__gameEvent(data, cah.Game.prototype.spectatorLeave,
data[cah.$.LongPollResponse.NICKNAME], "spectator leave");
};
cah.longpoll.EventHandlers[cah.$.LongPollEvent.HAND_DEAL] = function(data) { cah.longpoll.EventHandlers[cah.$.LongPollEvent.HAND_DEAL] = function(data) {
cah.longpoll.EventHandlers.__gameEvent(data, cah.Game.prototype.dealtCards, cah.longpoll.EventHandlers.__gameEvent(data, cah.Game.prototype.dealtCards,
data[cah.$.LongPollResponse.HAND], "dealt cards"); data[cah.$.LongPollResponse.HAND], "dealt cards");

View File

@ -172,6 +172,7 @@ public class Constants {
GET_CARDS("gc"), GET_CARDS("gc"),
GET_GAME_INFO("ggi"), GET_GAME_INFO("ggi"),
JOIN_GAME("jg"), JOIN_GAME("jg"),
SPECTATE_GAME("vg"),
JUDGE_SELECT("js"), JUDGE_SELECT("js"),
KICK("K"), KICK("K"),
LEAVE_GAME("lg"), LEAVE_GAME("lg"),
@ -208,6 +209,7 @@ public class Constants {
OP("o"), OP("o"),
PASSWORD("pw"), PASSWORD("pw"),
PLAYER_LIMIT("pL"), PLAYER_LIMIT("pL"),
SPECTATOR_LIMIT("vL"),
SCORE_LIMIT("sl"), SCORE_LIMIT("sl"),
SERIAL("s"), SERIAL("s"),
USE_TIMER("ut"), USE_TIMER("ut"),
@ -371,6 +373,8 @@ public class Constants {
GAME_PLAYER_KICKED_IDLE("gpki"), GAME_PLAYER_KICKED_IDLE("gpki"),
GAME_PLAYER_LEAVE("gpl"), GAME_PLAYER_LEAVE("gpl"),
GAME_PLAYER_SKIPPED("gps"), GAME_PLAYER_SKIPPED("gps"),
GAME_SPECTATOR_JOIN("gvj"),
GAME_SPECTATOR_LEAVE("gvl"),
GAME_ROUND_COMPLETE("grc"), GAME_ROUND_COMPLETE("grc"),
GAME_STATE_CHANGE("gsc"), GAME_STATE_CHANGE("gsc"),
GAME_WHITE_RESHUFFLE("gwr"), GAME_WHITE_RESHUFFLE("gwr"),
@ -596,6 +600,9 @@ public class Constants {
PLAYER_LIMIT(AjaxRequest.PLAYER_LIMIT), PLAYER_LIMIT(AjaxRequest.PLAYER_LIMIT),
PLAYERS("P"), PLAYERS("P"),
@DuplicationAllowed @DuplicationAllowed
SPECTATOR_LIMIT(AjaxRequest.SPECTATOR_LIMIT),
SPECTATORS("V"),
@DuplicationAllowed
SCORE_LIMIT(AjaxRequest.SCORE_LIMIT), SCORE_LIMIT(AjaxRequest.SCORE_LIMIT),
STATE("S"), STATE("S"),
@DuplicationAllowed @DuplicationAllowed
@ -648,7 +655,8 @@ public class Constants {
JUDGE("sj", "Card Czar", "You are the Card Czar."), JUDGE("sj", "Card Czar", "You are the Card Czar."),
JUDGING("sjj", "Selecting", "Select a winning card."), JUDGING("sjj", "Selecting", "Select a winning card."),
PLAYING("sp", "Playing", "Select a card to play."), PLAYING("sp", "Playing", "Select a card to play."),
WINNER("sw", "Winner!", "You have won!"); WINNER("sw", "Winner!", "You have won!"),
SPECTATOR("sv", "Spectator", "You are just spectating.");
private final String status; private final String status;
private final String message; private final String message;

View File

@ -92,6 +92,7 @@ public class Game {
*/ */
private final List<Player> roundPlayers = Collections.synchronizedList(new ArrayList<Player>(9)); private final List<Player> roundPlayers = Collections.synchronizedList(new ArrayList<Player>(9));
private final PlayerPlayedCardsTracker playedCards = new PlayerPlayedCardsTracker(); private final PlayerPlayedCardsTracker playedCards = new PlayerPlayedCardsTracker();
private final List<User> spectators = Collections.synchronizedList(new ArrayList<User>(10));
private final ConnectedUsers connectedUsers; private final ConnectedUsers connectedUsers;
private final GameManager gameManager; private final GameManager gameManager;
private Player host; private Player host;
@ -101,6 +102,7 @@ public class Game {
private WhiteDeck whiteDeck; private WhiteDeck whiteDeck;
private GameState state; private GameState state;
private int maxPlayers = 6; private int maxPlayers = 6;
private int maxSpectators = 6;
private int judgeIndex = 0; private int judgeIndex = 0;
private final static int ROUND_INTERMISSION = 8 * 1000; private final static int ROUND_INTERMISSION = 8 * 1000;
/** /**
@ -302,6 +304,65 @@ public class Game {
return false; return false;
} }
/**
* Add a spectator to the game.
*
* Synchronizes on {@link #spectators}.
*
* @param user
* Spectator to add to this game.
* @throws TooManySpectatorsException
* Thrown if this game is at its maximum spectator capacity.
* @throws IllegalStateException
* Thrown if {@code user} is already in a game.
*/
public void addSpectator(final User user) throws TooManySpectatorsException,
IllegalStateException {
logger.info(String.format("%s joined game %d as a spectator.", user.toString(), id));
synchronized (spectators) {
if (spectators.size() >= maxSpectators) {
throw new TooManySpectatorsException();
}
// this will throw IllegalStateException if the user is already in a game, including this one.
user.joinGame(this);
spectators.add(user);
}
final HashMap<ReturnableData, Object> data = getEventMap();
data.put(LongPollResponse.EVENT, LongPollEvent.GAME_SPECTATOR_JOIN.toString());
data.put(LongPollResponse.NICKNAME, user.getNickname());
broadcastToPlayers(MessageType.GAME_PLAYER_EVENT, data);
gameManager.broadcastGameListRefresh();
}
/**
* Remove a spectator from the game.
* <br/>
* Synchronizes on {@link #spectator}.
*
* @param user
* Spectator to remove from the game.
*/
public void removeSpectator(final User user) {
logger.info(String.format("Removing spectator %s from game %d.", user.toString(), id));
synchronized (spectators) {
if (!spectators.remove(user)) {
return;
} // not actually spectating
user.leaveGame(this);
}
// do this down here so the person that left doesn't get the notice too
final HashMap<ReturnableData, Object> data = getEventMap();
data.put(LongPollResponse.EVENT, LongPollEvent.GAME_SPECTATOR_LEAVE.toString());
data.put(LongPollResponse.NICKNAME, user.getNickname());
broadcastToPlayers(MessageType.GAME_PLAYER_EVENT, data);
// Don't do this anymore, it was driving up a crazy amount of traffic.
// gameManager.broadcastGameListRefresh();
}
/** /**
* Return all played cards to their respective player's hand. * Return all played cards to their respective player's hand.
* <br/> * <br/>
@ -368,9 +429,11 @@ public class Game {
} }
public void updateGameSettings(final int newScoreGoal, final int newMaxPlayers, public void updateGameSettings(final int newScoreGoal, final int newMaxPlayers,
final int newMaxSpectators,
final Set<CardSet> newCardSets, final String newPassword, final boolean newUseTimer) { final Set<CardSet> newCardSets, final String newPassword, final boolean newUseTimer) {
this.scoreGoal = newScoreGoal; this.scoreGoal = newScoreGoal;
this.maxPlayers = newMaxPlayers; this.maxPlayers = newMaxPlayers;
this.maxSpectators = newMaxSpectators;
synchronized (this.cardSets) { synchronized (this.cardSets) {
this.cardSets.clear(); this.cardSets.clear();
this.cardSets.addAll(newCardSets); this.cardSets.addAll(newCardSets);
@ -424,6 +487,7 @@ public class Game {
} }
info.put(GameInfo.CARD_SETS, cardSetIds); info.put(GameInfo.CARD_SETS, cardSetIds);
info.put(GameInfo.PLAYER_LIMIT, maxPlayers); info.put(GameInfo.PLAYER_LIMIT, maxPlayers);
info.put(GameInfo.SPECTATOR_LIMIT, maxSpectators);
info.put(GameInfo.SCORE_LIMIT, scoreGoal); info.put(GameInfo.SCORE_LIMIT, scoreGoal);
info.put(GameInfo.USE_TIMER, useTimer); info.put(GameInfo.USE_TIMER, useTimer);
if (includePassword) { if (includePassword) {
@ -437,6 +501,14 @@ public class Game {
playerNames.add(player.toString()); playerNames.add(player.toString());
} }
info.put(GameInfo.PLAYERS, playerNames); info.put(GameInfo.PLAYERS, playerNames);
final User[] spectatorsCopy = spectators.toArray(new User[spectators.size()]);
final List<String> spectatorNames = new ArrayList<String>(spectatorsCopy.length);
for (final User spectator : spectatorsCopy) {
spectatorNames.add(spectator.toString());
}
info.put(GameInfo.SPECTATORS, spectatorNames);
return info; return info;
} }
@ -1160,6 +1232,9 @@ public class Game {
for (final Player player : playersCopy) { for (final Player player : playersCopy) {
users.add(player.getUser()); users.add(player.getUser());
} }
synchronized (spectators) {
users.addAll(spectators);
}
return users; return users;
} }
@ -1313,4 +1388,11 @@ public class Game {
public class TooManyPlayersException extends Exception { public class TooManyPlayersException extends Exception {
private static final long serialVersionUID = -6603422097641992017L; private static final long serialVersionUID = -6603422097641992017L;
} }
/**
* Exception to be thrown when there are too many spectators in a game.
*/
public class TooManySpectatorsException extends Exception {
private static final long serialVersionUID = -6603422097641992018L;
}
} }

View File

@ -166,6 +166,7 @@ public class GameManager implements Provider<Integer> {
final List<User> usersToRemove = game.getUsers(); final List<User> usersToRemove = game.getUsers();
for (final User user : usersToRemove) { for (final User user : usersToRemove) {
game.removePlayer(user); game.removePlayer(user);
game.removeSpectator(user);
} }
logger.info(String.format("Destroyed game %d.", game.getId())); logger.info(String.format("Destroyed game %d.", game.getId()));

View File

@ -43,6 +43,7 @@ public class ChangeGameOptionHandler extends GameWithPlayerHandler {
try { try {
final int scoreLimit = Integer.parseInt(request.getParameter(AjaxRequest.SCORE_LIMIT)); final int scoreLimit = Integer.parseInt(request.getParameter(AjaxRequest.SCORE_LIMIT));
final int playerLimit = Integer.parseInt(request.getParameter(AjaxRequest.PLAYER_LIMIT)); final int playerLimit = Integer.parseInt(request.getParameter(AjaxRequest.PLAYER_LIMIT));
final int spectatorLimit = Integer.parseInt(request.getParameter(AjaxRequest.SPECTATOR_LIMIT));
final String[] cardSetsParsed = request.getParameter(AjaxRequest.CARD_SETS).split(","); final String[] cardSetsParsed = request.getParameter(AjaxRequest.CARD_SETS).split(",");
final Set<CardSet> cardSets = new HashSet<CardSet>(); final Set<CardSet> cardSets = new HashSet<CardSet>();
for (final String cardSetId : cardSetsParsed) { for (final String cardSetId : cardSetsParsed) {
@ -63,7 +64,7 @@ public class ChangeGameOptionHandler extends GameWithPlayerHandler {
if (null != useTimerString && !"".equals(useTimerString)) { if (null != useTimerString && !"".equals(useTimerString)) {
useTimer = Boolean.valueOf(useTimerString); useTimer = Boolean.valueOf(useTimerString);
} }
game.updateGameSettings(scoreLimit, playerLimit, cardSets, password, useTimer); game.updateGameSettings(scoreLimit, playerLimit, spectatorLimit, cardSets, password, useTimer);
// only broadcast an update if the password state has changed, because it needs to change // only broadcast an update if the password state has changed, because it needs to change
// the text on the join button and the sort order // the text on the join button and the sort order

View File

@ -28,6 +28,7 @@ public class Handlers {
LIST.put(NamesHandler.OP, NamesHandler.class); LIST.put(NamesHandler.OP, NamesHandler.class);
LIST.put(PlayCardHandler.OP, PlayCardHandler.class); LIST.put(PlayCardHandler.OP, PlayCardHandler.class);
LIST.put(RegisterHandler.OP, RegisterHandler.class); LIST.put(RegisterHandler.OP, RegisterHandler.class);
LIST.put(SpectateGameHandler.OP, SpectateGameHandler.class);
LIST.put(StartGameHandler.OP, StartGameHandler.class); LIST.put(StartGameHandler.OP, StartGameHandler.class);
} }
} }

View File

@ -58,6 +58,7 @@ public class LeaveGameHandler extends GameWithPlayerHandler {
final Map<ReturnableData, Object> data = new HashMap<ReturnableData, Object>(); final Map<ReturnableData, Object> data = new HashMap<ReturnableData, Object>();
game.removePlayer(user); game.removePlayer(user);
game.removeSpectator(user);
return data; return data;
} }
} }

View File

@ -0,0 +1,80 @@
/**
* Copyright (c) 2012, Andy Janata
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification, are permitted
* provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice, this list of conditions
* and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright notice, this list of
* conditions and the following disclaimer in the documentation and/or other materials provided
* with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR
* IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
* FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
* WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY
* WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
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.ErrorCode;
import net.socialgamer.cah.Constants.ReturnableData;
import net.socialgamer.cah.RequestWrapper;
import net.socialgamer.cah.data.Game;
import net.socialgamer.cah.data.Game.TooManySpectatorsException;
import net.socialgamer.cah.data.GameManager;
import net.socialgamer.cah.data.User;
import com.google.inject.Inject;
/**
* Handler to spectate a game.
*
* @author Gavin Lambert (cah@mirality.co.nz)
*/
public class SpectateGameHandler extends GameHandler {
public static final String OP = AjaxOperation.SPECTATE_GAME.toString();
@Inject
public SpectateGameHandler(final GameManager gameManager) {
super(gameManager);
}
@Override
public Map<ReturnableData, Object> handle(final RequestWrapper request,
final HttpSession session, final User user, final Game game) {
final Map<ReturnableData, Object> data = new HashMap<ReturnableData, Object>();
final String password = request.getParameter(AjaxRequest.PASSWORD);
final String gamePassword = game.getPassword();
if (gamePassword != null && !gamePassword.equals("")) {
if (password == null || !gamePassword.equals(password)) {
return error(ErrorCode.WRONG_PASSWORD);
}
}
try {
game.addSpectator(user);
} catch (final IllegalStateException e) {
return error(ErrorCode.CANNOT_JOIN_ANOTHER_GAME);
} catch (final TooManySpectatorsException e) {
return error(ErrorCode.GAME_FULL);
}
gameManager.broadcastGameListRefresh();
return data;
}
}