Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add /host and /skip commands. #102

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion WebContent/game.jsp
Original file line number Diff line number Diff line change
Expand Up @@ -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."/>
<label id="use_timer_template_label" for="use_timer_template"
title="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 <code>/skip</code> or <code>/kick</code> idle players manually.)
</label>
<br/>
<fieldset class="card_sets">
Expand Down
8 changes: 8 additions & 0 deletions WebContent/js/cah.ajax.handlers.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
};
18 changes: 16 additions & 2 deletions WebContent/js/cah.app.js
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Expand All @@ -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;
Expand Down
2 changes: 2 additions & 0 deletions WebContent/js/cah.constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
2 changes: 2 additions & 0 deletions src/net/socialgamer/cah/Constants.java
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand Down
90 changes: 90 additions & 0 deletions src/net/socialgamer/cah/data/Game.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add a log message. Should also include who requested the change (not that I currently think there could be admin abuse, but future proofing). Might just be easier to do the logging in the handler so that user doesn't need to be passed in here.

}

/**
* Add a spectator to the game.
*
Expand Down Expand Up @@ -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<WhiteCard> 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).",
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please include the username that requested the skip. Probably have to pass them in only for the log message... Alternatively the log message could be emitted from the handler. Just seems like it'd be useful for tracking abuse.

player.getUser().toString(), id));
//player.skipped(); // don't do this for the moment

final HashMap<ReturnableData, Object> 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<WhiteCard> 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) {
Expand Down
85 changes: 85 additions & 0 deletions src/net/socialgamer/cah/handlers/GameHostHandler.java
Original file line number Diff line number Diff line change
@@ -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<ReturnableData, Object> 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<ReturnableData, Object>();
} else {
return error(ErrorCode.BAD_REQUEST);
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe add a new error code (and message) for "user is not in this game"?

}
}
}
85 changes: 85 additions & 0 deletions src/net/socialgamer/cah/handlers/GameSkipHandler.java
Original file line number Diff line number Diff line change
@@ -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<ReturnableData, Object> 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<ReturnableData, Object>();
} else {
return error(ErrorCode.BAD_REQUEST);
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same re: error code.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This one's a little trickier -- a false return from skipPlayer could mean that the user is not in the game, but it could also mean that the game is not in a state where it is reasonable to skip (eg. not playing, or judging but they're not the judge). That's mainly why I left it as a generic "bad request".

I suppose I could make skipPlayer return the appropriate ErrorCode, but that feels like an abstraction layer leak.

}
}
}
2 changes: 2 additions & 0 deletions src/net/socialgamer/cah/handlers/Handlers.java
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down