diff --git a/WebContent/js/cah.constants.js b/WebContent/js/cah.constants.js index 90ce80a..d31f565 100644 --- a/WebContent/js/cah.constants.js +++ b/WebContent/js/cah.constants.js @@ -154,6 +154,7 @@ cah.$.GamePlayerStatus = function() { cah.$.GamePlayerStatus.prototype.dummyForAutocomplete = undefined; cah.$.GamePlayerStatus.HOST = "host"; cah.$.GamePlayerStatus.IDLE = "idle"; +cah.$.GamePlayerStatus.WINNER = "winner"; cah.$.GamePlayerStatus.PLAYING = "playing"; cah.$.GamePlayerStatus.JUDGE = "judge"; cah.$.GamePlayerStatus.JUDGING = "judging"; @@ -163,12 +164,14 @@ cah.$.GamePlayerStatus_msg['idle'] = ""; cah.$.GamePlayerStatus_msg['judging'] = "Selecting"; cah.$.GamePlayerStatus_msg['host'] = "Host"; cah.$.GamePlayerStatus_msg['judge'] = "Card Czar"; +cah.$.GamePlayerStatus_msg['winner'] = "Winner!"; cah.$.GamePlayerStatus_msg_2 = {}; cah.$.GamePlayerStatus_msg_2['playing'] = "Select a card to play."; cah.$.GamePlayerStatus_msg_2['idle'] = "Waiting for players..."; cah.$.GamePlayerStatus_msg_2['judging'] = "Select a winning card."; cah.$.GamePlayerStatus_msg_2['host'] = "Wait for players then click Start Game."; cah.$.GamePlayerStatus_msg_2['judge'] = "You are the Card Czar this round."; +cah.$.GamePlayerStatus_msg_2['winner'] = "You have won!"; cah.$.GameState = function() { // Dummy constructor to make Eclipse auto-complete. @@ -182,7 +185,7 @@ cah.$.GameState.DEALING = "dealing"; cah.$.GameState_msg = {}; cah.$.GameState_msg['playing'] = "In Progress"; cah.$.GameState_msg['judging'] = "In Progress"; -cah.$.GameState_msg['lobby'] = "Joinable (Not Started)"; +cah.$.GameState_msg['lobby'] = "Not Started"; cah.$.GameState_msg['dealing'] = "In Progress"; cah.$.GameState_msg['round_over'] = "In Progress"; @@ -190,17 +193,20 @@ cah.$.LongPollEvent = function() { // Dummy constructor to make Eclipse auto-complete. }; cah.$.LongPollEvent.prototype.dummyForAutocomplete = undefined; -cah.$.LongPollEvent.GAME_ROUND_COMPLETE = "game_round_complete"; -cah.$.LongPollEvent.NOOP = "noop"; -cah.$.LongPollEvent.GAME_PLAYER_INFO_CHANGE = "game_player_info_change"; -cah.$.LongPollEvent.GAME_STATE_CHANGE = "game_state_change"; cah.$.LongPollEvent.GAME_PLAYER_LEAVE = "game_player_leave"; cah.$.LongPollEvent.NEW_PLAYER = "new_player"; -cah.$.LongPollEvent.PLAYER_LEAVE = "player_leave"; cah.$.LongPollEvent.GAME_PLAYER_JOIN = "game_player_join"; -cah.$.LongPollEvent.HAND_DEAL = "hand_deal"; -cah.$.LongPollEvent.CHAT = "chat"; cah.$.LongPollEvent.GAME_LIST_REFRESH = "game_list_refresh"; +cah.$.LongPollEvent.GAME_ROUND_COMPLETE = "game_round_complete"; +cah.$.LongPollEvent.GAME_PLAYER_INFO_CHANGE = "game_player_info_change"; +cah.$.LongPollEvent.NOOP = "noop"; +cah.$.LongPollEvent.GAME_BLACK_RESHUFFLE = "game_black_reshuffle"; +cah.$.LongPollEvent.GAME_WHITE_RESHUFFLE = "game_white_reshuffle"; +cah.$.LongPollEvent.GAME_STATE_CHANGE = "game_state_change"; +cah.$.LongPollEvent.PLAYER_LEAVE = "player_leave"; +cah.$.LongPollEvent.CHAT = "chat"; +cah.$.LongPollEvent.HAND_DEAL = "hand_deal"; +cah.$.LongPollEvent.GAME_JUDGE_LEFT = "game_judge_left"; cah.$.LongPollResponse = function() { // Dummy constructor to make Eclipse auto-complete. diff --git a/WebContent/js/cah.game.js b/WebContent/js/cah.game.js index 71648bd..8cb46e4 100644 --- a/WebContent/js/cah.game.js +++ b/WebContent/js/cah.game.js @@ -538,6 +538,9 @@ cah.Game.prototype.updateUserStatus = function(playerInfo) { this.handSelectedCard_ = null; $(".selected", $(".game_hand", this.element_)).removeClass("selected"); } + if (playerStatus == cah.$.GamePlayerStatus.HOST) { + $("#start_game").show(); + } } if (playerStatus == cah.$.GamePlayerStatus.JUDGE @@ -572,6 +575,24 @@ cah.Game.prototype.roundComplete = function(data) { + (data[cah.$.LongPollResponse.INTERMISSION] / 1000) + " seconds."); }; +/** + * Notify the player that a deck has been reshuffled. + * + * @param {String} + * deck Deck name which has been reshuffled. + */ +cah.Game.prototype.reshuffle = function(deck) { + cah.log.status("The " + deck + " deck has been reshuffled."); +}; + +/** + * Notify the player that the judge has left the game and cards are being returned to hands. + */ +cah.Game.prototype.judgeLeft = function() { + cah.log + .status("The judge has left the game. 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 c86f417..2a290a0 100644 --- a/WebContent/js/cah.longpoll.handlers.js +++ b/WebContent/js/cah.longpoll.handlers.js @@ -81,6 +81,20 @@ cah.longpoll.EventHandlers[cah.$.LongPollEvent.GAME_ROUND_COMPLETE] = function(d "round complete"); }; +cah.longpoll.EventHandlers[cah.$.LongPollEvent.GAME_WHITE_RESHUFFLE] = function(data) { + cah.longpoll.EventHandlers.__gameEvent(data, cah.Game.prototype.reshuffle, "white", + "white reshuffle"); +}; + +cah.longpoll.EventHandlers[cah.$.LongPollEvent.GAME_BLACK_RESHUFFLE] = function(data) { + cah.longpoll.EventHandlers.__gameEvent(data, cah.Game.prototype.reshuffle, "black", + "black reshuffle"); +}; + +cah.longpoll.EventHandlers[cah.$.LongPollEvent.GAME_JUDGE_LEFT] = function(data) { + cah.longpoll.EventHandlers.__gameEvent(data, cah.Game.prototype.judgeLeft, "", "judge left"); +}; + /** * Helper for event handlers for game events. * diff --git a/src/net/socialgamer/cah/Constants.java b/src/net/socialgamer/cah/Constants.java index cdc876d..3fe4119 100644 --- a/src/net/socialgamer/cah/Constants.java +++ b/src/net/socialgamer/cah/Constants.java @@ -200,12 +200,15 @@ public class Constants { public enum LongPollEvent { CHAT("chat"), + GAME_BLACK_RESHUFFLE("game_black_reshuffle"), + GAME_JUDGE_LEFT("game_judge_left"), GAME_LIST_REFRESH("game_list_refresh"), GAME_PLAYER_INFO_CHANGE("game_player_info_change"), GAME_PLAYER_JOIN("game_player_join"), GAME_PLAYER_LEAVE("game_player_leave"), GAME_ROUND_COMPLETE("game_round_complete"), GAME_STATE_CHANGE("game_state_change"), + GAME_WHITE_RESHUFFLE("game_white_reshuffle"), HAND_DEAL("hand_deal"), NEW_PLAYER("new_player"), NOOP("noop"), @@ -295,7 +298,7 @@ public class Constants { public enum GameState implements Localizable { DEALING("dealing", "In Progress"), JUDGING("judging", "In Progress"), - LOBBY("lobby", "Joinable (Not Started)"), + LOBBY("lobby", "Not Started"), PLAYING("playing", "In Progress"), ROUND_OVER("round_over", "In Progress"); @@ -358,7 +361,8 @@ public class Constants { IDLE("idle", "", "Waiting for players..."), JUDGE("judge", "Card Czar", "You are the Card Czar this round."), JUDGING("judging", "Selecting", "Select a winning card."), - PLAYING("playing", "Playing", "Select a card to play."); + PLAYING("playing", "Playing", "Select a card to play."), + WINNER("winner", "Winner!", "You have won!"); private final String status; private final String message; diff --git a/src/net/socialgamer/cah/data/BlackDeck.java b/src/net/socialgamer/cah/data/BlackDeck.java index 8b6132e..de0f6b8 100644 --- a/src/net/socialgamer/cah/data/BlackDeck.java +++ b/src/net/socialgamer/cah/data/BlackDeck.java @@ -1,6 +1,7 @@ package net.socialgamer.cah.data; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import net.socialgamer.cah.HibernateUtil; @@ -12,6 +13,7 @@ import org.hibernate.Session; public class BlackDeck { private final List deck; private final List dealt; + private final List discard; @SuppressWarnings("unchecked") public BlackDeck() { @@ -19,14 +21,31 @@ public class BlackDeck { // TODO option to restrict to only stock cards or allow customs deck = session.createQuery("from BlackCard order by random()").list(); dealt = new ArrayList(); + discard = new ArrayList(); } - public BlackCard getNextCard() { - // Without knowing what the implementation of List that Hibernate returns - // is, I'm going to pull from the end instead of the beginning. + public BlackCard getNextCard() throws OutOfCardsException { + if (deck.size() == 0) { + throw new OutOfCardsException(); + } + // Hibernate is returning an ArrayList, so this is a bit faster. final BlackCard card = deck.get(deck.size() - 1); deck.remove(deck.size() - 1); dealt.add(card); return card; } + + public void discard(final BlackCard card) { + if (card != null) { + discard.add(card); + } + } + + /** + * Shuffles the discard pile and puts the cards under the cards remaining in the deck. + */ + public void reshuffle() { + Collections.shuffle(discard); + deck.addAll(0, discard); + } } diff --git a/src/net/socialgamer/cah/data/Game.java b/src/net/socialgamer/cah/data/Game.java index 851baca..cd9caab 100644 --- a/src/net/socialgamer/cah/data/Game.java +++ b/src/net/socialgamer/cah/data/Game.java @@ -1,6 +1,7 @@ package net.socialgamer.cah.data; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; @@ -51,6 +52,7 @@ import com.google.inject.Inject; public class Game { private final int id; private final List players = new ArrayList(10); + private final List roundPlayers = new ArrayList(9); // TODO make this Map> once we support the multiple play black cards private final BidiFromIdHashMap playedCards = new BidiFromIdHashMap(); @@ -62,14 +64,15 @@ public class Game { private final Object blackCardLock = new Object(); private WhiteDeck whiteDeck; private GameState state; - // TODO make this work with "draw x" cards. probably will not actually be done here. - private final int currentHandSize = 10; // TODO make this host-configurable private final int maxPlayers = 10; + // TODO also need to configure this private int judgeIndex = 0; private final static int ROUND_INTERMISSION = 8 * 1000; private Timer nextRoundTimer; private final Object nextRoundTimerLock = new Object(); + // TODO host config + private final int scoreGoal = 8; /** * TODO Injection here would be much nicer, but that would need a Provider for the id... Too much @@ -110,12 +113,10 @@ public class Game { if (host == null) { host = player; } - } - final HashMap data = new HashMap(); + final HashMap data = getEventMap(); data.put(LongPollResponse.EVENT, LongPollEvent.GAME_PLAYER_JOIN.toString()); - data.put(LongPollResponse.GAME_ID, id); data.put(LongPollResponse.NICKNAME, user.getNickname()); broadcastToPlayers(MessageType.GAME_PLAYER_EVENT, data); } @@ -134,16 +135,14 @@ public class Game { * @return True if {@code user} was the last player in the game. */ public boolean removePlayer(final User user) { + boolean wasJudge = false; synchronized (players) { final Iterator iterator = players.iterator(); while (iterator.hasNext()) { final Player player = iterator.next(); if (player.getUser() == user) { - iterator.remove(); - user.leaveGame(this); - final HashMap data = new HashMap(); + HashMap data = getEventMap(); data.put(LongPollResponse.EVENT, LongPollEvent.GAME_PLAYER_LEAVE.toString()); - data.put(LongPollResponse.GAME_ID, id); data.put(LongPollResponse.NICKNAME, user.getNickname()); broadcastToPlayers(MessageType.GAME_PLAYER_EVENT, data); if (host == player) { @@ -153,6 +152,63 @@ public class Game { host = null; } } + // If they played this round, remove card from played card list. + synchronized (playedCards) { + if (playedCards.containsKey(player)) { + synchronized (whiteDeck) { + // FIXME for multi-play + whiteDeck.discard(playedCards.get(player)); + } + playedCards.remove(player); + } + } + // If they are to play this round, remove them from that list. + synchronized (roundPlayers) { + if (roundPlayers.contains(player)) { + roundPlayers.remove(player); + if (roundPlayers.size() == playedCards.size()) { + // FIXME for multi-play + judgingState(); + } + } + } + // If they have a hand, return it to discard pile. + if (player.getHand().size() > 0) { + synchronized (whiteDeck) { + final List hand = player.getHand(); + for (final WhiteCard card : hand) { + whiteDeck.discard(card); + } + } + } + // If they are judge, return all played cards to hand, and move to next judge. + if (getJudge() == player) { + data = getEventMap(); + data.put(LongPollResponse.EVENT, LongPollEvent.GAME_JUDGE_LEFT.toString()); + broadcastToPlayers(MessageType.GAME_EVENT, data); + synchronized (playedCards) { + // FIXME for multi-play + for (final Player p : playedCards.keySet()) { + p.getHand().add(playedCards.get(p)); + sendCardsToPlayer(p, Arrays.asList(playedCards.get(p))); + } + // prevent startNextRound from discarding cards + playedCards.clear(); + } + // startNextRound will advance it again. + judgeIndex--; + // Can't start the next round right here. + wasJudge = true; + } + // If they aren't judge but are earlier in judging order, fix the judge index. + else if (players.indexOf(player) < judgeIndex) { + judgeIndex--; + } + + // we can't actually remove them until down here because we need to deal with the judge + // index stuff first. + iterator.remove(); + user.leaveGame(this); break; } } @@ -162,6 +218,8 @@ public class Game { } if (players.size() < 3 && state != GameState.LOBBY) { resetState(true); + } else if (wasJudge) { + startNextRound(); } return players.size() == 0; } @@ -261,7 +319,15 @@ public class Game { } break; case ROUND_OVER: - playerStatus = GamePlayerStatus.IDLE; + if (getJudge() == player) { + playerStatus = GamePlayerStatus.JUDGE; + } + // TODO win-by-x + else if (player.getScore() == scoreGoal) { + playerStatus = GamePlayerStatus.WINNER; + } else { + playerStatus = GamePlayerStatus.IDLE; + } break; default: throw new IllegalStateException("Unknown GameState " + state.toString()); @@ -283,7 +349,7 @@ public class Game { if (players.size() >= 3) { blackDeck = new BlackDeck(); whiteDeck = new WhiteDeck(); - dealState(); + startNextRound(); return true; } else { return false; @@ -306,10 +372,21 @@ public class Game { for (final Player player : players) { final List hand = player.getHand(); final List newCards = new LinkedList(); - while (hand.size() < currentHandSize) { - final WhiteCard card = whiteDeck.getNextCard(); - hand.add(card); - newCards.add(card); + synchronized (whiteDeck) { + while (hand.size() < 10) { + final WhiteCard card; + try { + card = whiteDeck.getNextCard(); + } catch (final OutOfCardsException e) { + whiteDeck.reshuffle(); + final HashMap data = getEventMap(); + data.put(LongPollResponse.EVENT, LongPollEvent.GAME_WHITE_RESHUFFLE); + broadcastToPlayers(MessageType.GAME_EVENT, data); + continue; + } + hand.add(card); + newCards.add(card); + } } sendCardsToPlayer(player, newCards); } @@ -320,11 +397,22 @@ public class Game { private void playingState() { state = GameState.PLAYING; - playedCards.clear(); + synchronized (playedCards) { + playedCards.clear(); + } synchronized (blackCardLock) { do { - blackCard = blackDeck.getNextCard(); + try { + blackDeck.discard(blackCard); + blackCard = blackDeck.getNextCard(); + } catch (final OutOfCardsException e) { + blackDeck.reshuffle(); + final HashMap data = getEventMap(); + data.put(LongPollResponse.EVENT, LongPollEvent.GAME_BLACK_RESHUFFLE); + broadcastToPlayers(MessageType.GAME_EVENT, data); + continue; + } // TODO remove this loop once the game supports the pick and draw features } while (blackCard.getPick() != 1 || blackCard.getDraw() != 0); } @@ -386,19 +474,35 @@ public class Game { synchronized (blackCardLock) { blackCard = null; } + synchronized (playedCards) { + playedCards.clear(); + } + synchronized (roundPlayers) { + roundPlayers.clear(); + } state = GameState.LOBBY; + final Player judge = getJudge(); judgeIndex = 0; - final HashMap data = getEventMap(); + HashMap data = getEventMap(); data.put(LongPollResponse.EVENT, LongPollEvent.GAME_STATE_CHANGE.toString()); data.put(LongPollResponse.GAME_STATE, GameState.LOBBY.toString()); broadcastToPlayers(MessageType.GAME_EVENT, data); + + data = getEventMap(); + data.put(LongPollResponse.EVENT, LongPollEvent.GAME_PLAYER_INFO_CHANGE.toString()); + data.put(LongPollResponse.PLAYER_INFO, getPlayerInfo(host)); + broadcastToPlayers(MessageType.GAME_PLAYER_EVENT, data); + + data = getEventMap(); + data.put(LongPollResponse.EVENT, LongPollEvent.GAME_PLAYER_INFO_CHANGE.toString()); + data.put(LongPollResponse.PLAYER_INFO, getPlayerInfo(judge)); + broadcastToPlayers(MessageType.GAME_PLAYER_EVENT, data); } private void sendCardsToPlayer(final Player player, final List cards) { - final Map data = new HashMap(); + final Map data = getEventMap(); data.put(LongPollResponse.EVENT, LongPollEvent.HAND_DEAL.toString()); - data.put(LongPollResponse.GAME_ID, id); final List> cardData = handSubsetToClient(cards); data.put(LongPollResponse.HAND, cardData); final QueuedMessage qm = new QueuedMessage(MessageType.GAME_EVENT, data); @@ -506,7 +610,11 @@ public class Game { } private Player getJudge() { - return players.get(judgeIndex); + if (judgeIndex >= 0 && judgeIndex < players.size()) { + return players.get(judgeIndex); + } else { + return null; + } } public ErrorCode playCard(final User user, final int cardId) { @@ -534,15 +642,14 @@ public class Game { synchronized (playedCards) { playedCards.put(player, playCard); - final HashMap data = new HashMap(); + final HashMap data = getEventMap(); data.put(LongPollResponse.EVENT, LongPollEvent.GAME_PLAYER_INFO_CHANGE.toString()); - data.put(LongPollResponse.GAME_ID, id); data.put(LongPollResponse.PLAYER_INFO, getPlayerInfo(player)); broadcastToPlayers(MessageType.GAME_PLAYER_EVENT, data); // TODO make this check that everybody has played proper number of cards when we support // multiple play blacks - if (playedCards.size() == players.size() - 1) { + if (playedCards.size() == roundPlayers.size()) { judgingState(); } } @@ -572,6 +679,7 @@ public class Game { } cardPlayer.increaseScore(); + state = GameState.ROUND_OVER; HashMap data = getEventMap(); data.put(LongPollResponse.EVENT, LongPollEvent.GAME_ROUND_COMPLETE.toString()); @@ -583,7 +691,6 @@ public class Game { data = getEventMap(); data.put(LongPollResponse.EVENT, LongPollEvent.GAME_PLAYER_INFO_CHANGE.toString()); data.put(LongPollResponse.PLAYER_INFO, getPlayerInfo(getJudge())); - state = GameState.ROUND_OVER; broadcastToPlayers(MessageType.GAME_PLAYER_EVENT, data); data = getEventMap(); @@ -593,23 +700,52 @@ public class Game { synchronized (nextRoundTimerLock) { nextRoundTimer = new Timer(); - nextRoundTimer.schedule(new TimerTask() { - @Override - public void run() { - startNextRound(); - } - }, ROUND_INTERMISSION); + final TimerTask task; + // TODO win-by-x option + if (cardPlayer.getScore() == scoreGoal) { + task = new TimerTask() { + @Override + public void run() { + winState(); + } + }; + } else { + task = new TimerTask() { + @Override + public void run() { + startNextRound(); + } + }; + } + nextRoundTimer.schedule(task, ROUND_INTERMISSION); } return null; } private void startNextRound() { + synchronized (whiteDeck) { + synchronized (playedCards) { + for (final WhiteCard card : playedCards.values()) { + // TODO fix this for multiple played cards + whiteDeck.discard(card); + } + } + } + synchronized (players) { judgeIndex++; if (judgeIndex >= players.size()) { judgeIndex = 0; } + synchronized (roundPlayers) { + roundPlayers.clear(); + for (final Player player : players) { + if (player != getJudge()) { + roundPlayers.add(player); + } + } + } } dealState(); diff --git a/src/net/socialgamer/cah/data/OutOfCardsException.java b/src/net/socialgamer/cah/data/OutOfCardsException.java new file mode 100644 index 0000000..1ac9397 --- /dev/null +++ b/src/net/socialgamer/cah/data/OutOfCardsException.java @@ -0,0 +1,5 @@ +package net.socialgamer.cah.data; + +public class OutOfCardsException extends Exception { + private static final long serialVersionUID = -1797946826947407779L; +} diff --git a/src/net/socialgamer/cah/data/WhiteDeck.java b/src/net/socialgamer/cah/data/WhiteDeck.java index 9eeced1..a9c5316 100644 --- a/src/net/socialgamer/cah/data/WhiteDeck.java +++ b/src/net/socialgamer/cah/data/WhiteDeck.java @@ -1,6 +1,7 @@ package net.socialgamer.cah.data; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import net.socialgamer.cah.HibernateUtil; @@ -12,6 +13,7 @@ import org.hibernate.Session; public class WhiteDeck { private final List deck; private final List dealt; + private final List discard; @SuppressWarnings("unchecked") public WhiteDeck() { @@ -19,14 +21,31 @@ public class WhiteDeck { // TODO option to restrict to only stock cards or allow customs deck = session.createQuery("from WhiteCard order by random()").list(); dealt = new ArrayList(); + discard = new ArrayList(); } - public WhiteCard getNextCard() { - // Without knowing what the implementation of List that Hibernate returns - // is, I'm going to pull from the end instead of the beginning. + public WhiteCard getNextCard() throws OutOfCardsException { + if (deck.size() == 0) { + throw new OutOfCardsException(); + } + // Hibernate is returning an ArrayList, so this is a bit faster. final WhiteCard card = deck.get(deck.size() - 1); deck.remove(deck.size() - 1); dealt.add(card); return card; } + + public void discard(final WhiteCard card) { + if (card != null) { + discard.add(card); + } + } + + /** + * Shuffles the discard pile and puts the cards under the cards remaining in the deck. + */ + public void reshuffle() { + Collections.shuffle(discard); + deck.addAll(0, discard); + } }