- discard cards after they are used, so that they can be reshuffled if more cards are required (hasn't been tested)

- much better handling when users leave the game:
  - put their hand in the discard pile
  - if they played this round, put that card in the discard pile and remove them from the played players list
  - if they were supposed to play this round, remove them from the players list
  - if they were judge, return all played cards to players and move to the next judge
  - if they weren't judge and were lower in judging order, fix the judge pointer
- add a goal to the game (8 points for now, hard-coded)
- keep track of which players are supposed to play this round so players joining and leaving don't affect the game progressing
This commit is contained in:
Andy Janata 2012-01-30 16:35:06 -08:00
parent ee327639eb
commit 2d5e9d18c0
8 changed files with 271 additions and 47 deletions

View File

@ -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.

View File

@ -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.
*

View File

@ -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.
*

View File

@ -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;

View File

@ -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<BlackCard> deck;
private final List<BlackCard> dealt;
private final List<BlackCard> 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<BlackCard>();
discard = new ArrayList<BlackCard>();
}
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);
}
}

View File

@ -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<Player> players = new ArrayList<Player>(10);
private final List<Player> roundPlayers = new ArrayList<Player>(9);
// TODO make this Map<Player, List<WhiteCard>> once we support the multiple play black cards
private final BidiFromIdHashMap<Player, WhiteCard> playedCards =
new BidiFromIdHashMap<Player, WhiteCard>();
@ -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<ReturnableData, Object> data = new HashMap<ReturnableData, Object>();
final HashMap<ReturnableData, Object> 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<Player> iterator = players.iterator();
while (iterator.hasNext()) {
final Player player = iterator.next();
if (player.getUser() == user) {
iterator.remove();
user.leaveGame(this);
final HashMap<ReturnableData, Object> data = new HashMap<ReturnableData, Object>();
HashMap<ReturnableData, Object> 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<WhiteCard> 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:
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,11 +372,22 @@ public class Game {
for (final Player player : players) {
final List<WhiteCard> hand = player.getHand();
final List<WhiteCard> newCards = new LinkedList<WhiteCard>();
while (hand.size() < currentHandSize) {
final WhiteCard card = whiteDeck.getNextCard();
synchronized (whiteDeck) {
while (hand.size() < 10) {
final WhiteCard card;
try {
card = whiteDeck.getNextCard();
} catch (final OutOfCardsException e) {
whiteDeck.reshuffle();
final HashMap<ReturnableData, Object> 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;
synchronized (playedCards) {
playedCards.clear();
}
synchronized (blackCardLock) {
do {
try {
blackDeck.discard(blackCard);
blackCard = blackDeck.getNextCard();
} catch (final OutOfCardsException e) {
blackDeck.reshuffle();
final HashMap<ReturnableData, Object> 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<ReturnableData, Object> data = getEventMap();
HashMap<ReturnableData, Object> 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<WhiteCard> cards) {
final Map<ReturnableData, Object> data = new HashMap<ReturnableData, Object>();
final Map<ReturnableData, Object> data = getEventMap();
data.put(LongPollResponse.EVENT, LongPollEvent.HAND_DEAL.toString());
data.put(LongPollResponse.GAME_ID, id);
final List<Map<WhiteCardData, Object>> 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() {
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<ReturnableData, Object> data = new HashMap<ReturnableData, Object>();
final HashMap<ReturnableData, Object> 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<ReturnableData, Object> 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() {
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();
}
}, ROUND_INTERMISSION);
};
}
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();

View File

@ -0,0 +1,5 @@
package net.socialgamer.cah.data;
public class OutOfCardsException extends Exception {
private static final long serialVersionUID = -1797946826947407779L;
}

View File

@ -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<WhiteCard> deck;
private final List<WhiteCard> dealt;
private final List<WhiteCard> 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<WhiteCard>();
discard = new ArrayList<WhiteCard>();
}
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);
}
}