diff --git a/WebContent/game.jsp b/WebContent/game.jsp
index 864bcdb3..6d0a263e 100644
--- a/WebContent/game.jsp
+++ b/WebContent/game.jsp
@@ -411,7 +411,7 @@ HttpSession hSession = request.getSession(true);
aria-label="Use idle timer. Players will be skipped if they have not played within a reasonable amount of time."/>
- Use idle timer.
+ Use idle timer. (Or the host can /skip
or /kick
idle players manually.)
diff --git a/WebContent/js/cah.ajax.handlers.js b/WebContent/js/cah.ajax.handlers.js
index 58b8f756..3d5647f9 100644
--- a/WebContent/js/cah.ajax.handlers.js
+++ b/WebContent/js/cah.ajax.handlers.js
@@ -264,3 +264,11 @@ cah.ajax.SuccessHandlers[cah.$.AjaxOperation.SCORE] = function(data, req) {
cah.log.status(msg);
}
};
+
+cah.ajax.SuccessHandlers[cah.$.AjaxOperation.GAME_HOST] = function(data) {
+ // pass
+};
+
+cah.ajax.SuccessHandlers[cah.$.AjaxOperation.GAME_SKIP] = function(data) {
+ // pass
+};
diff --git a/WebContent/js/cah.app.js b/WebContent/js/cah.app.js
index 257d5122..e2db54e7 100644
--- a/WebContent/js/cah.app.js
+++ b/WebContent/js/cah.app.js
@@ -173,6 +173,20 @@ function chatsubmit_click(game_id, parent_element) {
// this could also be an IP address
ajax = cah.Ajax.build(cah.$.AjaxOperation.BAN).withNickname(text.split(' ')[0]);
break;
+ case 'host':
+ if (game_id !== null) {
+ ajax = cah.Ajax.build(cah.$.AjaxOperation.GAME_HOST).withGameId(game_id).withNickname(text.split(' ')[0]);
+ } else {
+ cah.log.error(cah.$.ErrorCode_msg[cah.$.ErrorCode.NO_GAME_SPECIFIED]);
+ }
+ break;
+ case 'skip':
+ if (game_id !== null) {
+ ajax = cah.Ajax.build(cah.$.AjaxOperation.GAME_SKIP).withGameId(game_id).withNickname(text.split(' ')[0]);
+ } else {
+ cah.log.error(cah.$.ErrorCode_msg[cah.$.ErrorCode.NO_GAME_SPECIFIED]);
+ }
+ break;
case 'sync':
if (game_id !== null) {
var game = cah.currentGames[game_id];
@@ -181,12 +195,12 @@ function chatsubmit_click(game_id, parent_element) {
}
ajax = cah.Ajax.build(cah.$.AjaxOperation.GET_CARDS).withGameId(game_id);
} else {
- cah.log.error("This command only works in a game.");
+ cah.log.error(cah.$.ErrorCode_msg[cah.$.ErrorCode.NO_GAME_SPECIFIED]);
}
break;
case 'score':
ajax = cah.Ajax.build(cah.$.AjaxOperation.SCORE).withMessage(text);
- if (game_id != null) {
+ if (game_id !== null) {
ajax = ajax.withGameId(game_id);
}
break;
diff --git a/WebContent/js/cah.constants.js b/WebContent/js/cah.constants.js
index f2656d21..9522b5ef 100644
--- a/WebContent/js/cah.constants.js
+++ b/WebContent/js/cah.constants.js
@@ -18,12 +18,14 @@ cah.$.AjaxOperation.CHANGE_GAME_OPTIONS = "cgo";
cah.$.AjaxOperation.GET_GAME_INFO = "ggi";
cah.$.AjaxOperation.PLAY_CARD = "pc";
cah.$.AjaxOperation.CREATE_GAME = "cg";
+cah.$.AjaxOperation.GAME_HOST = "GH";
cah.$.AjaxOperation.KICK = "K";
cah.$.AjaxOperation.GAME_CHAT = "GC";
cah.$.AjaxOperation.ADMIN_SET_VERBOSE_LOG = "svl";
cah.$.AjaxOperation.GET_CARDS = "gc";
cah.$.AjaxOperation.JOIN_GAME = "jg";
cah.$.AjaxOperation.REGISTER = "r";
+cah.$.AjaxOperation.GAME_SKIP = "GS";
cah.$.AjaxOperation.STOP_GAME = "Sg";
cah.$.AjaxOperation.CHAT = "c";
cah.$.AjaxOperation.NAMES = "gn";
diff --git a/src/net/socialgamer/cah/Constants.java b/src/net/socialgamer/cah/Constants.java
index 2560d56a..59f277ac 100644
--- a/src/net/socialgamer/cah/Constants.java
+++ b/src/net/socialgamer/cah/Constants.java
@@ -169,7 +169,9 @@ public enum AjaxOperation {
CREATE_GAME("cg"),
FIRST_LOAD("fl"),
GAME_CHAT("GC"),
+ GAME_HOST("GH"),
GAME_LIST("ggl"),
+ GAME_SKIP("GS"),
/**
* Get all cards for a particular game: black, hand, and round white cards.
*/
diff --git a/src/net/socialgamer/cah/data/Game.java b/src/net/socialgamer/cah/data/Game.java
index f81dc56d..7c2aebe2 100644
--- a/src/net/socialgamer/cah/data/Game.java
+++ b/src/net/socialgamer/cah/data/Game.java
@@ -321,6 +321,34 @@ public void process() {
return false;
}
+ /**
+ * Change the game host to a different user.
+ *
+ * @param newHost
+ * The user that should be the new host.
+ * @return True if the host was changed successfully; false if not.
+ */
+ public boolean changeHost(final User user) {
+ final Player newHost = getPlayerForUser(user);
+ if (newHost == null) {
+ return false;
+ }
+
+ final Player oldHost = host;
+ if (oldHost == newHost) {
+ return true;
+ }
+ host = newHost;
+
+ if (oldHost != null) {
+ notifyPlayerInfoChange(oldHost);
+ }
+ notifyPlayerInfoChange(newHost);
+ notifyGameOptionsChanged();
+
+ return true;
+ }
+
/**
* Add a spectator to the game.
*
@@ -924,6 +952,68 @@ private void skipIdlePlayers() {
}
}
+ /**
+ * Skip an idle player, via an explicit request.
+ *
+ * @param target
+ * The player to be skipped.
+ * @return True if the player was skipped.
+ */
+ public boolean skipPlayer(final User target) {
+ final Player player = getPlayerForUser(target);
+ if (player == null) {
+ return false;
+ }
+
+ if (player == getJudge()) {
+ skipIdleJudge();
+ } else {
+ synchronized (roundPlayers) {
+ if (state != GameState.PLAYING) {
+ return false; // not playing
+ }
+
+ final List cards = playedCards.getCards(player);
+ if (cards != null && cards.size() >= blackCard.getPick()) {
+ return false; // player already played
+ }
+ if (!roundPlayers.contains(player)) {
+ return false; // player not in this round (newly joined or already skipped)
+ }
+
+ logger.info(String.format("Skipping player %s in game %d (requested).",
+ player.getUser().toString(), id));
+ //player.skipped(); // don't do this for the moment
+
+ final HashMap data = getEventMap();
+ data.put(LongPollResponse.NICKNAME, player.getUser().getNickname());
+ data.put(LongPollResponse.EVENT, LongPollEvent.GAME_PLAYER_SKIPPED.toString());
+ broadcastToPlayers(MessageType.GAME_EVENT, data);
+
+ // put their cards back
+ final List returnCards = playedCards.remove(player);
+ if (returnCards != null) {
+ player.getHand().addAll(returnCards);
+ sendCardsToPlayer(player, returnCards);
+ }
+ roundPlayers.remove(player);
+ }
+
+ notifyPlayerInfoChange(player);
+ if (startJudging()) {
+ judgingState();
+ } else if (roundPlayers.size() < 2) {
+ // not enough players left to judge
+ logger.info(String.format(
+ "Skipping judging on game %d due to insufficient played cards after manual skip.",
+ id));
+ returnCardsToHand();
+ startNextRound();
+ }
+ }
+ return true;
+ }
+
private void killRoundTimer() {
synchronized (roundTimerLock) {
if (null != lastScheduledFuture) {
diff --git a/src/net/socialgamer/cah/handlers/GameHostHandler.java b/src/net/socialgamer/cah/handlers/GameHostHandler.java
new file mode 100644
index 00000000..492f021a
--- /dev/null
+++ b/src/net/socialgamer/cah/handlers/GameHostHandler.java
@@ -0,0 +1,85 @@
+/**
+ * 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.ConnectedUsers;
+import net.socialgamer.cah.data.Game;
+import net.socialgamer.cah.data.GameManager;
+import net.socialgamer.cah.data.User;
+
+import com.google.inject.Inject;
+
+
+/**
+ * Handler for host change requests.
+ *
+ * @author Gavin Lambert (uecasm)
+ */
+public class GameHostHandler extends GameWithPlayerHandler {
+
+ public static final String OP = AjaxOperation.GAME_HOST.toString();
+
+ private final ConnectedUsers connectedUsers;
+
+ @Inject
+ public GameHostHandler(final ConnectedUsers connectedUsers, final GameManager gameManager) {
+ super(gameManager);
+ this.connectedUsers = connectedUsers;
+ }
+
+ @Override
+ public Map handleWithUserInGame(final RequestWrapper request,
+ final HttpSession session, final User user, final Game game) {
+ if (null == request.getParameter(AjaxRequest.NICKNAME)
+ || request.getParameter(AjaxRequest.NICKNAME).isEmpty()) {
+ return error(ErrorCode.NO_NICK_SPECIFIED);
+ }
+
+ // only an admin or the current host may change the host of a game
+ if (!user.isAdmin() && user != game.getHost()) {
+ return error(ErrorCode.NOT_GAME_HOST);
+ }
+
+ final User newHost = connectedUsers.getUser(request.getParameter(AjaxRequest.NICKNAME));
+ if (null == newHost) {
+ return error(ErrorCode.NO_SUCH_USER);
+ }
+
+ if (game.changeHost(newHost)) {
+ return new HashMap();
+ } else {
+ return error(ErrorCode.BAD_REQUEST);
+ }
+ }
+}
diff --git a/src/net/socialgamer/cah/handlers/GameSkipHandler.java b/src/net/socialgamer/cah/handlers/GameSkipHandler.java
new file mode 100644
index 00000000..a9c8cc2a
--- /dev/null
+++ b/src/net/socialgamer/cah/handlers/GameSkipHandler.java
@@ -0,0 +1,85 @@
+/**
+ * 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.ConnectedUsers;
+import net.socialgamer.cah.data.Game;
+import net.socialgamer.cah.data.GameManager;
+import net.socialgamer.cah.data.User;
+
+import com.google.inject.Inject;
+
+
+/**
+ * Handler for player skip requests.
+ *
+ * @author Gavin Lambert (uecasm)
+ */
+public class GameSkipHandler extends GameWithPlayerHandler {
+
+ public static final String OP = AjaxOperation.GAME_SKIP.toString();
+
+ private final ConnectedUsers connectedUsers;
+
+ @Inject
+ public GameSkipHandler(final ConnectedUsers connectedUsers, final GameManager gameManager) {
+ super(gameManager);
+ this.connectedUsers = connectedUsers;
+ }
+
+ @Override
+ public Map handleWithUserInGame(final RequestWrapper request,
+ final HttpSession session, final User user, final Game game) {
+ if (null == request.getParameter(AjaxRequest.NICKNAME)
+ || request.getParameter(AjaxRequest.NICKNAME).isEmpty()) {
+ return error(ErrorCode.NO_NICK_SPECIFIED);
+ }
+
+ // only an admin or the current host may skip a player (maybe let the judge?)
+ if (!user.isAdmin() && user != game.getHost()) {
+ return error(ErrorCode.NOT_GAME_HOST);
+ }
+
+ final User target = connectedUsers.getUser(request.getParameter(AjaxRequest.NICKNAME));
+ if (null == target) {
+ return error(ErrorCode.NO_SUCH_USER);
+ }
+
+ if (game.skipPlayer(target)) {
+ return new HashMap();
+ } else {
+ return error(ErrorCode.BAD_REQUEST);
+ }
+ }
+}
diff --git a/src/net/socialgamer/cah/handlers/Handlers.java b/src/net/socialgamer/cah/handlers/Handlers.java
index ef85e748..e2cc1978 100644
--- a/src/net/socialgamer/cah/handlers/Handlers.java
+++ b/src/net/socialgamer/cah/handlers/Handlers.java
@@ -17,7 +17,9 @@ public class Handlers {
LIST.put(CreateGameHandler.OP, CreateGameHandler.class);
LIST.put(FirstLoadHandler.OP, FirstLoadHandler.class);
LIST.put(GameChatHandler.OP, GameChatHandler.class);
+ LIST.put(GameHostHandler.OP, GameHostHandler.class);
LIST.put(GameListHandler.OP, GameListHandler.class);
+ LIST.put(GameSkipHandler.OP, GameSkipHandler.class);
LIST.put(GetCardsHandler.OP, GetCardsHandler.class);
LIST.put(GetGameInfoHandler.OP, GetGameInfoHandler.class);
LIST.put(JoinGameHandler.OP, JoinGameHandler.class);