diff --git a/WebContent/index.jsp b/WebContent/index.jsp
index f7e1980..b00055c 100644
--- a/WebContent/index.jsp
+++ b/WebContent/index.jsp
@@ -51,6 +51,18 @@ to, for instance, display the number of connected players.
The name you enter and your computer's IP address will always be logged when you
load the game client. Chat and gameplay may also be logged.
+Recent Changes:
+
+ - 17 March, Midnight UTC:
+ - Initial AFK timer support added. This will skip (or kick, if there are not enough players) a
+ player that takes longer than 15 + 15 * PICK seconds to play, or skip a judge that takes longer
+ than 20 + 5 * PICK * PLAYERS seconds to select a winner. If a player is idle for two consecutive
+ rounds, they will be kicked from the game. All of these numbers are adjustable; if the timeouts
+ are too long or too short, please let me know!
+ - The game host can specify the Awesome Point goal from 4 to 10.
+ - The game host can specify the maximum number of players in a game from 3 to 10.
+
+
Known issues:
- Do not open the game more than once in the same browser. Neither instances
@@ -80,7 +92,7 @@ to, for instance, display the number of connected players.
game state until the next round begins.
- Reloading the page when the winning card is displayed does not display the winning card
again.
- - Played cards seem to blank when someone joins (or leaves?). You may have to refresh the page
+
- Played cards seem to blank when someone joins or leaves. You may have to refresh the page
to see the cards again if you're the Card Czar.
Current limitations:
@@ -100,17 +112,10 @@ to, for instance, display the number of connected players.
All games and the main lobby share the same chat.
- There is no play timer to keep the game moving if one person goes idle. However, if their
- browser crashes or they lose connection, they will be removed from the game after approximately 45
- seconds. An AFK timer is near the top of the priority list to add.
- The first player to 8 Awesome Points wins. This is currently hard-coded, but you will be able
- to change it later.
You can't bet Awesome Points to play another card, and I am unsure if I will add this.
Future enhancements:
- - There will be host options to limit the number of players and set the target score soon.
- - There will be a timer to keep the game moving if somebody goes AFK soon.
- There may be an option to display who played every card.
- A registration system and long-term statistics tracking may be added at some point.
- Support for custom Black and White cards will also likely be added, with a game host option to
diff --git a/WebContent/js/cah.constants.js b/WebContent/js/cah.constants.js
index 8aa583f..9d56f45 100644
--- a/WebContent/js/cah.constants.js
+++ b/WebContent/js/cah.constants.js
@@ -201,6 +201,9 @@ cah.$.LongPollEvent = function() {
};
cah.$.LongPollEvent.prototype.dummyForAutocomplete = undefined;
cah.$.LongPollEvent.KICKED = "k";
+cah.$.LongPollEvent.HURRY_UP = "hu";
+cah.$.LongPollEvent.KICKED_FROM_GAME_IDLE = "kfgi";
+cah.$.LongPollEvent.GAME_JUDGE_SKIPPED = "gjs";
cah.$.LongPollEvent.GAME_PLAYER_LEAVE = "gpl";
cah.$.LongPollEvent.NEW_PLAYER = "np";
cah.$.LongPollEvent.GAME_PLAYER_JOIN = "gpj";
@@ -208,13 +211,15 @@ cah.$.LongPollEvent.GAME_LIST_REFRESH = "glr";
cah.$.LongPollEvent.GAME_ROUND_COMPLETE = "grc";
cah.$.LongPollEvent.NOOP = "_";
cah.$.LongPollEvent.GAME_PLAYER_INFO_CHANGE = "gpic";
+cah.$.LongPollEvent.GAME_PLAYER_KICKED_IDLE = "gpki";
cah.$.LongPollEvent.GAME_BLACK_RESHUFFLE = "gbr";
cah.$.LongPollEvent.GAME_WHITE_RESHUFFLE = "gwr";
cah.$.LongPollEvent.GAME_STATE_CHANGE = "gsc";
cah.$.LongPollEvent.GAME_OPTIONS_CHANGED = "goc";
+cah.$.LongPollEvent.GAME_PLAYER_SKIPPED = "gps";
cah.$.LongPollEvent.PLAYER_LEAVE = "pl";
-cah.$.LongPollEvent.CHAT = "c";
cah.$.LongPollEvent.HAND_DEAL = "hd";
+cah.$.LongPollEvent.CHAT = "c";
cah.$.LongPollEvent.GAME_JUDGE_LEFT = "gjl";
cah.$.LongPollResponse = function() {
@@ -222,8 +227,8 @@ cah.$.LongPollResponse = function() {
};
cah.$.LongPollResponse.prototype.dummyForAutocomplete = undefined;
cah.$.LongPollResponse.WHITE_CARDS = "wc";
-cah.$.LongPollResponse.GAME_ID = "gid";
cah.$.LongPollResponse.REASON = "qr";
+cah.$.LongPollResponse.GAME_ID = "gid";
cah.$.LongPollResponse.HAND = "h";
cah.$.LongPollResponse.INTERMISSION = "i";
cah.$.LongPollResponse.PLAYER_INFO = "pi";
@@ -231,6 +236,7 @@ cah.$.LongPollResponse.BLACK_CARD = "bc";
cah.$.LongPollResponse.WINNING_CARD = "WC";
cah.$.LongPollResponse.GAME_STATE = "gs";
cah.$.LongPollResponse.NICKNAME = "n";
+cah.$.LongPollResponse.PLAY_TIMER = "pt";
cah.$.LongPollResponse.MESSAGE = "m";
cah.$.LongPollResponse.ERROR = "e";
cah.$.LongPollResponse.EVENT = "E";
diff --git a/WebContent/js/cah.game.js b/WebContent/js/cah.game.js
index 285c973..13556b7 100644
--- a/WebContent/js/cah.game.js
+++ b/WebContent/js/cah.game.js
@@ -749,6 +749,42 @@ cah.Game.prototype.roundComplete = function(data) {
$(".game_show_last_round", this.element_).removeAttr("disabled");
};
+/**
+ * Notify the user that they are running out of time to play.
+ */
+cah.Game.prototype.hurryUp = function() {
+ cah.log.status("Hurry up! You have less than 10 seconds to decide, or you will be skipped.");
+};
+
+/**
+ * A player was kicked due to being idle.
+ *
+ * @param {object}
+ * data Event data from server.
+ */
+cah.Game.prototype.playerKickedIdle = function(data) {
+ cah.log.status(data[cah.$.LongPollResponse.NICKNAME]
+ + " was kicked for being idle for too many rounds.");
+};
+
+/**
+ * A player was skipped due to being idle.
+ *
+ * @param {obejct}
+ * data Event data from server.
+ */
+cah.Game.prototype.playerSkipped = function(data) {
+ cah.log.status(data[cah.$.LongPollResponse.NICKNAME]
+ + " was skipped this round for being idle for too long.");
+};
+
+/**
+ * This player was kicked due to being idle.
+ */
+cah.Game.prototype.iWasKickedIdle = function() {
+
+};
+
/**
* Notify the player that a deck has been reshuffled.
*
@@ -766,13 +802,21 @@ cah.Game.prototype.reshuffle = function(deck) {
* data Event data from the server.
*/
cah.Game.prototype.judgeLeft = function(data) {
- cah.log
- .status("The judge has left the game. Cards played this round are being returned to hands.");
+ cah.log.status("The Card Czar has left the game. Cards played this round are being returned to "
+ + "hands.");
cah.log.status("The next round will begin in "
+ (data[cah.$.LongPollResponse.INTERMISSION] / 1000) + " seconds.");
cah.log.status("(Displayed state will look weird until the next round.)");
};
+/**
+ * The judge was skipped for taking too long.
+ */
+cah.Game.prototype.judgeSkipped = function() {
+ cah.log.status("The Card Czar has taken too long to decide and has been skipped. "
+ + "Cards played this round are being returned to hands.");
+};
+
/**
* Event handler for confirm selection button.
*
diff --git a/WebContent/js/cah.longpoll.handlers.js b/WebContent/js/cah.longpoll.handlers.js
index 3b4a757..71f1741 100644
--- a/WebContent/js/cah.longpoll.handlers.js
+++ b/WebContent/js/cah.longpoll.handlers.js
@@ -133,6 +133,37 @@ cah.longpoll.EventHandlers[cah.$.LongPollEvent.GAME_OPTIONS_CHANGED] = function(
"options changed");
};
+cah.longpoll.EventHandlers[cah.$.LongPollEvent.HURRY_UP] = function(data) {
+ cah.longpoll.EventHandlers.__gameEvent(data, cah.Game.prototype.hurryUp, "", "hurry up");
+};
+
+cah.longpoll.EventHandlers[cah.$.LongPollEvent.GAME_PLAYER_KICKED_IDLE] = function(data) {
+ cah.longpoll.EventHandlers.__gameEvent(data, cah.Game.prototype.playerKickedIdle, data,
+ "idle kick");
+};
+
+cah.longpoll.EventHandlers[cah.$.LongPollEvent.GAME_PLAYER_SKIPPED] = function(data) {
+ cah.longpoll.EventHandlers.__gameEvent(data, cah.Game.prototype.playerSkipped, data,
+ "player skip");
+};
+
+cah.longpoll.EventHandlers[cah.$.LongPollEvent.GAME_JUDGE_SKIPPED] = function(data) {
+ cah.longpoll.EventHandlers.__gameEvent(data, cah.Game.prototype.judgeSkipped, "", "judge skip");
+};
+
+cah.longpoll.EventHandlers[cah.$.LongPollEvent.KICKED_FROM_GAME_IDLE] = function(data) {
+ var game = cah.currentGames[data[cah.$.LongPollResponse.GAME_ID]];
+ if (game) {
+ game.dispose();
+ delete cah.currentGames[data[cah.$.LongPollResponse.GAME_ID]];
+ }
+ cah.GameList.instance.update();
+ cah.GameList.instance.show();
+
+ cah.log.error("You were kicked from game " + data[cah.$.LongPollResponse.GAME_ID]
+ + " for being idle for too long.");
+};
+
/**
* Helper for event handlers for game events.
*
diff --git a/src/net/socialgamer/cah/Constants.java b/src/net/socialgamer/cah/Constants.java
index c2d37a6..1805e1a 100644
--- a/src/net/socialgamer/cah/Constants.java
+++ b/src/net/socialgamer/cah/Constants.java
@@ -317,17 +317,22 @@ public class Constants {
CHAT(AjaxOperation.CHAT),
GAME_BLACK_RESHUFFLE("gbr"),
GAME_JUDGE_LEFT("gjl"),
+ GAME_JUDGE_SKIPPED("gjs"),
GAME_LIST_REFRESH("glr"),
GAME_OPTIONS_CHANGED("goc"),
GAME_PLAYER_INFO_CHANGE("gpic"),
GAME_PLAYER_JOIN("gpj"),
+ GAME_PLAYER_KICKED_IDLE("gpki"),
GAME_PLAYER_LEAVE("gpl"),
+ GAME_PLAYER_SKIPPED("gps"),
GAME_ROUND_COMPLETE("grc"),
GAME_STATE_CHANGE("gsc"),
GAME_WHITE_RESHUFFLE("gwr"),
HAND_DEAL("hd"),
+ HURRY_UP("hu"),
@DuplicationAllowed
KICKED(DisconnectReason.KICKED),
+ KICKED_FROM_GAME_IDLE("kfgi"),
NEW_PLAYER("np"),
/**
* There has been no other action to inform the client about in a certain timeframe, so inform
@@ -380,6 +385,7 @@ public class Constants {
MESSAGE(AjaxRequest.MESSAGE),
@DuplicationAllowed
NICKNAME(AjaxRequest.NICKNAME),
+ PLAY_TIMER("pt"),
@DuplicationAllowed
PLAYER_INFO(AjaxResponse.PLAYER_INFO),
/**
diff --git a/src/net/socialgamer/cah/data/Game.java b/src/net/socialgamer/cah/data/Game.java
index ff1638d..899ce9d 100644
--- a/src/net/socialgamer/cah/data/Game.java
+++ b/src/net/socialgamer/cah/data/Game.java
@@ -94,6 +94,30 @@ public class Game {
private int maxPlayers = 10;
private int judgeIndex = 0;
private final static int ROUND_INTERMISSION = 8 * 1000;
+ /**
+ * Duration, in milliseconds, for the minimum timeout a player has to choose a card to play.
+ * Minimum 10 seconds.
+ */
+ private final static int PLAY_TIMEOUT_BASE = 30 * 1000;
+ /**
+ * Duration, in milliseconds, for the additional timeout a player has to choose a card to play,
+ * for each additional card that must be played. For example, on a PICK 2 card, this amount of
+ * time is added to {@code PLAY_TIMEOUT_BASE}.
+ */
+ private final static int PLAY_TIMEOUT_PER_CARD = 15 * 1000;
+ /**
+ * Duration, in milliseconds, for the minimum timeout a judge has to choose a winner.
+ * Minimum combined of this and 2 * {@code JUDGE_TIMEOUT_PER_CARD} is 10 seconds.
+ */
+ private final static int JUDGE_TIMEOUT_BASE = 20 * 1000;
+ /**
+ * Duration, in milliseconds, for the additional timeout a judge has to choose a winning card,
+ * for each additional card that was played in the round. For example, on a PICK 2 card with
+ * 3 non-judge players, 6 times this value is added to {@code JUDGE_TIMEOUT_BASE}.
+ */
+ private final static int JUDGE_TIMEOUT_PER_CARD = 5 * 1000;
+ private final static int MAX_SKIPS_BEFORE_KICK = 2;
+ private final Object judgeLock = new Object();
private Timer nextRoundTimer;
private final Object nextRoundTimerLock = new Object();
private int scoreGoal = 8;
@@ -202,14 +226,7 @@ public class Game {
data.put(LongPollResponse.EVENT, LongPollEvent.GAME_JUDGE_LEFT.toString());
data.put(LongPollResponse.INTERMISSION, ROUND_INTERMISSION);
broadcastToPlayers(MessageType.GAME_EVENT, data);
- synchronized (playedCards) {
- for (final Player p : playedCards.playedPlayers()) {
- p.getHand().addAll(playedCards.getCards(p));
- sendCardsToPlayer(p, playedCards.getCards(p));
- }
- // prevent startNextRound from discarding cards
- playedCards.clear();
- }
+ returnCardsToHand();
// startNextRound will advance it again.
judgeIndex--;
// Can't start the next round right here.
@@ -252,13 +269,13 @@ public class Game {
resetState(true);
} else if (wasJudge) {
synchronized (nextRoundTimerLock) {
- nextRoundTimer = new Timer();
final TimerTask task = new TimerTask() {
@Override
public void run() {
startNextRound();
}
};
+ nextRoundTimer = new Timer("judge-left-" + id, true);
nextRoundTimer.schedule(task, ROUND_INTERMISSION);
}
}
@@ -266,6 +283,17 @@ public class Game {
}
}
+ private void returnCardsToHand() {
+ synchronized (playedCards) {
+ for (final Player p : playedCards.playedPlayers()) {
+ p.getHand().addAll(playedCards.getCards(p));
+ sendCardsToPlayer(p, playedCards.getCards(p));
+ }
+ // prevent startNextRound from discarding cards
+ playedCards.clear();
+ }
+ }
+
/**
* Broadcast a message to all players in this game.
*
@@ -520,30 +548,203 @@ public class Game {
}
}
+ final int playTimer = PLAY_TIMEOUT_BASE + (PLAY_TIMEOUT_PER_CARD * (blackCard.getPick() - 1));
+
final HashMap data = getEventMap();
data.put(LongPollResponse.EVENT, LongPollEvent.GAME_STATE_CHANGE.toString());
data.put(LongPollResponse.BLACK_CARD, getBlackCard());
data.put(LongPollResponse.GAME_STATE, GameState.PLAYING.toString());
+ data.put(LongPollResponse.PLAY_TIMER, playTimer);
broadcastToPlayers(MessageType.GAME_EVENT, data);
+
+ synchronized (nextRoundTimerLock) {
+ killRoundTimer();
+ final TimerTask task = new TimerTask() {
+ @Override
+ public void run() {
+ warnPlayersToPlay();
+ }
+ };
+ // 10 second warning
+ nextRoundTimer = new Timer("hurry-up-" + id, true);
+ nextRoundTimer.schedule(task, playTimer - 10 * 1000);
+ }
+ }
+
+ private void warnPlayersToPlay() {
+ // have to do this all synchronized in case they play while we're processing this
+ synchronized (nextRoundTimerLock) {
+ killRoundTimer();
+
+ synchronized (players) {
+ for (final Player player : players) {
+ if (getJudge() == player) {
+ continue;
+ }
+ synchronized (playedCards) {
+ if (!playedCards.hasPlayer(player)) {
+ final Map data = new HashMap();
+ data.put(LongPollResponse.EVENT, LongPollEvent.HURRY_UP.toString());
+ data.put(LongPollResponse.GAME_ID, this.id);
+ final QueuedMessage q = new QueuedMessage(MessageType.GAME_EVENT, data);
+ player.getUser().enqueueMessage(q);
+ }
+ }
+ }
+ }
+
+ final TimerTask task = new TimerTask() {
+ @Override
+ public void run() {
+ skipIdlePlayers();
+ }
+ };
+ // 10 seconds to finish playing
+ nextRoundTimer = new Timer("hurry-up-" + id, true);
+ nextRoundTimer.schedule(task, 10 * 1000);
+ }
+ }
+
+ private void warnJudgeToJudge() {
+ // have to do this all synchronized in case they play while we're processing this
+ synchronized (nextRoundTimerLock) {
+ killRoundTimer();
+
+ if (state == GameState.JUDGING) {
+ final Map data = new HashMap();
+ data.put(LongPollResponse.EVENT, LongPollEvent.HURRY_UP.toString());
+ data.put(LongPollResponse.GAME_ID, this.id);
+ final QueuedMessage q = new QueuedMessage(MessageType.GAME_EVENT, data);
+ getJudge().getUser().enqueueMessage(q);
+ }
+
+ final TimerTask task = new TimerTask() {
+ @Override
+ public void run() {
+ skipIdleJudge();
+ }
+ };
+ // 10 seconds to finish playing
+ nextRoundTimer = new Timer("hurry-up-" + id, true);
+ nextRoundTimer.schedule(task, 10 * 1000);
+ }
+ }
+
+ private void skipIdleJudge() {
+ killRoundTimer();
+ synchronized (judgeLock) {
+ if (state != GameState.JUDGING) {
+ return;
+ }
+ getJudge().skipped();
+ final HashMap data = getEventMap();
+ data.put(LongPollResponse.EVENT, LongPollEvent.GAME_JUDGE_SKIPPED.toString());
+ broadcastToPlayers(MessageType.GAME_EVENT, data);
+ returnCardsToHand();
+ startNextRound();
+ }
+ }
+
+ private void skipIdlePlayers() {
+ killRoundTimer();
+ synchronized (players) {
+ final List playersToRemove = new ArrayList();
+ final List playersToUpdateStatus = new ArrayList();
+
+ for (final Player player : players) {
+ if (getJudge() == player) {
+ continue;
+ }
+ synchronized (playedCards) {
+ if (!playedCards.hasPlayer(player)) {
+ player.skipped();
+ final HashMap data = getEventMap();
+
+ if (player.getSkipCount() >= MAX_SKIPS_BEFORE_KICK || playedCards.size() < 2) {
+ data.put(LongPollResponse.EVENT, LongPollEvent.GAME_PLAYER_KICKED_IDLE.toString());
+ data.put(LongPollResponse.NICKNAME, player.getUser().getNickname());
+ playersToRemove.add(player.getUser());
+ } else {
+ data.put(LongPollResponse.EVENT, LongPollEvent.GAME_PLAYER_SKIPPED.toString());
+ data.put(LongPollResponse.NICKNAME, player.getUser().getNickname());
+ playersToUpdateStatus.add(player);
+ }
+ broadcastToPlayers(MessageType.GAME_EVENT, data);
+ }
+ }
+ }
+
+ for (final User user : playersToRemove) {
+ removePlayer(user);
+ final HashMap data = getEventMap();
+ data.put(LongPollResponse.EVENT, LongPollEvent.KICKED_FROM_GAME_IDLE.toString());
+ final QueuedMessage q = new QueuedMessage(MessageType.GAME_PLAYER_EVENT, data);
+ user.enqueueMessage(q);
+ }
+
+ synchronized (playedCards) {
+ // not sure how much of this check is actually required
+ if (players.size() < 3 || playedCards.size() < 2 || state != GameState.PLAYING) {
+ resetState(true);
+ } else {
+ judgingState();
+ }
+ }
+
+ // have to do this after we move to judging state
+ for (final Player player : playersToUpdateStatus) {
+ final HashMap data = getEventMap();
+ data.put(LongPollResponse.EVENT, LongPollEvent.GAME_PLAYER_INFO_CHANGE.toString());
+ data.put(LongPollResponse.PLAYER_INFO, getPlayerInfo(player));
+ broadcastToPlayers(MessageType.GAME_PLAYER_EVENT, data);
+ }
+ }
+ }
+
+ private void killRoundTimer() {
+ synchronized (nextRoundTimerLock) {
+ if (nextRoundTimer != null) {
+ nextRoundTimer.cancel();
+ nextRoundTimer = null;
+ }
+ }
}
/**
* Move the game into the {@code JUDGING} state.
*/
private void judgingState() {
+ killRoundTimer();
state = GameState.JUDGING;
+ final int judgeTimer = JUDGE_TIMEOUT_BASE
+ + (JUDGE_TIMEOUT_PER_CARD * playedCards.size() * blackCard.getPick());
+
HashMap data = getEventMap();
data.put(LongPollResponse.EVENT, LongPollEvent.GAME_STATE_CHANGE.toString());
data.put(LongPollResponse.GAME_STATE, GameState.JUDGING.toString());
data.put(LongPollResponse.WHITE_CARDS, getWhiteCards());
+ data.put(LongPollResponse.PLAY_TIMER, judgeTimer);
broadcastToPlayers(MessageType.GAME_EVENT, data);
data = getEventMap();
data.put(LongPollResponse.EVENT, LongPollEvent.GAME_PLAYER_INFO_CHANGE.toString());
data.put(LongPollResponse.PLAYER_INFO, getPlayerInfo(getJudge()));
broadcastToPlayers(MessageType.GAME_PLAYER_EVENT, data);
+
+ synchronized (nextRoundTimerLock) {
+ killRoundTimer();
+ final TimerTask task = new TimerTask() {
+ @Override
+ public void run() {
+ warnJudgeToJudge();
+ }
+ };
+ // 10 second warning
+ nextRoundTimer = new Timer("hurry-up-" + id, true);
+ nextRoundTimer.schedule(task, judgeTimer - 10 * 1000);
+ }
}
/**
@@ -889,6 +1090,7 @@ public class Game {
public ErrorCode playCard(final User user, final int cardId) {
final Player player = getPlayerForUser(user);
if (player != null) {
+ player.resetSkipCount();
if (getJudge() == player || state != GameState.PLAYING) {
return ErrorCode.NOT_YOUR_TURN;
}
@@ -941,23 +1143,27 @@ public class Game {
* @return Error code if there is an error, or null if success.
*/
public ErrorCode judgeCard(final User user, final int cardId) {
- final Player player = getPlayerForUser(user);
- if (getJudge() != player) {
- return ErrorCode.NOT_JUDGE;
- } else if (state != GameState.JUDGING) {
- return ErrorCode.NOT_YOUR_TURN;
- }
-
final Player cardPlayer;
- synchronized (playedCards) {
- cardPlayer = playedCards.getPlayerForId(cardId);
- }
- if (cardPlayer == null) {
- return ErrorCode.INVALID_CARD;
- }
+ synchronized (judgeLock) {
+ final Player player = getPlayerForUser(user);
+ if (getJudge() != player) {
+ return ErrorCode.NOT_JUDGE;
+ } else if (state != GameState.JUDGING) {
+ return ErrorCode.NOT_YOUR_TURN;
+ }
- cardPlayer.increaseScore();
- state = GameState.ROUND_OVER;
+ player.resetSkipCount();
+
+ synchronized (playedCards) {
+ cardPlayer = playedCards.getPlayerForId(cardId);
+ }
+ if (cardPlayer == null) {
+ return ErrorCode.INVALID_CARD;
+ }
+
+ cardPlayer.increaseScore();
+ state = GameState.ROUND_OVER;
+ }
final int clientCardId = playedCards.getCards(cardPlayer).get(0).getId();
HashMap data = getEventMap();
@@ -978,7 +1184,7 @@ public class Game {
broadcastToPlayers(MessageType.GAME_PLAYER_EVENT, data);
synchronized (nextRoundTimerLock) {
- nextRoundTimer = new Timer();
+ killRoundTimer();
final TimerTask task;
// TODO win-by-x option
if (cardPlayer.getScore() == scoreGoal) {
@@ -996,6 +1202,7 @@ public class Game {
}
};
}
+ nextRoundTimer = new Timer("round-intermission-" + id, true);
nextRoundTimer.schedule(task, ROUND_INTERMISSION);
}
diff --git a/src/net/socialgamer/cah/data/Player.java b/src/net/socialgamer/cah/data/Player.java
index 6471134..78e63b0 100644
--- a/src/net/socialgamer/cah/data/Player.java
+++ b/src/net/socialgamer/cah/data/Player.java
@@ -39,6 +39,7 @@ public class Player {
private final List hand = new LinkedList();
private int score = 0;
+ private int skipCount = 0;
/**
* Create a new player object.
@@ -78,6 +79,27 @@ public class Player {
score = 0;
}
+ /**
+ * Increases this player's skipped round count.
+ */
+ public void skipped() {
+ skipCount++;
+ }
+
+ /**
+ * Reset this player's skipped round count to 0, because they have been back for a round.
+ */
+ public void resetSkipCount() {
+ skipCount = 0;
+ }
+
+ /**
+ * @return This player's skipped round count.
+ */
+ public int getSkipCount() {
+ return skipCount;
+ }
+
/**
* @return The backing object for the player's hand (i.e., it can be modified).
*/