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."/>
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);