The implementation has been fixed up and lots of tests added.
The next step is to add the UI for Hearts on top of this.
Unfortunately, with a single device, it may not look very good.
diff --git a/Makefile b/Makefile
index c905372..6bcebad 100644
--- a/Makefile
+++ b/Makefile
@@ -1,6 +1,7 @@
# Get the packages used by the dart project, according to pubspec.yaml
-# I don't know why but pub get reverts me... Or perhaps Sublime does?
-get-packages:
+# Can also use `pub get`, but Sublime occasionally reverts me to an ealier version.
+# Only `pub upgrade` can escape such a thing.
+get-packages: pubspec.yaml
pub upgrade
TEST_FILES := $(shell find test -name *.dart ! -name *.part.dart)
@@ -8,18 +9,21 @@
check-fmt:
dartfmt -n lib/main.dart $(TEST_FILES)
-lint: get-packages
+lint:
dartanalyzer lib/main.dart
dartanalyzer $(TEST_FILES)
-start: get-packages
+start:
./packages/sky/sky_tool start
install: get-packages
./packages/sky/sky_tool start --install
-test: get-packages
- pub run test
+# Could use `pub run test` too, but I like seeing every assertion print out.
+test:
+ dart --checked $(TEST_FILES)
clean:
rm -rf packages
+
+.PHONY: check-fmt lint start install test clean
\ No newline at end of file
diff --git a/lib/components/croupier.dart b/lib/components/croupier.dart
index e33e015..b06ae24 100644
--- a/lib/components/croupier.dart
+++ b/lib/components/croupier.dart
@@ -69,7 +69,10 @@
case logic_croupier.CroupierState.ArrangePlayers:
return null; // If needed, lists the players around and what devices they'd like to use.
case logic_croupier.CroupierState.PlayGame:
- return new GameComponent(croupier.game); // Asks the game UI to draw itself.
+ return new Container(
+ padding: new EdgeDims.only(top: sky.view.paddingTop),
+ child: new GameComponent(croupier.game) // Asks the game UI to draw itself.
+ );
default:
assert(false);
return null;
diff --git a/lib/components/game.dart b/lib/components/game.dart
index 1f384b2..dc0daf2 100644
--- a/lib/components/game.dart
+++ b/lib/components/game.dart
@@ -1,5 +1,5 @@
import '../logic/card.dart' show Card;
-import '../logic/game.dart' show Game, GameType, Viewer;
+import '../logic/game.dart' show Game, GameType, Viewer, HeartsGame, HeartsPhase;
import 'card_collection.dart' show CardCollectionComponent, Orientation;
import 'package:sky/widgets/basic.dart';
import 'package:sky/widgets.dart' show FlatButton;
@@ -43,7 +43,12 @@
_updateGameCallback(Card card, List<Card> dest) {
setState(() {
- game.move(card, dest);
+ try {
+ game.move(card, dest);
+ } catch(e) {
+ print("You can't do that! ${e.toString()}");
+ game.debugString = e.toString();
+ }
});
}
@@ -104,7 +109,42 @@
);
}
+ Widget _makeSwitchViewButton() =>_makeButton('Switch View', _switchPlayersCallback);
+
+ Widget _makeButton(String text, Function callback) {
+ return new FlatButton(
+ child: new Text(text),
+ onPressed: callback
+ );
+ }
+
Widget buildHearts() {
+ HeartsGame game = this.game as HeartsGame;
+
+ switch (game.phase) {
+ case HeartsPhase.Deal:
+ return new Container(
+ decoration: new BoxDecoration(backgroundColor: colors.Pink[500]),
+ child: new Flex([
+ new Text('Player ${game.playerNumber}'),
+ _makeButton('Deal', game.dealCards),
+ _makeSwitchViewButton()
+ ], direction: FlexDirection.vertical)
+ );
+ case HeartsPhase.Pass:
+ case HeartsPhase.Take:
+ case HeartsPhase.Play:
+ case HeartsPhase.Score:
+ return showBoard();
+ default:
+ assert(false); // What?
+ return null;
+ }
+ }
+
+ Widget showBoard() {
+ HeartsGame game = this.game as HeartsGame;
+
List<Widget> cardCollections = new List<Widget>();
cardCollections.add(new Text(game.debugString));
diff --git a/lib/logic/game.dart b/lib/logic/game.dart
index 1a450f2..b5f7d36 100644
--- a/lib/logic/game.dart
+++ b/lib/logic/game.dart
@@ -1,5 +1,5 @@
import 'card.dart' show Card;
-import 'dart:math' show Random;
+import 'dart:math' as math;
// Note: Proto and Board are "fake" games intended to demonstrate what we can do.
// Proto is just a drag cards around "game".
@@ -15,7 +15,7 @@
final List<List<Card>> cardCollections = new List<List<Card>>();
final List<Card> deck = new List<Card>.from(Card.All);
- final Random random = new Random();
+ final math.Random random = new math.Random();
final GameLog gamelog = new GameLog();
int playerNumber;
String debugString = 'hello?';
@@ -139,51 +139,45 @@
static const OFFSET_TRICK = 8;
static const OFFSET_PASS = 12;
+ static const MAX_SCORE = 100; // Play until someone gets to 100.
+
// Note: These cards are final because the "classic" deck has 52 cards.
// It is up to the renderer to reskin those cards as needed.
final Card TWO_OF_CLUBS = new Card("classic", "c2");
final Card QUEEN_OF_SPADES = new Card("classic", "sq");
- HeartsPhase phase;
- int roundNumber;
+ HeartsPhase _phase = HeartsPhase.Deal;
+ HeartsPhase get phase => _phase;
+ void set phase(HeartsPhase other) {
+ print('setting phase from ${_phase} to ${other}');
+ _phase = other;
+ }
+ int roundNumber = 0;
int lastTrickTaker;
bool heartsBroken;
+ int trickNumber;
// Used by the score screen to track scores and see which players are ready to continue to the next round.
List<int> scores = [0, 0, 0, 0];
List<bool> ready;
HeartsGame(int playerNumber) : super._create(GameType.Hearts, playerNumber, 16) {
- prepareRound();
+ resetGame();
}
- void prepareRound() {
- if (roundNumber == null) {
- roundNumber = 0;
- } else {
- roundNumber++;
- }
-
- phase = HeartsPhase.Deal;
-
+ void resetGame() {
this.resetCards();
heartsBroken = false;
lastTrickTaker = null;
+ trickNumber = 0;
+ }
+
+ void dealCards() {
deck.shuffle();
deal(PLAYER_A, 13);
deal(PLAYER_B, 13);
deal(PLAYER_C, 13);
deal(PLAYER_D, 13);
-
- if (this.passTarget != null) {
- phase = HeartsPhase.Pass;
- } else {
- phase = HeartsPhase.Play;
- }
- }
-
- int get trickNumber {
- return 13 - cardCollections[0].length;
}
int get passTarget {
@@ -201,14 +195,15 @@
return null;
}
}
- int get takeTarget {
+ int get takeTarget => _getTakeTarget(playerNumber);
+ int _getTakeTarget(takerId) {
switch (roundNumber % 4) { // is a 4-cycle
case 0:
- return (playerNumber + 1) % 4; // takeRight
+ return (takerId + 1) % 4; // takeRight
case 1:
- return (playerNumber - 1) % 4; // takeLeft
+ return (takerId - 1) % 4; // takeLeft
case 2:
- return (playerNumber + 2) % 4; // taleAcross
+ return (takerId + 2) % 4; // taleAcross
case 3:
return null; // no player to pass to
default:
@@ -222,17 +217,13 @@
if (phase != HeartsPhase.Play) {
return null;
}
- if (trickNumber == 0) {
- return (this.findCard(TWO_OF_CLUBS) + this.numPlayed) % 4;
- } else {
- return (lastTrickTaker + this.numPlayed) % 4;
- }
+ return (lastTrickTaker + this.numPlayed) % 4;
}
int getCardValue(Card c) {
String remainder = c.identifier.substring(1);
switch (remainder) {
- case "0": // ace
+ case "1": // ace
return 14;
case "k":
return 13;
@@ -271,25 +262,28 @@
}
Card get leadingCard {
- assert(this.numPlayed == 1);
- for (int i = 0; i < 4; i++) {
- if (cardCollections[i + OFFSET_HAND].length == 1) {
- return cardCollections[i + OFFSET_HAND][0];
- }
+ if(this.numPlayed >= 1) {
+ return cardCollections[this.lastTrickTaker + OFFSET_PLAY][0];
}
- assert(false);
return null;
}
int get numPlayed {
int count = 0;
for (int i = 0; i < 4; i++) {
- if (cardCollections[i + OFFSET_HAND].length == 1) {
+ if (cardCollections[i + OFFSET_PLAY].length == 1) {
count++;
}
}
return count;
}
+ bool get hasGameEnded => this.scores.reduce(math.max) >= HeartsGame.MAX_SCORE;
+
+ bool get allDealt => cardCollections[PLAYER_A].length == 13 &&
+ cardCollections[PLAYER_B].length == 13 &&
+ cardCollections[PLAYER_C].length == 13 &&
+ cardCollections[PLAYER_D].length == 13;
+
bool get allPassed => cardCollections[PLAYER_A_PASS].length == 3 &&
cardCollections[PLAYER_B_PASS].length == 3 &&
cardCollections[PLAYER_C_PASS].length == 3 &&
@@ -317,7 +311,7 @@
void passCards(List<Card> cards) {
assert(phase == HeartsPhase.Pass && this.passTarget != null);
if (cards.length != 3) {
- throw new ArgumentError('3 cards expected, but got: ${cards.toString()}');
+ throw new StateError('3 cards expected, but got: ${cards.toString()}');
}
gamelog.add(new HeartsCommand.pass(playerNumber, cards));
}
@@ -375,6 +369,16 @@
void triggerEvents() {
switch (this.phase) {
case HeartsPhase.Deal:
+ if (this.allDealt) {
+ if (this.passTarget != null) {
+ phase = HeartsPhase.Pass;
+ } else {
+ // All cards are dealt. The person who "won" the last trick goes first.
+ // In this case, we'll just pretend it's the person with the 2 of clubs.
+ this.lastTrickTaker = this.findCard(TWO_OF_CLUBS);
+ phase = HeartsPhase.Play;
+ }
+ }
return;
case HeartsPhase.Pass:
if (this.allPassed) {
@@ -383,6 +387,9 @@
return;
case HeartsPhase.Take:
if (this.allTaken) {
+ // All cards are dealt. The person who "won" the last trick goes first.
+ // In this case, we'll just pretend it's the person with the 2 of clubs.
+ this.lastTrickTaker = this.findCard(TWO_OF_CLUBS);
phase = HeartsPhase.Play;
}
return;
@@ -404,16 +411,20 @@
// Set them as the next person to go.
this.lastTrickTaker = winner;
+ this.trickNumber++;
// Additionally, if that was the last trick, move onto the score phase.
if (this.trickNumber == 13) {
+ phase = HeartsPhase.Score;
this.prepareScore();
}
}
return;
case HeartsPhase.Score:
- if (this.allReady) {
- this.prepareRound();
+ if (!this.hasGameEnded && this.allReady) {
+ this.roundNumber++;
+ phase = HeartsPhase.Deal;
+ this.resetGame();
}
return;
default:
@@ -438,13 +449,15 @@
if (trickNumber == 0 && isPenaltyCard(c)) {
return "Cannot play a penalty card on the first round of Hearts.";
}
- if (isHeartsCard(c) && !heartsBroken) {
+ if (this.numPlayed == 0 && isHeartsCard(c) && !heartsBroken) {
return "Cannot lead with a heart when the suit has not been broken yet.";
}
- String leadingSuit = getCardSuit(this.leadingCard);
- String otherSuit = getCardSuit(c);
- if (this.numPlayed >= 1 && leadingSuit != otherSuit && hasSuit(player, leadingSuit)) {
- return "Must follow with a ${leadingSuit}.";
+ if (this.leadingCard != null) {
+ String leadingSuit = getCardSuit(this.leadingCard);
+ String otherSuit = getCardSuit(c);
+ if (this.numPlayed >= 1 && leadingSuit != otherSuit && hasSuit(player, leadingSuit)) {
+ return "Must follow with a ${leadingSuit}.";
+ }
}
return null;
}
@@ -467,9 +480,13 @@
}
void prepareScore() {
this.unsetReady();
+ this.updateScore();
- phase = HeartsPhase.Score;
+ // At this point, it's up to the UI to determine what to do if the game is 'over'.
+ // Check this.hasGameEnded to determine if that is the case. Logically, there is nothing for this game to do.
+ }
+ void updateScore() {
// Count up points and check if someone shot the moon.
int shotMoon = null;
for (int i = 0; i < 4; i++) {
@@ -596,6 +613,9 @@
// Deal appends cards to playerId's hand.
int playerId = int.parse(parts[1]);
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 = 2; i < parts.length - 1; i++) {
@@ -613,6 +633,11 @@
List<Card> handS = game.cardCollections[senderId];
List<Card> handR = game.cardCollections[receiverId];
+ int numPassing = parts.length - 3;
+ 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 = 2; i < parts.length - 1; i++) {
Card c = new Card.fromString(parts[i]);
@@ -624,11 +649,11 @@
throw new StateError("Cannot process take commands when not in Take phase");
}
int takerId = int.parse(parts[1]);
- int senderPile = game.takeTarget + HeartsGame.OFFSET_PASS;
- List<Card> handS = game.cardCollections[senderPile];
+ int senderPile = game._getTakeTarget(takerId) + HeartsGame.OFFSET_PASS;
List<Card> handT = game.cardCollections[takerId];
- handS.addAll(handT);
- handT.clear();
+ List<Card> handS = game.cardCollections[senderPile];
+ handT.addAll(handS);
+ handS.clear();
return;
case "Play":
if (game.phase != HeartsPhase.Play) {
@@ -651,6 +676,9 @@
this.transfer(hand, discard, c);
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");
}
@@ -664,7 +692,9 @@
}
void transfer(List<Card> sender, List<Card> receiver, Card c) {
- assert(sender.contains(c));
+ if (!sender.contains(c)) {
+ throw new StateError("Sender ${sender.toString()} lacks Card ${c.toString()}");
+ }
sender.remove(c);
receiver.add(c);
}
diff --git a/lib/main.dart b/lib/main.dart
index 64cef29..173d822 100644
--- a/lib/main.dart
+++ b/lib/main.dart
@@ -24,7 +24,13 @@
}
Widget build() {
- return new CroupierComponent(this.croupier);
+ return new Container(
+ decoration: new BoxDecoration(
+ backgroundColor: const Color(0xFF0000FF),
+ borderRadius: 5.0
+ ),
+ child: new CroupierComponent(this.croupier)
+ );
}
}
diff --git a/test/game_log_hearts_test.txt b/test/game_log_hearts_test.txt
new file mode 100644
index 0000000..de1b3b2
--- /dev/null
+++ b/test/game_log_hearts_test.txt
@@ -0,0 +1,508 @@
+# Deal
+Deal:0:classic h1:classic h2:classic h3:classic h4:classic h5:classic d6:classic d7:classic d8:classic d9:classic d10:classic dj:classic dq:classic dk:END
+Deal:1:classic d1:classic d2:classic d3:classic d4:classic d5:classic s6:classic s7:classic s8:classic s9:classic s10:classic sj:classic sq:classic sk:END
+Deal:2:classic s1:classic s2:classic s3:classic s4:classic s5:classic c6:classic c7:classic c8:classic c9:classic c10:classic cj:classic cq:classic ck:END
+Deal:3:classic c1:classic c2:classic c3:classic c4:classic c5:classic h6:classic h7:classic h8:classic h9:classic h10:classic hj:classic hq:classic hk:END
+
+# Pass
+Pass:3:classic c1:classic c2:classic c3:END
+Pass:2:classic s1:classic s2:classic s3:END
+Pass:0:classic h1:classic h2:classic h3:END
+Pass:1:classic d1:classic d2:classic d3:END
+
+# Take
+Take:1:END
+Take:2:END
+Take:0:END
+Take:3:END
+
+# 0 has all diamonds except for h4 and h5
+# 1 has all spades except for d4 and d5
+# 2 has all clubs except for s4 and s5
+# 3 has all hearts except for c4 and c5
+
+# Trick 1 (2 leads with 2 of clubs)
+Play:2:classic c2:END
+Play:3:classic c4:END
+Play:0:classic d1:END
+Play:1:classic s1:END
+
+# Trick 2 (3 won last round with 4 of clubs)
+Play:3:classic c5:END
+Play:0:classic d2:END
+Play:1:classic s2:END
+Play:2:classic c1:END
+
+# Trick 3 (2 won with ace of clubs)
+Play:2:classic s4:END
+Play:3:classic h1:END
+Play:0:classic h5:END
+Play:1:classic s3:END
+
+# Trick 4 (2 won with s4)
+Play:2:classic s5:END
+Play:3:classic hk:END
+Play:0:classic h4:END
+Play:1:classic sk:END
+
+# Trick 5 (1 won with sk)
+Play:1:classic d5:END
+Play:2:classic ck:END
+Play:3:classic hq:END
+Play:0:classic d3:END
+
+# Trick 6 (1 won with d5)
+Play:1:classic d4:END
+Play:2:classic cq:END
+Play:3:classic hj:END
+Play:0:classic dk:END
+
+# Trick 7 (0 won with dk)
+Play:0:classic dq:END
+Play:1:classic sq:END
+Play:2:classic c3:END
+Play:3:classic h2:END
+
+# Trick 8 (0 won with dq)
+Play:0:classic dj:END
+Play:1:classic sj:END
+Play:2:classic cj:END
+Play:3:classic h3:END
+
+# Trick 9 (0 won with dj)
+Play:0:classic d10:END
+Play:1:classic s10:END
+Play:2:classic c10:END
+Play:3:classic h10:END
+
+# Trick 10 (0 won with d10)
+Play:0:classic d9:END
+Play:1:classic s9:END
+Play:2:classic c9:END
+Play:3:classic h9:END
+
+# Trick 11 (0 won with d9)
+Play:0:classic d8:END
+Play:1:classic s8:END
+Play:2:classic c8:END
+Play:3:classic h8:END
+
+# Trick 12 (0 won with d8)
+Play:0:classic d7:END
+Play:1:classic s7:END
+Play:2:classic c7:END
+Play:3:classic h7:END
+
+# Trick 13 (0 won with d7)
+Play:0:classic d6:END
+Play:1:classic s6:END
+Play:2:classic c6:END
+Play:3:classic h6:END
+
+# Score Phase (ready)
+Ready:2:END
+Ready:0:END
+Ready:3:END
+Ready:1:END
+
+# The score is [21, 3, 2, 0]
+
+# 2nd Round here
+
+# Deal
+Deal:0:classic d1:classic d2:classic d3:classic c4:classic c5:classic c6:classic c7:classic c8:classic c9:classic c10:classic cj:classic cq:classic ck:END
+Deal:1:classic h1:classic h2:classic h3:classic s4:classic d5:classic d6:classic d7:classic d8:classic d9:classic d10:classic dj:classic dq:classic dk:END
+Deal:2:classic s1:classic s2:classic s3:classic d4:classic h5:classic h6:classic h7:classic h8:classic h9:classic h10:classic hj:classic hq:classic hk:END
+Deal:3:classic c1:classic c2:classic c3:classic h4:classic s5:classic s6:classic s7:classic s8:classic s9:classic s10:classic sj:classic sq:classic sk:END
+
+# Pass
+Pass:1:classic h1:classic h2:classic h3:END
+Pass:2:classic s1:classic s2:classic s3:END
+Pass:3:classic c1:classic c2:classic c3:END
+Pass:0:classic d1:classic d2:classic d3:END
+
+# Take
+Take:1:END
+Take:2:END
+Take:0:END
+Take:3:END
+
+# Trick 1
+Play:0:classic c2:END
+Play:1:classic d2:END
+Play:2:classic d4:END
+Play:3:classic s2:END
+
+# Trick 2
+Play:0:classic c3:END
+Play:1:classic d3:END
+Play:2:classic h3:END
+Play:3:classic s3:END
+
+# Trick 3
+Play:0:classic c4:END
+Play:1:classic s4:END
+Play:2:classic h2:END
+Play:3:classic h4:END
+
+# Trick 4
+Play:0:classic c5:END
+Play:1:classic d5:END
+Play:2:classic h5:END
+Play:3:classic s5:END
+
+# Trick 5
+Play:0:classic c6:END
+Play:1:classic d6:END
+Play:2:classic h6:END
+Play:3:classic s6:END
+
+# Trick 6
+Play:0:classic c7:END
+Play:1:classic d7:END
+Play:2:classic h7:END
+Play:3:classic s7:END
+
+# Trick 7
+Play:0:classic c8:END
+Play:1:classic d8:END
+Play:2:classic h8:END
+Play:3:classic s8:END
+
+# Trick 8
+Play:0:classic c9:END
+Play:1:classic d9:END
+Play:2:classic h9:END
+Play:3:classic s9:END
+
+# Trick 9
+Play:0:classic c1:END
+Play:1:classic d1:END
+Play:2:classic h1:END
+Play:3:classic s1:END
+
+# Trick 10
+Play:0:classic c10:END
+Play:1:classic d10:END
+Play:2:classic h10:END
+Play:3:classic s10:END
+
+# Trick 11
+Play:0:classic cj:END
+Play:1:classic dj:END
+Play:2:classic hj:END
+Play:3:classic sj:END
+
+# Trick 12
+Play:0:classic cq:END
+Play:1:classic dq:END
+Play:2:classic hq:END
+Play:3:classic sq:END
+
+# Trick 13
+Play:0:classic ck:END
+Play:1:classic dk:END
+Play:2:classic hk:END
+Play:3:classic sk:END
+
+# Score Phase (ready)
+Ready:2:END
+Ready:0:END
+Ready:3:END
+Ready:1:END
+
+# 3rd Round here
+
+# Deal
+Deal:0:classic h1:classic h2:classic h3:classic c4:classic c5:classic c6:classic c7:classic c8:classic c9:classic c10:classic cj:classic cq:classic ck:END
+Deal:1:classic s1:classic s2:classic s3:classic s4:classic d5:classic d6:classic d7:classic d8:classic d9:classic d10:classic dj:classic dq:classic dk:END
+Deal:2:classic c1:classic c2:classic c3:classic d4:classic h5:classic h6:classic h7:classic h8:classic h9:classic h10:classic hj:classic hq:classic hk:END
+Deal:3:classic d1:classic d2:classic d3:classic h4:classic s5:classic s6:classic s7:classic s8:classic s9:classic s10:classic sj:classic sq:classic sk:END
+
+# Pass
+Pass:0:classic h1:classic h2:classic h3:END
+Pass:1:classic s1:classic s2:classic s3:END
+Pass:2:classic c1:classic c2:classic c3:END
+Pass:3:classic d1:classic d2:classic d3:END
+
+# Take
+Take:1:END
+Take:2:END
+Take:0:END
+Take:3:END
+
+# Trick 1
+Play:0:classic c2:END
+Play:1:classic d2:END
+Play:2:classic d4:END
+Play:3:classic s2:END
+
+# Trick 2
+Play:0:classic c3:END
+Play:1:classic d3:END
+Play:2:classic h3:END
+Play:3:classic s3:END
+
+# Trick 3
+Play:0:classic c4:END
+Play:1:classic s4:END
+Play:2:classic h2:END
+Play:3:classic h4:END
+
+# Trick 4
+Play:0:classic c5:END
+Play:1:classic d5:END
+Play:2:classic h5:END
+Play:3:classic s5:END
+
+# Trick 5
+Play:0:classic c6:END
+Play:1:classic d6:END
+Play:2:classic h6:END
+Play:3:classic s6:END
+
+# Trick 6
+Play:0:classic c7:END
+Play:1:classic d7:END
+Play:2:classic h7:END
+Play:3:classic s7:END
+
+# Trick 7
+Play:0:classic c8:END
+Play:1:classic d8:END
+Play:2:classic h8:END
+Play:3:classic s8:END
+
+# Trick 8
+Play:0:classic c9:END
+Play:1:classic d9:END
+Play:2:classic h9:END
+Play:3:classic s9:END
+
+# Trick 9
+Play:0:classic c1:END
+Play:1:classic d1:END
+Play:2:classic h1:END
+Play:3:classic s1:END
+
+# Trick 10
+Play:0:classic c10:END
+Play:1:classic d10:END
+Play:2:classic h10:END
+Play:3:classic s10:END
+
+# Trick 11
+Play:0:classic cj:END
+Play:1:classic dj:END
+Play:2:classic hj:END
+Play:3:classic sj:END
+
+# Trick 12
+Play:0:classic cq:END
+Play:1:classic dq:END
+Play:2:classic hq:END
+Play:3:classic sq:END
+
+# Trick 13
+Play:0:classic ck:END
+Play:1:classic dk:END
+Play:2:classic hk:END
+Play:3:classic sk:END
+
+# Score Phase (ready)
+Ready:2:END
+Ready:0:END
+Ready:3:END
+Ready:1:END
+
+# 4th round here
+
+# Deal
+Deal:0:classic c1:classic c2:classic c3:classic c4:classic c5:classic c6:classic c7:classic c8:classic c9:classic c10:classic cj:classic cq:classic ck:END
+Deal:1:classic d1:classic d2:classic d3:classic s4:classic d5:classic d6:classic d7:classic d8:classic d9:classic d10:classic dj:classic dq:classic dk:END
+Deal:2:classic h1:classic h2:classic h3:classic d4:classic h5:classic h6:classic h7:classic h8:classic h9:classic h10:classic hj:classic hq:classic hk:END
+Deal:3:classic s1:classic s2:classic s3:classic h4:classic s5:classic s6:classic s7:classic s8:classic s9:classic s10:classic sj:classic sq:classic sk:END
+
+# Trick 1
+Play:0:classic c2:END
+Play:1:classic d2:END
+Play:2:classic d4:END
+Play:3:classic s2:END
+
+# Trick 2
+Play:0:classic c3:END
+Play:1:classic d3:END
+Play:2:classic h3:END
+Play:3:classic s3:END
+
+# Trick 3
+Play:0:classic c4:END
+Play:1:classic s4:END
+Play:2:classic h2:END
+Play:3:classic h4:END
+
+# Trick 4
+Play:0:classic c5:END
+Play:1:classic d5:END
+Play:2:classic h5:END
+Play:3:classic s5:END
+
+# Trick 5
+Play:0:classic c6:END
+Play:1:classic d6:END
+Play:2:classic h6:END
+Play:3:classic s6:END
+
+# Trick 6
+Play:0:classic c7:END
+Play:1:classic d7:END
+Play:2:classic h7:END
+Play:3:classic s7:END
+
+# Trick 7
+Play:0:classic c8:END
+Play:1:classic d8:END
+Play:2:classic h8:END
+Play:3:classic s8:END
+
+# Trick 8
+Play:0:classic c9:END
+Play:1:classic d9:END
+Play:2:classic h9:END
+Play:3:classic s9:END
+
+# Trick 9
+Play:0:classic c1:END
+Play:1:classic d1:END
+Play:2:classic h1:END
+Play:3:classic s1:END
+
+# Trick 10
+Play:0:classic c10:END
+Play:1:classic d10:END
+Play:2:classic h10:END
+Play:3:classic s10:END
+
+# Trick 11
+Play:0:classic cj:END
+Play:1:classic dj:END
+Play:2:classic hj:END
+Play:3:classic sj:END
+
+# Trick 12
+Play:0:classic cq:END
+Play:1:classic dq:END
+Play:2:classic hq:END
+Play:3:classic sq:END
+
+# Trick 13
+Play:0:classic ck:END
+Play:1:classic dk:END
+Play:2:classic hk:END
+Play:3:classic sk:END
+
+# Score Phase (ready)
+Ready:2:END
+Ready:0:END
+Ready:3:END
+Ready:1:END
+
+# 5th round here
+
+# Deal
+Deal:0:classic s1:classic s2:classic s3:classic c4:classic c5:classic c6:classic c7:classic c8:classic c9:classic c10:classic cj:classic cq:classic ck:END
+Deal:1:classic c1:classic c2:classic c3:classic s4:classic d5:classic d6:classic d7:classic d8:classic d9:classic d10:classic dj:classic dq:classic dk:END
+Deal:2:classic d1:classic d2:classic d3:classic d4:classic h5:classic h6:classic h7:classic h8:classic h9:classic h10:classic hj:classic hq:classic hk:END
+Deal:3:classic h1:classic h2:classic h3:classic h4:classic s5:classic s6:classic s7:classic s8:classic s9:classic s10:classic sj:classic sq:classic sk:END
+
+# Pass
+Pass:3:classic h1:classic h2:classic h3:END
+Pass:0:classic s1:classic s2:classic s3:END
+Pass:1:classic c1:classic c2:classic c3:END
+Pass:2:classic d1:classic d2:classic d3:END
+
+# Take
+Take:1:END
+Take:2:END
+Take:0:END
+Take:3:END
+
+# Trick 1
+Play:0:classic c2:END
+Play:1:classic d2:END
+Play:2:classic d4:END
+Play:3:classic s2:END
+
+# Trick 2
+Play:0:classic c3:END
+Play:1:classic d3:END
+Play:2:classic h3:END
+Play:3:classic s3:END
+
+# Trick 3
+Play:0:classic c4:END
+Play:1:classic s4:END
+Play:2:classic h2:END
+Play:3:classic h4:END
+
+# Trick 4
+Play:0:classic c5:END
+Play:1:classic d5:END
+Play:2:classic h5:END
+Play:3:classic s5:END
+
+# Trick 5
+Play:0:classic c6:END
+Play:1:classic d6:END
+Play:2:classic h6:END
+Play:3:classic s6:END
+
+# Trick 6
+Play:0:classic c7:END
+Play:1:classic d7:END
+Play:2:classic h7:END
+Play:3:classic s7:END
+
+# Trick 7
+Play:0:classic c8:END
+Play:1:classic d8:END
+Play:2:classic h8:END
+Play:3:classic s8:END
+
+# Trick 8
+Play:0:classic c9:END
+Play:1:classic d9:END
+Play:2:classic h9:END
+Play:3:classic s9:END
+
+# Trick 9
+Play:0:classic c1:END
+Play:1:classic d1:END
+Play:2:classic h1:END
+Play:3:classic s1:END
+
+# Trick 10
+Play:0:classic c10:END
+Play:1:classic d10:END
+Play:2:classic h10:END
+Play:3:classic s10:END
+
+# Trick 11
+Play:0:classic cj:END
+Play:1:classic dj:END
+Play:2:classic hj:END
+Play:3:classic sj:END
+
+# Trick 12
+Play:0:classic cq:END
+Play:1:classic dq:END
+Play:2:classic hq:END
+Play:3:classic sq:END
+
+# Trick 13
+Play:0:classic ck:END
+Play:1:classic dk:END
+Play:2:classic hk:END
+Play:3:classic sk:END
+
+# Game is over!
\ No newline at end of file
diff --git a/test/hearts_test.dart b/test/hearts_test.dart
index ffdb360..85fc692 100644
--- a/test/hearts_test.dart
+++ b/test/hearts_test.dart
@@ -1,77 +1,445 @@
import "package:test/test.dart";
import "../lib/logic/game.dart";
+import "../lib/logic/card.dart";
+
+import "dart:io";
void main() {
- HeartsGame game = new HeartsGame(0);
-
- group("Card Manipulation", () {
+ group("Initialization", () {
+ HeartsGame game = new HeartsGame(0);
test("Dealing", () {
+ game.dealCards(); // What the dealer actually runs to get cards to everybody.
+
// By virtue of creating the game, HeartsGame should have 4 collections with 13 cards and 8 collections with 0 cards each.
-
- });
- test("Passing", () {
-
- });
- test("Playing", () {
-
+ expect(game.cardCollections[HeartsGame.PLAYER_A + HeartsGame.OFFSET_HAND].length, equals(13), reason: "Dealt 13 cards to A");
+ expect(game.cardCollections[HeartsGame.PLAYER_B + HeartsGame.OFFSET_HAND].length, equals(13), reason: "Dealt 13 cards to B");
+ expect(game.cardCollections[HeartsGame.PLAYER_C + HeartsGame.OFFSET_HAND].length, equals(13), reason: "Dealt 13 cards to C");
+ expect(game.cardCollections[HeartsGame.PLAYER_D + HeartsGame.OFFSET_HAND].length, equals(13), reason: "Dealt 13 cards to D");
+ expect(game.cardCollections[HeartsGame.PLAYER_A + HeartsGame.OFFSET_PLAY].length, equals(0), reason: "Not playing yet");
+ expect(game.cardCollections[HeartsGame.PLAYER_B + HeartsGame.OFFSET_PLAY].length, equals(0), reason: "Not playing yet");
+ expect(game.cardCollections[HeartsGame.PLAYER_C + HeartsGame.OFFSET_PLAY].length, equals(0), reason: "Not playing yet");
+ expect(game.cardCollections[HeartsGame.PLAYER_D + HeartsGame.OFFSET_PLAY].length, equals(0), reason: "Not playing yet");
+ expect(game.cardCollections[HeartsGame.PLAYER_A + HeartsGame.OFFSET_PASS].length, equals(0), reason: "Not passing yet");
+ expect(game.cardCollections[HeartsGame.PLAYER_B + HeartsGame.OFFSET_PASS].length, equals(0), reason: "Not passing yet");
+ expect(game.cardCollections[HeartsGame.PLAYER_C + HeartsGame.OFFSET_PASS].length, equals(0), reason: "Not passing yet");
+ expect(game.cardCollections[HeartsGame.PLAYER_D + HeartsGame.OFFSET_PASS].length, equals(0), reason: "Not passing yet");
+ expect(game.cardCollections[HeartsGame.PLAYER_A + HeartsGame.OFFSET_TRICK].length, equals(0), reason: "No tricks yet");
+ expect(game.cardCollections[HeartsGame.PLAYER_B + HeartsGame.OFFSET_TRICK].length, equals(0), reason: "No tricks yet");
+ expect(game.cardCollections[HeartsGame.PLAYER_C + HeartsGame.OFFSET_TRICK].length, equals(0), reason: "No tricks yet");
+ expect(game.cardCollections[HeartsGame.PLAYER_D + HeartsGame.OFFSET_TRICK].length, equals(0), reason: "No tricks yet");
});
});
+
+ // For this test, the cards may end up being duplicate or inconsistent.
+ group("Scoring", () {
+ HeartsGame game = new HeartsGame(0);
+ test("Compute/Prepare Score", () {
+ // In this situation, what's the score?
+ game.cardCollections[HeartsGame.PLAYER_A_TRICK] = <Card>[
+ new Card("classic", "dq"),
+ new Card("classic", "dk"),
+ new Card("classic", "h1"),
+ new Card("classic", "h2"),
+ new Card("classic", "h3"),
+ new Card("classic", "h4")
+ ];
+
+ expect(game.computeScore(HeartsGame.PLAYER_A), equals(4), reason: "Player A has 4 hearts");
+
+ // In this alternative situation, what's the score?
+ game.cardCollections[HeartsGame.PLAYER_B_TRICK] = <Card>[
+ new Card("classic", "h6"),
+ new Card("classic", "h7"),
+ new Card("classic", "h8"),
+ new Card("classic", "h9"),
+ new Card("classic", "h10"),
+ new Card("classic", "hj"),
+ new Card("classic", "hq"),
+ new Card("classic", "hk"),
+ new Card("classic", "s1"),
+ new Card("classic", "s2")
+ ];
+
+ expect(game.computeScore(HeartsGame.PLAYER_B), equals(8), reason: "Player B has 8 hearts.");
+
+ // Should prepare C as well.
+ game.cardCollections[HeartsGame.PLAYER_C_TRICK] = <Card>[
+ new Card("classic", "h5"),
+ new Card("classic", "sq")
+ ];
+ expect(game.computeScore(HeartsGame.PLAYER_C), equals(14), reason: "Player C has 1 heart and the queen of spades.");
+
+ // Now, update the score, modifying game.scores.
+ game.updateScore();
+ expect(game.scores, equals([4, 8, 14, 0]));
+
+ // Do it again.
+ game.updateScore();
+ expect(game.scores, equals([8, 16, 28, 0]));
+
+ // Shoot the moon!
+ game.cardCollections[HeartsGame.PLAYER_A_TRICK] = <Card>[];
+ game.cardCollections[HeartsGame.PLAYER_B_TRICK] = <Card>[];
+ game.cardCollections[HeartsGame.PLAYER_C_TRICK] = <Card>[];
+ game.cardCollections[HeartsGame.PLAYER_D_TRICK] = Card.All;
+ game.updateScore();
+ expect(game.scores, equals([34, 42, 54, 0]));
+ });
+ });
+
+ group("Game Over", () {
+ HeartsGame game = new HeartsGame(0);
+
+ test("Has the game ended? Yes", () {
+ // Check if the game has ended. Should be yes.
+ game.scores = <int>[HeartsGame.MAX_SCORE + 5, 40, 35, 0];
+ expect(game.hasGameEnded, isTrue);
+ });
+ test("Has the game ended? No", () {
+ // Check if the game has ended. Should be no.
+ game.scores = <int>[HeartsGame.MAX_SCORE - 5, 40, 35, 0];
+ expect(game.hasGameEnded, isFalse);
+ });
+ });
+
+ // At this point, we should prepare the canonical game by setting up state and
+ // performing a single action or set of actions.
+ // Reads from a log, so we will go through logical game mechanics.
+ group("Card Manipulation", () {
+ HeartsGame game = new HeartsGame(0);
+
+ // Note: This could have been a non-file (in-memory), but it's fine to use a file too.
+ File file = new File("test/game_log_hearts_test.txt");
+ List<String> commands = file.readAsStringSync().split("\n");
+ int commandIndex = 0;
+
+ void runCommand() {
+ String c = commands[commandIndex];
+ commandIndex++;
+ if (c == "" || c[0] == "#") { // Essentially, this case allows empty lines and comments.
+ runCommand();
+ } else {
+ game.gamelog.add(new HeartsCommand(c));
+ }
+ }
+
+ test("Deal Phase", () {
+ expect(game.phase, equals(HeartsPhase.Deal));
+
+ // Deal consists of 4 deal commands.
+ runCommand();
+ runCommand();
+ runCommand();
+ runCommand();
+
+ // Confirm cards in hands.
+ List<Card> expectedAHand = new List<Card>.from(Card.All.getRange(26, 26+5))..addAll(Card.All.getRange(13+5, 26));
+ List<Card> expectedBHand = new List<Card>.from(Card.All.getRange(13, 13+5))..addAll(Card.All.getRange(39+5, 52));
+ List<Card> expectedCHand = new List<Card>.from(Card.All.getRange(39, 39+5))..addAll(Card.All.getRange(0+5, 13));
+ List<Card> expectedDHand = new List<Card>.from(Card.All.getRange(0, 0+5))..addAll(Card.All.getRange(26+5, 39));
+ expect(game.cardCollections[HeartsGame.PLAYER_A], equals(expectedAHand));
+ expect(game.cardCollections[HeartsGame.PLAYER_B], equals(expectedBHand));
+ expect(game.cardCollections[HeartsGame.PLAYER_C], equals(expectedCHand));
+ expect(game.cardCollections[HeartsGame.PLAYER_D], equals(expectedDHand));
+ });
+ test("Pass Phase", () {
+ expect(game.phase, equals(HeartsPhase.Pass));
+
+ // Pass consists of 4 pass commands.
+ runCommand();
+ runCommand();
+ runCommand();
+ runCommand();
+
+ // Confirm cards in hands and passes.
+ List<Card> expectedAHand = new List<Card>.from(Card.All.getRange(26+3, 26+5))..addAll(Card.All.getRange(13+5, 26));
+ List<Card> expectedBHand = new List<Card>.from(Card.All.getRange(13+3, 13+5))..addAll(Card.All.getRange(39+5, 52));
+ List<Card> expectedCHand = new List<Card>.from(Card.All.getRange(39+3, 39+5))..addAll(Card.All.getRange(0+5, 13));
+ List<Card> expectedDHand = new List<Card>.from(Card.All.getRange(0+3, 0+5))..addAll(Card.All.getRange(26+5, 39));
+ List<Card> expectedAPass = new List<Card>.from(Card.All.getRange(26, 26+3));
+ List<Card> expectedBPass = new List<Card>.from(Card.All.getRange(13, 13+3));
+ List<Card> expectedCPass = new List<Card>.from(Card.All.getRange(39, 39+3));
+ List<Card> expectedDPass = new List<Card>.from(Card.All.getRange(0, 0+3));
+ expect(game.cardCollections[HeartsGame.PLAYER_A], equals(expectedAHand));
+ expect(game.cardCollections[HeartsGame.PLAYER_B], equals(expectedBHand));
+ expect(game.cardCollections[HeartsGame.PLAYER_C], equals(expectedCHand));
+ expect(game.cardCollections[HeartsGame.PLAYER_D], equals(expectedDHand));
+ expect(game.cardCollections[HeartsGame.PLAYER_A_PASS], equals(expectedAPass));
+ expect(game.cardCollections[HeartsGame.PLAYER_B_PASS], equals(expectedBPass));
+ expect(game.cardCollections[HeartsGame.PLAYER_C_PASS], equals(expectedCPass));
+ expect(game.cardCollections[HeartsGame.PLAYER_D_PASS], equals(expectedDPass));
+ });
+ test("Take Phase", () {
+ expect(game.phase, equals(HeartsPhase.Take));
+
+ // Take consists of 4 take commands.
+ runCommand();
+ runCommand();
+ runCommand();
+ runCommand();
+
+ // Confirm cards in hands again.
+ // Note: I will eventually want to do a sorted comparison or set comparison instead.
+ List<Card> expectedAHand = new List<Card>.from(Card.All.getRange(26+3, 26+5))
+ ..addAll(Card.All.getRange(13+5, 26))
+ ..addAll(Card.All.getRange(13, 13+3));
+ List<Card> expectedBHand = new List<Card>.from(Card.All.getRange(13+3, 13+5))
+ ..addAll(Card.All.getRange(39+5, 52))
+ ..addAll(Card.All.getRange(39, 39+3));
+ List<Card> expectedCHand = new List<Card>.from(Card.All.getRange(39+3, 39+5))
+ ..addAll(Card.All.getRange(0+5, 13))
+ ..addAll(Card.All.getRange(0, 0+3));
+ List<Card> expectedDHand = new List<Card>.from(Card.All.getRange(0+3, 0+5))
+ ..addAll(Card.All.getRange(26+5, 39))
+ ..addAll(Card.All.getRange(26, 26+3));
+ expect(game.cardCollections[HeartsGame.PLAYER_A], equals(expectedAHand));
+ expect(game.cardCollections[HeartsGame.PLAYER_B], equals(expectedBHand));
+ expect(game.cardCollections[HeartsGame.PLAYER_C], equals(expectedCHand));
+ expect(game.cardCollections[HeartsGame.PLAYER_D], equals(expectedDHand));
+
+ });
+ test("Play Phase - Trick 1", () {
+ expect(game.phase, equals(HeartsPhase.Play));
+
+ // Play Trick 1 consists of 4 play commands.
+ runCommand();
+ runCommand();
+ runCommand();
+ runCommand();
+
+ // Confirm the winner of the round.
+ expect(game.lastTrickTaker, equals(3), reason: "Player 3 played 4 of Clubs");
+ expect(game.cardCollections[HeartsGame.PLAYER_D_TRICK].length, equals(4), reason: "Player 3 won 1 trick.");
+ });
+ test("Play Phase - Trick 2", () {
+ expect(game.phase, equals(HeartsPhase.Play));
+
+ // Play Trick 2 consists of 4 play commands.
+ runCommand();
+ runCommand();
+ runCommand();
+ runCommand();
+
+ // Confirm the winner of the round.
+ expect(game.lastTrickTaker, equals(2), reason: "Player 2 played Ace of Clubs");
+ expect(game.cardCollections[HeartsGame.PLAYER_C_TRICK].length, equals(4), reason: "Player 2 won 1 trick.");
+ expect(game.cardCollections[HeartsGame.PLAYER_D_TRICK].length, equals(4), reason: "Player 3 won 1 trick.");
+
+ });
+ test("Play Phase - Trick 13", () {
+ expect(game.phase, equals(HeartsPhase.Play));
+
+ // Play Trick 13 consists of 44 play commands.
+ // Read line by line until the game is "over".
+ for (int i = 8; i < 52; i++) {
+ runCommand();
+ }
+
+ // Assert that hands/plays/passes are empty.
+ expect(game.cardCollections[HeartsGame.PLAYER_A + HeartsGame.OFFSET_HAND].length, equals(0), reason: "Played all cards");
+ expect(game.cardCollections[HeartsGame.PLAYER_B + HeartsGame.OFFSET_HAND].length, equals(0), reason: "Played all cards");
+ expect(game.cardCollections[HeartsGame.PLAYER_C + HeartsGame.OFFSET_HAND].length, equals(0), reason: "Played all cards");
+ expect(game.cardCollections[HeartsGame.PLAYER_D + HeartsGame.OFFSET_HAND].length, equals(0), reason: "Played all cards");
+
+ // Check that all 52 cards are in tricks.
+ expect(game.lastTrickTaker, equals(0), reason: "Player 0 won the last trick.");
+ expect(game.cardCollections[HeartsGame.PLAYER_A_TRICK].length, equals(4*8), reason: "Player 0 won 8 tricks.");
+ expect(game.cardCollections[HeartsGame.PLAYER_B_TRICK].length, equals(4*2), reason: "Player 1 won 2 tricks.");
+ expect(game.cardCollections[HeartsGame.PLAYER_C_TRICK].length, equals(4*2), reason: "Player 2 won 2 tricks.");
+ expect(game.cardCollections[HeartsGame.PLAYER_D_TRICK].length, equals(4), reason: "Player 3 won 1 trick.");
+ });
+ test("Score Phase", () {
+ expect(game.phase, equals(HeartsPhase.Score));
+
+ // Check score to ensure it matches the expectation.
+ expect(game.scores, equals([21, 3, 2, 0]));
+
+ // Score consists of 4 ready commands.
+ runCommand();
+ expect(game.allReady, isFalse);
+ runCommand();
+ expect(game.allReady, isFalse);
+ runCommand();
+ expect(game.allReady, isFalse);
+ runCommand();
+
+ // Back to the deal phase once everyone indicates that they are ready.
+ expect(game.phase, equals(HeartsPhase.Deal));
+ });
+ test("Score Phase - end of game", () {
+ expect(game.hasGameEnded, isFalse);
+
+ // 2nd Round: 4 deal, 4 pass, 4 take, 52 play, 4 ready
+ // Player A will shoot the moon for all remaining games (for simplicity).
+ for (int i = 0; i < 68; i++) {
+ runCommand();
+ }
+ expect(game.scores, equals([21+0, 3+26, 2+26, 0+26]));
+ expect(game.hasGameEnded, isFalse);
+
+ // 3rd Round: 4 deal, 4 pass, 4 take, 52 play, 4 ready
+ for (int i = 0; i < 68; i++) {
+ runCommand();
+ }
+ expect(game.scores, equals([21+0+0, 3+26+26, 2+26+26, 0+26+26]));
+ expect(game.hasGameEnded, isFalse);
+
+ // 4th Round: 4 deal, 52 play, 4 ready
+ for (int i = 0; i < 60; i++) {
+ runCommand();
+ }
+ expect(game.scores, equals([21+0+0+0, 3+26+26+26, 2+26+26+26, 0+26+26+26]));
+ expect(game.hasGameEnded, isFalse);
+
+ // 5th round: 4 deal, 4 pass, 4 take, 52 play. Game is over, so no ready phase.
+ for (int i = 0; i < 64; i++) {
+ runCommand();
+ }
+ expect(game.scores, equals([21+0+0+0+0, 3+26+26+26+26, 2+26+26+26+26, 0+26+26+26+26]));
+ expect(game.hasGameEnded, isTrue); // assumes game ends after about 100 points.
+ });
+ });
+
group("Card Manipulation - Error Cases", () {
- test("Dealing - missing card", () {
-
- });
- test("Dealing - wrong number of cards", () {
-
- });
test("Dealing - wrong phase", () {
-
+ expect(() {
+ HeartsGame game = new HeartsGame(0);
+ game.phase = HeartsPhase.Score;
+ game.gamelog.add(new HeartsCommand.deal(0, new List<Card>.from(Card.All.getRange(0, 13))));
+ }, throwsA(new isInstanceOf<StateError>()));
});
- test("Passing - missing card", () {
-
+ test("Dealing - missing card", () {
+ expect(() {
+ HeartsGame game = new HeartsGame(0);
+ game.gamelog.add(new HeartsCommand.deal(0, <Card>[new Card("fake", "not real")]));
+ }, throwsA(new isInstanceOf<StateError>()));
});
- test("Passing - wrong number of cards", () {
-
+ test("Dealing - too many cards dealt", () {
+ expect(() {
+ HeartsGame game = new HeartsGame(0);
+ game.gamelog.add(new HeartsCommand.deal(0, new List<Card>.from(Card.All.getRange(0, 15))));
+ }, throwsA(new isInstanceOf<StateError>()));
+ expect(() {
+ HeartsGame game = new HeartsGame(0);
+ game.gamelog.add(new HeartsCommand.deal(0, new List<Card>.from(Card.All.getRange(0, 5))));
+ game.gamelog.add(new HeartsCommand.deal(0, new List<Card>.from(Card.All.getRange(5, 15))));
+ }, throwsA(new isInstanceOf<StateError>()));
});
test("Passing - wrong phase", () {
-
+ expect(() {
+ HeartsGame game = new HeartsGame(0);
+ game.gamelog.add(new HeartsCommand.deal(0, new List<Card>.from(Card.All.getRange(0, 13))));
+ game.gamelog.add(new HeartsCommand.pass(0, new List<Card>.from(Card.All.getRange(0, 4))));
+ }, throwsA(new isInstanceOf<StateError>()));
+ });
+ test("Passing - missing card", () {
+ expect(() {
+ HeartsGame game = new HeartsGame(0);
+ game.gamelog.add(new HeartsCommand.deal(0, new List<Card>.from(Card.All.getRange(0, 13))));
+ game.phase = HeartsPhase.Pass;
+ game.gamelog.add(new HeartsCommand.pass(0, new List<Card>.from(Card.All.getRange(13, 16))));
+ }, throwsA(new isInstanceOf<StateError>()));
+ });
+ test("Passing - wrong number of cards", () {
+ expect(() {
+ HeartsGame game = new HeartsGame(0);
+ game.gamelog.add(new HeartsCommand.deal(0, new List<Card>.from(Card.All.getRange(0, 13))));
+ game.phase = HeartsPhase.Pass;
+ game.gamelog.add(new HeartsCommand.pass(0, new List<Card>.from(Card.All.getRange(0, 2))));
+ }, throwsA(new isInstanceOf<StateError>()));
+ expect(() {
+ HeartsGame game = new HeartsGame(0);
+ game.gamelog.add(new HeartsCommand.deal(0, new List<Card>.from(Card.All.getRange(0, 13))));
+ game.phase = HeartsPhase.Pass;
+ game.gamelog.add(new HeartsCommand.pass(0, new List<Card>.from(Card.All.getRange(0, 4))));
+ }, throwsA(new isInstanceOf<StateError>()));
+ });
+ test("Taking - wrong phase", () {
+ expect(() {
+ HeartsGame game = new HeartsGame(0);
+ game.gamelog.add(new HeartsCommand.take(3));
+ }, throwsA(new isInstanceOf<StateError>()));
+ });
+ test("Playing - wrong phase", () {
+ expect(() {
+ HeartsGame game = new HeartsGame(0);
+ game.gamelog.add(new HeartsCommand.deal(0, new List<Card>.from(Card.All.getRange(0, 13))));
+ game.gamelog.add(new HeartsCommand.play(0, Card.All[0]));
+ }, throwsA(new isInstanceOf<StateError>()));
});
test("Playing - missing card", () {
-
+ expect(() {
+ HeartsGame game = new HeartsGame(0);
+ game.gamelog.add(new HeartsCommand.deal(0, new List<Card>.from(Card.All.getRange(0, 13))));
+ game.phase = HeartsPhase.Play;
+ game.gamelog.add(new HeartsCommand.play(0, Card.All[13]));
+ }, throwsA(new isInstanceOf<StateError>()));
});
test("Playing - invalid card (not 2 of clubs as first card)", () {
-
+ expect(() {
+ HeartsGame game = new HeartsGame(0);
+ game.gamelog.add(new HeartsCommand.deal(0, new List<Card>.from(Card.All.getRange(0, 13))));
+ game.phase = HeartsPhase.Play;
+ game.lastTrickTaker = 0;
+ game.gamelog.add(new HeartsCommand.play(0, Card.All[0]));
+ }, throwsA(new isInstanceOf<StateError>()));
});
test("Playing - invalid card (no penalty on first round)", () {
// NOTE: It is actually possible to be forced to play a penalty card on round 1.
// But the odds are miniscule, so this rule will be enforced.
- });
- test("Playing - invalid card (suit mismatch)", () {
-
- });
- test("Playing - invalid card (hearts not broken yet)", () {
-
+ expect(() {
+ HeartsGame game = new HeartsGame(0);
+ game.gamelog.add(new HeartsCommand.deal(0, new List<Card>.from(Card.All.getRange(0, 13))));
+ game.gamelog.add(new HeartsCommand.deal(1, new List<Card>.from(Card.All.getRange(13, 26))));
+ game.gamelog.add(new HeartsCommand.deal(2, new List<Card>.from(Card.All.getRange(26, 39))));
+ game.gamelog.add(new HeartsCommand.deal(3, new List<Card>.from(Card.All.getRange(39, 52))));
+ game.phase = HeartsPhase.Play;
+ game.lastTrickTaker = 0;
+ game.gamelog.add(new HeartsCommand.play(0, Card.All[1]));
+ game.gamelog.add(new HeartsCommand.play(1, Card.All[13]));
+ game.gamelog.add(new HeartsCommand.play(2, Card.All[26]));
+ }, throwsA(new isInstanceOf<StateError>()));
});
test("Playing - wrong turn", () {
-
+ expect(() {
+ HeartsGame game = new HeartsGame(0);
+ game.gamelog.add(new HeartsCommand.deal(0, new List<Card>.from(Card.All.getRange(0, 13))));
+ game.gamelog.add(new HeartsCommand.deal(1, new List<Card>.from(Card.All.getRange(13, 26))));
+ game.gamelog.add(new HeartsCommand.deal(2, new List<Card>.from(Card.All.getRange(26, 39))));
+ game.gamelog.add(new HeartsCommand.deal(3, new List<Card>.from(Card.All.getRange(39, 52))));
+ game.phase = HeartsPhase.Play;
+ game.lastTrickTaker = 0;
+ game.gamelog.add(new HeartsCommand.play(1, Card.All[13])); // player 0's turn, not player 1's.
+ }, throwsA(new isInstanceOf<StateError>()));
});
- test("Playing - wrong phase", () {
-
+ test("Playing - invalid card (suit mismatch)", () {
+ expect(() {
+ HeartsGame game = new HeartsGame(0);
+ game.gamelog.add(new HeartsCommand.deal(0, new List<Card>.from(Card.All.getRange(0, 12))..add(Card.All[25])));
+ game.gamelog.add(new HeartsCommand.deal(1, new List<Card>.from(Card.All.getRange(12, 25))));
+ game.gamelog.add(new HeartsCommand.deal(2, new List<Card>.from(Card.All.getRange(26, 39))));
+ game.gamelog.add(new HeartsCommand.deal(3, new List<Card>.from(Card.All.getRange(39, 52))));
+ game.phase = HeartsPhase.Play;
+ game.lastTrickTaker = 0;
+ game.gamelog.add(new HeartsCommand.play(0, Card.All[1]));
+ game.gamelog.add(new HeartsCommand.play(0, Card.All[13])); // should play 12
+ }, throwsA(new isInstanceOf<StateError>()));
});
- });
- group("Scoring", () {
- test("Count Points", () {
- // In this situation, what's the score?
- });
- test("Count Points 2", () {
- // In this alternative situation, what's the score?
- });
- });
- group("Game Over", () {
- test("Has the game ended? Yes", () {
- // Check if the game has ended. Should be yes.
- });
- test("Has the game ended? No", () {
- // Check if the game has ended. Should be no.
+ test("Playing - invalid card (hearts not broken yet)", () {
+ expect(() {
+ HeartsGame game = new HeartsGame(0);
+ game.gamelog.add(new HeartsCommand.deal(0, new List<Card>.from(Card.All.getRange(0, 12))..add(Card.All[38])));
+ game.gamelog.add(new HeartsCommand.deal(1, new List<Card>.from(Card.All.getRange(13, 26))));
+ game.gamelog.add(new HeartsCommand.deal(2, new List<Card>.from(Card.All.getRange(26, 38))..add(Card.All[12])));
+ game.gamelog.add(new HeartsCommand.deal(3, new List<Card>.from(Card.All.getRange(39, 52))));
+ game.phase = HeartsPhase.Play;
+ game.lastTrickTaker = 0;
+ game.gamelog.add(new HeartsCommand.play(0, Card.All[1]));
+ game.gamelog.add(new HeartsCommand.play(1, Card.All[13]));
+ game.gamelog.add(new HeartsCommand.play(2, Card.All[12])); // 2 won!
+ game.gamelog.add(new HeartsCommand.play(3, Card.All[39]));
+ game.gamelog.add(new HeartsCommand.play(2, Card.All[26])); // But 2 can't lead with a hearts.
+ }, throwsA(new isInstanceOf<StateError>()));
});
});
}
\ No newline at end of file