New feature: blank cards

Add support for blank white cards, that allow the player to enter their
own answer.  (Game host can choose how many to include in the deck; the
default is none.)
This commit is contained in:
Gavin Lambert 2013-06-20 00:23:10 +12:00
parent 40c8c1e09b
commit a67eedd4ba
10 changed files with 112 additions and 16 deletions

View File

@ -368,6 +368,14 @@ HttpSession hSession = request.getSession(true);
<br/>
Select any number of: <span class="extra_card_sets"></span>
</fieldset>
<label id="blanks_limit_label" title="Blank cards allow a player to type in their own answer.">
Also include <select id="blanks_limit_template" class="blanks_limit">
<% for (int i = 0; i <= 10; i++) { %>
<option <%= i == 0 ? "selected='selected' " : "" %>value="<%= i %>"><%= i %></option>
<% } %>
</select> blank white cards.
</label>
<br/>
<br/>
<label id="game_password_template_label" for="game_password_template">Game password:</label>
<input type="text" id="game_password_template" class="game_password"

View File

@ -177,6 +177,17 @@ cah.ajax.Builder.prototype.withPassword = function(password) {
return this;
};
/**
* @param {number}
* scoreLimit Score limit field to use in the request.
* @returns {cah.ajax.Builder} This object.
*/
cah.ajax.Builder.prototype.withBlanksLimit = function(blanksLimit) {
this.assertNotExecuted_();
this.data[cah.$.AjaxRequest.BLANKS_LIMIT] = blanksLimit;
return this;
};
/**
* @param {boolean}
* useTimer Whether or not the game should use the idle timer.

View File

@ -335,6 +335,15 @@ cah.card.WhiteCard = function(opt_faceUp, opt_id) {
};
cah.inherits(cah.card.WhiteCard, cah.card.BaseCard);
/**
* Checks if this is a blank card.
*
* @returns True if this is a blank card.
*/
cah.card.WhiteCard.prototype.isBlankCard = function() {
return this.getServerId() == 0;
};
/**
* @override
*/

View File

@ -31,17 +31,18 @@ cah.$.AjaxRequest = function() {
};
cah.$.AjaxRequest.prototype.dummyForAutocomplete = undefined;
cah.$.AjaxRequest.WALL = "wall";
cah.$.AjaxRequest.MESSAGE = "m";
cah.$.AjaxRequest.CARD_ID = "cid";
cah.$.AjaxRequest.USE_TIMER = "ut";
cah.$.AjaxRequest.GAME_ID = "gid";
cah.$.AjaxRequest.CARD_SETS = "css";
cah.$.AjaxRequest.SERIAL = "s";
cah.$.AjaxRequest.PLAYER_LIMIT = "pL";
cah.$.AjaxRequest.PASSWORD = "pw";
cah.$.AjaxRequest.GAME_ID = "gid";
cah.$.AjaxRequest.OP = "o";
cah.$.AjaxRequest.SCORE_LIMIT = "sl";
cah.$.AjaxRequest.PLAYER_LIMIT = "pL";
cah.$.AjaxRequest.NICKNAME = "n";
cah.$.AjaxRequest.SCORE_LIMIT = "sl";
cah.$.AjaxRequest.CARD_ID = "cid";
cah.$.AjaxRequest.MESSAGE = "m";
cah.$.AjaxRequest.BLANKS_LIMIT = "bl";
cah.$.AjaxRequest.SERIAL = "s";
cah.$.AjaxRequest.PASSWORD = "pw";
cah.$.AjaxResponse = function() {
// Dummy constructor to make Eclipse auto-complete.
@ -178,6 +179,7 @@ cah.$.GameInfo.HOST = "H";
cah.$.GameInfo.STATE = "S";
cah.$.GameInfo.PLAYERS = "P";
cah.$.GameInfo.USE_TIMER = "ut";
cah.$.GameInfo.BLANKS_LIMIT = "bl";
cah.$.GameInfo.CARD_SETS = "css";
cah.$.GameInfo.ID = "gid";
cah.$.GameInfo.PLAYER_LIMIT = "pL";

View File

@ -95,6 +95,7 @@ cah.Game = function(id) {
$("#game_fake_password_template", this.optionsElement_).attr("id", "game_fake_password_" + id);
$("#game_hide_password_template", this.optionsElement_).attr("id", "game_hide_password_" + id);
$("#use_timer_template", this.optionsElement_).attr("id", "use_timer_" + id);
$("#blanks_limit_template", this.optionsElement_).attr("id", "blanks_limit_" + id);
for ( var key in cah.CardSet.byWeight) {
/** @type {cah.CardSet} */
@ -758,6 +759,7 @@ cah.Game.prototype.updateGameStatus = function(data) {
var cardSetId = cardSetIds[key];
$("#card_set_" + this.id_ + "_" + cardSetId, this.optionsElement_).attr("checked", "checked");
}
$(".blanks_limit", this.optionsElement_).val(gameInfo[cah.$.GameInfo.BLANKS_LIMIT]);
var playerInfos = data[cah.$.AjaxResponse.PLAYER_INFO];
for ( var index in playerInfos) {
@ -943,8 +945,17 @@ cah.Game.prototype.confirmClick_ = function() {
}
} else {
if (this.handSelectedCard_ != null) {
cah.Ajax.build(cah.$.AjaxOperation.PLAY_CARD).withGameId(this.id_).withCardId(
if (this.handSelectedCard_.isBlankCard()) {
// blank card
var text = prompt("What would you like this card to say?", "");
if (text == null || text == '') { return; }
text = $("<div/>").text(text).html(); // html sanitise
this.handSelectedCard_.setText(text);
cah.Ajax.build(cah.$.AjaxOperation.PLAY_CARD).withGameId(this.id_).withCardId(0).withMessage(text).run();
} else {
cah.Ajax.build(cah.$.AjaxOperation.PLAY_CARD).withGameId(this.id_).withCardId(
this.handSelectedCard_.getServerId()).run();
}
}
}
};
@ -1246,7 +1257,8 @@ cah.Game.prototype.optionChanged_ = function(e) {
cah.Ajax.build(cah.$.AjaxOperation.CHANGE_GAME_OPTIONS).withGameId(this.id_).withScoreLimit(
$(".score_limit", this.optionsElement_).val()).withPlayerLimit(
$(".player_limit", this.optionsElement_).val()).withCardSets(cardSetIds).withPassword(
$(".game_password", this.optionsElement_).val()).withUseTimer(
$(".game_password", this.optionsElement_).val()).withBlanksLimit(
$(".blanks_limit", this.optionsElement_).val()).withUseTimer(
!!$('.use_timer', this.optionsElement_).attr('checked')).run();
};

View File

@ -209,6 +209,7 @@ public class Constants {
PASSWORD("pw"),
PLAYER_LIMIT("pL"),
SCORE_LIMIT("sl"),
BLANKS_LIMIT("bl"),
SERIAL("s"),
USE_TIMER("ut"),
WALL("wall");
@ -597,6 +598,8 @@ public class Constants {
PLAYERS("P"),
@DuplicationAllowed
SCORE_LIMIT(AjaxRequest.SCORE_LIMIT),
@DuplicationAllowed
BLANKS_LIMIT(AjaxRequest.BLANKS_LIMIT),
STATE("S"),
@DuplicationAllowed
USE_TIMER(AjaxRequest.USE_TIMER);

View File

@ -100,6 +100,7 @@ public class Game {
private final Object blackCardLock = new Object();
private WhiteDeck whiteDeck;
private GameState state;
private int maxBlanks = 0;
private int maxPlayers = 6;
private int judgeIndex = 0;
private final static int ROUND_INTERMISSION = 8 * 1000;
@ -366,13 +367,15 @@ public class Game {
}
public void updateGameSettings(final int newScoreGoal, final int newMaxPlayers,
final Set<CardSet> newCardSets, final String newPassword, final boolean newUseTimer) {
final Set<CardSet> newCardSets, final int newMaxBlanks, final String newPassword,
final boolean newUseTimer) {
this.scoreGoal = newScoreGoal;
this.maxPlayers = newMaxPlayers;
synchronized (this.cardSets) {
this.cardSets.clear();
this.cardSets.addAll(newCardSets);
}
this.maxBlanks = newMaxBlanks;
this.password = newPassword;
this.useTimer = newUseTimer;
@ -421,6 +424,7 @@ public class Game {
}
}
info.put(GameInfo.CARD_SETS, cardSetIds);
info.put(GameInfo.BLANKS_LIMIT, maxBlanks);
info.put(GameInfo.PLAYER_LIMIT, maxPlayers);
info.put(GameInfo.SCORE_LIMIT, scoreGoal);
info.put(GameInfo.USE_TIMER, useTimer);
@ -564,7 +568,7 @@ public class Game {
// time, and not at the same time as trying to lock users, which has caused deadlocks
synchronized (cardSets) {
blackDeck = new BlackDeck(cardSets);
whiteDeck = new WhiteDeck(cardSets);
whiteDeck = new WhiteDeck(cardSets, maxBlanks);
}
startNextRound();
gameManager.broadcastGameListRefresh();
@ -1187,11 +1191,13 @@ public class Game {
* User playing the card.
* @param cardId
* ID of the card to play.
* @param cardText
* User text for a blank card. Ignored for normal cards.
* @return An {@code ErrorCode} if the play was unsuccessful ({@code user} doesn't have the card,
* {@code user} is the judge, etc.), or {@code null} if there was no error and the play
* was successful.
*/
public ErrorCode playCard(final User user, final int cardId) {
public ErrorCode playCard(final User user, final int cardId, final String cardText) {
final Player player = getPlayerForUser(user);
if (player != null) {
player.resetSkipCount();
@ -1206,6 +1212,11 @@ public class Game {
final WhiteCard card = iter.next();
if (card.getId() == cardId) {
playCard = card;
if (WhiteDeck.isBlankCard(card)) {
playCard.setText(cardText);
// note that since blank cards are indistinguishable to the server, we might end up
// removing a different card than the client did. but this shouldn't break anything.
}
// remove the card from their hand. the client will also do so when we return
// success, so no need to tell it to do so here.
iter.remove();

View File

@ -49,12 +49,15 @@ public class WhiteDeck {
/**
* Create a new white card deck, loading the cards from the database and shuffling them.
*/
public WhiteDeck(final Set<CardSet> cardSets) {
public WhiteDeck(final Set<CardSet> cardSets, final int numBlanks) {
final Set<WhiteCard> allCards = new HashSet<WhiteCard>();
for (final CardSet cardSet : cardSets) {
allCards.addAll(cardSet.getWhiteCards());
}
deck = new ArrayList<WhiteCard>(allCards);
for (int i = 0; i < numBlanks; i++) {
deck.add(createBlankCard());
}
Collections.shuffle(deck);
dealt = new LinkedList<WhiteCard>();
discard = new ArrayList<WhiteCard>(deck.size());
@ -85,7 +88,12 @@ public class WhiteDeck {
*/
public synchronized void discard(final WhiteCard card) {
if (card != null) {
discard.add(card);
if (isBlankCard(card)) {
// create a fresh blank card to ensure player text is cleared
discard.add(createBlankCard());
} else {
discard.add(card);
}
}
}
@ -97,4 +105,28 @@ public class WhiteDeck {
deck.addAll(0, discard);
discard.clear();
}
/**
* Creates a new blank card.
*
* @return A newly created blank card.
*/
private WhiteCard createBlankCard() {
final WhiteCard blank = new WhiteCard();
blank.setId(0);
blank.setText("____");
blank.setWatermark("____");
return blank;
}
/**
* Checks if a particular card is a blank card.
*
* @param card
* Card to check.
* @return True if the card is a blank card.
*/
public static boolean isBlankCard(final WhiteCard card) {
return card.getId() == 0;
}
}

View File

@ -51,6 +51,7 @@ public class ChangeGameOptionHandler extends GameWithPlayerHandler {
Integer.parseInt(cardSetId)));
}
}
final int blanksLimit = Integer.parseInt(request.getParameter(AjaxRequest.BLANKS_LIMIT));
String password = request.getParameter(AjaxRequest.PASSWORD);
if (password == null) {
password = "";
@ -62,7 +63,7 @@ public class ChangeGameOptionHandler extends GameWithPlayerHandler {
if (null != useTimerString && !"".equals(useTimerString)) {
useTimer = Boolean.valueOf(useTimerString);
}
game.updateGameSettings(scoreLimit, playerLimit, cardSets, password, useTimer);
game.updateGameSettings(scoreLimit, playerLimit, cardSets, blanksLimit, password, useTimer);
} catch (final NumberFormatException nfe) {
return error(ErrorCode.BAD_REQUEST);
}

View File

@ -37,6 +37,8 @@ import net.socialgamer.cah.data.Game;
import net.socialgamer.cah.data.GameManager;
import net.socialgamer.cah.data.User;
import org.apache.commons.lang3.StringEscapeUtils;
import com.google.inject.Inject;
@ -69,8 +71,13 @@ public class PlayCardHandler extends GameWithPlayerHandler {
} catch (final NumberFormatException nfe) {
return error(ErrorCode.INVALID_CARD);
}
String text = request.getParameter(AjaxRequest.MESSAGE);
if (text != null && text.contains("<")) {
// somebody must be using a hacked client, because this should have been escaped already.
text = StringEscapeUtils.escapeXml(text);
}
final ErrorCode ec = game.playCard(user, cardId);
final ErrorCode ec = game.playCard(user, cardId, text);
if (ec != null) {
return error(ec);
} else {