blob: fe6826b29397c30f7ee4f7287f8c68f72066f6d0 [file] [log] [blame]
// Copyright 2015 The Vanadium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
part of hearts;
class HeartsCommand extends GameCommand {
// Usually this constructor is used when reading from a log/syncbase.
HeartsCommand(String phase, String data)
: super(phase, data, simultaneity: computeSimul(phase));
HeartsCommand.fromCommand(String cmd)
: super(cmd.split("|")[0], cmd.split("|")[1],
simultaneity: computeSimul(cmd.split("|")[0]));
// The following constructors are used for the player generating the HeartsCommand.
HeartsCommand.deal(int playerId, List<Card> cards)
: super("Deal", computeDeal(playerId, cards),
simultaneity: SimulLevel.INDEPENDENT);
HeartsCommand.pass(int senderId, List<Card> cards)
: super("Pass", computePass(senderId, cards),
simultaneity: SimulLevel.INDEPENDENT);
HeartsCommand.take(int takerId)
: super("Take", computeTake(takerId),
simultaneity: SimulLevel.INDEPENDENT);
HeartsCommand.play(int playerId, Card c)
: super("Play", computePlay(playerId, c),
simultaneity: SimulLevel.TURN_BASED);
HeartsCommand.takeTrick()
: super("TakeTrick", computeTakeTrick(),
simultaneity: SimulLevel.TURN_BASED);
HeartsCommand.ready(int playerId)
: super("Ready", computeReady(playerId),
simultaneity: SimulLevel.INDEPENDENT);
static SimulLevel computeSimul(String phase) {
switch (phase) {
case "Deal":
return SimulLevel.INDEPENDENT;
case "Pass":
return SimulLevel.INDEPENDENT;
case "Take":
return SimulLevel.INDEPENDENT;
case "Play":
return SimulLevel.TURN_BASED;
case "TakeTrick":
return SimulLevel.TURN_BASED;
case "Ready":
return SimulLevel.INDEPENDENT;
default:
print(phase);
assert(false); // How could this have happened?
return null;
}
}
static String computeDeal(int playerId, List<Card> cards) {
StringBuffer buff = new StringBuffer();
buff.write("${playerId}:");
cards.forEach((card) => buff.write("${card.toString()}:"));
buff.write("END");
return buff.toString();
}
static String computePass(int senderId, List<Card> cards) {
StringBuffer buff = new StringBuffer();
buff.write("${senderId}:");
cards.forEach((card) => buff.write("${card.toString()}:"));
buff.write("END");
return buff.toString();
}
static String computeTake(int takerId) {
return "${takerId}:END";
}
static String computePlay(int playerId, Card c) {
return "${playerId}:${c.toString()}:END";
}
static String computeTakeTrick() {
return "END";
}
static String computeReady(int playerId) {
return "${playerId}:END";
}
@override
bool canExecute(Game g) {
// TODO(alexfandrianto): This is very similar to execute, but without the
// mutations. It's possible to use a shared function to simplify/combine the
// logic.
HeartsGame game = g as HeartsGame;
print("HeartsCommand is checking: ${data}");
List<String> parts = data.split(":");
switch (phase) {
case "Deal":
if (game.phase != HeartsPhase.Deal) {
return false;
}
// Deal appends cards to playerId's hand.
int playerId = int.parse(parts[0]);
List<Card> hand = game.cardCollections[playerId];
if (hand.length + parts.length - 3 > 13) {
return false;
}
// The last part is 'END', but the rest are cards.
for (int i = 1; i < parts.length - 1; i++) {
Card c = new Card.fromString(parts[i]);
bool canTransfer = this.transferCheck(game.deck, hand, c);
if (!canTransfer) {
return false;
}
}
return true;
case "Pass":
if (game.phase != HeartsPhase.Pass) {
return false;
}
// Pass moves a set of cards from senderId to receiverId.
int senderId = int.parse(parts[0]);
int receiverId = senderId + HeartsGame.OFFSET_PASS;
List<Card> handS = game.cardCollections[senderId];
List<Card> handR = game.cardCollections[receiverId];
int numPassing = parts.length - 2; // not senderId and not end
if (numPassing != 3) {
return false;
}
// The last part is 'END', but the rest are cards.
for (int i = 1; i < parts.length - 1; i++) {
Card c = new Card.fromString(parts[i]);
bool canTransfer = this.transferCheck(handS, handR, c);
if (!canTransfer) {
return false;
}
}
return true;
case "Take":
if (game.phase != HeartsPhase.Take) {
return false;
}
return true;
case "Play":
if (game.phase != HeartsPhase.Play) {
return false;
}
// Play the card from the player's hand to their play pile.
int playerId = int.parse(parts[0]);
int targetId = playerId + HeartsGame.OFFSET_PLAY;
List<Card> hand = game.cardCollections[playerId];
List<Card> discard = game.cardCollections[targetId];
Card c = new Card.fromString(parts[1]);
// If the card isn't valid, then we have an error.
String reason = game.canPlay(playerId, c);
if (reason != null) {
return false;
}
bool canTransfer = this.transferCheck(hand, discard, c);
return canTransfer;
case "TakeTrick":
if (game.phase != HeartsPhase.Play) {
return false;
}
// There must be 4 cards played.
return game.allPlayed;
case "Ready":
if (game.hasGameEnded) {
return false;
}
if (game.phase != HeartsPhase.Score) {
return false;
}
return true;
default:
print(data);
assert(false); // How could this have happened?
return false;
}
}
@override
void execute(Game g) {
HeartsGame game = g as HeartsGame;
print("HeartsCommand is executing: ${data}");
List<String> parts = data.split(":");
switch (phase) {
case "Deal":
if (game.phase != HeartsPhase.Deal) {
throw new StateError(
"Cannot process deal commands when not in Deal phase");
}
// Deal appends cards to playerId's hand.
int playerId = int.parse(parts[0]);
List<Card> hand = game.cardCollections[playerId];
if (hand.length + parts.length - 3 > 13) {
throw new StateError("Cannot deal more than 13 cards to a hand");
}
// The last part is 'END', but the rest are cards.
for (int i = 1; i < parts.length - 1; i++) {
Card c = new Card.fromString(parts[i]);
this.transfer(game.deck, hand, c);
}
return;
case "Pass":
if (game.phase != HeartsPhase.Pass) {
throw new StateError(
"Cannot process pass commands when not in Pass phase");
}
// Pass moves a set of cards from senderId to receiverId.
int senderId = int.parse(parts[0]);
int receiverId = senderId + HeartsGame.OFFSET_PASS;
List<Card> handS = game.cardCollections[senderId];
List<Card> handR = game.cardCollections[receiverId];
int numPassing = parts.length - 2; // not senderId and not end
if (numPassing != 3) {
throw new StateError("Must pass 3 cards, attempted ${numPassing}");
}
// The last part is 'END', but the rest are cards.
for (int i = 1; i < parts.length - 1; i++) {
Card c = new Card.fromString(parts[i]);
this.transfer(handS, handR, c);
}
return;
case "Take":
if (game.phase != HeartsPhase.Take) {
throw new StateError(
"Cannot process take commands when not in Take phase");
}
int takerId = int.parse(parts[0]);
int senderPile = game.getTakeTarget(takerId) + HeartsGame.OFFSET_PASS;
List<Card> handT = game.cardCollections[takerId];
List<Card> handS = game.cardCollections[senderPile];
handT.addAll(handS);
handS.clear();
return;
case "Play":
if (game.phase != HeartsPhase.Play) {
throw new StateError(
"Cannot process play commands when not in Play phase");
}
// Play the card from the player's hand to their play pile.
int playerId = int.parse(parts[0]);
int targetId = playerId + HeartsGame.OFFSET_PLAY;
List<Card> hand = game.cardCollections[playerId];
List<Card> discard = game.cardCollections[targetId];
Card c = new Card.fromString(parts[1]);
// If the card isn't valid, then we have an error.
String reason = game.canPlay(playerId, c);
if (reason != null) {
throw new StateError(
"Player ${playerId} cannot play ${c.toString()} because ${reason}");
}
this.transfer(hand, discard, c);
return;
case "TakeTrick":
if (game.phase != HeartsPhase.Play) {
throw new StateError(
"Cannot process take trick commands when not in Play phase");
}
if (!game.allPlayed) {
throw new StateError(
"Cannot take trick when some players have not played");
}
// Determine who won this trick.
int winner = game.determineTrickWinner();
// Move the cards to their trick list. Also check if hearts was broken.
// Note: While some variants of Hearts allow the QUEEN_OF_SPADES to
// break hearts, this version does NOT implement that rule.
for (int i = 0; i < 4; i++) {
List<Card> play = game.cardCollections[i + HeartsGame.OFFSET_PLAY];
if (!game.heartsBroken && game.isHeartsCard(play[0])) {
game.heartsBroken = true;
}
game.cardCollections[winner + HeartsGame.OFFSET_TRICK]
.addAll(play); // or add(play[0])
play.clear();
}
// Set them as the next person to go.
game.lastTrickTaker = winner;
game.trickNumber++;
return;
case "Ready":
if (game.hasGameEnded) {
throw new StateError(
"Game has already ended. Start a new one to play again.");
}
if (game.phase != HeartsPhase.Score) {
throw new StateError(
"Cannot process ready commands when not in Score phase");
}
int playerId = int.parse(parts[0]);
game.setReady(playerId);
return;
default:
print(data);
assert(false); // How could this have happened?
}
}
void transfer(List<Card> sender, List<Card> receiver, Card c) {
if (!sender.contains(c)) {
throw new StateError(
"Sender ${sender.toString()} lacks Card ${c.toString()}");
}
sender.remove(c);
receiver.add(c);
}
bool transferCheck(List<Card> sender, List<Card> receiver, Card c) {
return sender.contains(c);
}
}