blob: 3cae95f27f8c2234726801340ce5027b45d46ff0 [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.
import "package:test/test.dart";
import "../lib/logic/solitaire/solitaire.dart";
import "../lib/logic/card.dart";
import "dart:io";
typedef bool KeepGoingCb();
KeepGoingCb makeCommandReader(SolitaireGame game, String filename) {
File file = new File(filename);
List<String> commands = file.readAsStringSync().split("\n");
int commandIndex = 0;
KeepGoingCb runCommand;
runCommand = () {
if (commandIndex >= commands.length) {
return false;
}
String c = commands[commandIndex];
commandIndex++;
if (c == "" || c[0] == "#") {
// Essentially, this case allows empty lines and comments.
return runCommand();
}
game.gamelog.add(new SolitaireCommand.fromCommand(c));
return commandIndex < commands.length;
};
return runCommand;
}
void main() {
group("Initialization", () {
SolitaireGame game = new SolitaireGame();
test("Dealing", () {
game.dealCardsUI(); // What we run when starting the game.
// By virtue of creating the game, SolitaireGame should have:
// 0 in each Ace pile
// 0 in the discard and 24 in the draw pile
// 0 to 6 in each down pile
// 1 in each up pile
for (int i = 0; i < 4; i++) {
expect(game.cardCollections[SolitaireGame.offsetAces + i].length,
equals(0),
reason: "Ace piles start empty");
}
expect(
game.cardCollections[SolitaireGame.offsetDiscard].length, equals(0),
reason: "Discard pile starts empty");
expect(game.cardCollections[SolitaireGame.offsetDraw].length, equals(24),
reason: "Draw pile gets the remaining 24 cards");
for (int i = 0; i < 7; i++) {
expect(game.cardCollections[SolitaireGame.offsetDown + i].length,
equals(i),
reason: "Down pile $i starts with $i cards");
expect(
game.cardCollections[SolitaireGame.offsetUp + i].length, equals(1),
reason: "Up piles start with 1 card");
}
});
});
// We have a debug cheat button that lets you advance in the game.
// After calling it once, it is forced to place cards up into the aces area.
group("Cheat Command", () {
SolitaireGame game = new SolitaireGame();
game.dealCardsUI(); // Get the cards out there.
test("Cheat Functionality", () {
for (int cheatNum = 0; cheatNum < 13; cheatNum++) {
game.cheatUI();
for (int i = 0; i < 4; i++) {
expect(game.cardCollections[SolitaireGame.offsetAces + i].length,
equals(cheatNum + 1));
}
}
expect(game.isGameWon, isTrue);
game.cheatUI(); // Should not error
expect(game.isGameWon, isTrue); // We still win.
});
});
group("Check Endgame", () {
SolitaireGame game = new SolitaireGame();
test("Has not won pre-deal", () {
expect(game.phase, equals(SolitairePhase.deal));
expect(game.isGameWon, isFalse);
});
test("Has not won immediately after deal", () {
game.dealCardsUI(); // What we run when starting the game.
expect(game.phase, equals(SolitairePhase.play));
expect(game.isGameWon, isFalse);
});
test("Wins with all cards placed - regardless of order", () {
for (int i = 0; i < 13; i++) {
game.cheatUI(); // What we run when cheating.
}
expect(game.phase, equals(SolitairePhase.score));
expect(game.isGameWon, isTrue);
// Now check an alternative suit order.
List<Card> aces0 =
new List<Card>.from(game.cardCollections[SolitaireGame.offsetAces]);
List<Card> aces1 = new List<Card>.from(
game.cardCollections[SolitaireGame.offsetAces + 1]);
game.cardCollections[SolitaireGame.offsetAces].clear();
game.cardCollections[SolitaireGame.offsetAces + 1].clear();
// This expectation can be removed if isGameWon becomes a parameter, as
// opposed to a computed property. However, if that happens, this test
// will need to be reworked too, to start with a fresh game.
expect(game.isGameWon, isFalse);
// Swap the piles
game.cardCollections[SolitaireGame.offsetAces].addAll(aces1);
game.cardCollections[SolitaireGame.offsetAces + 1].addAll(aces0);
expect(game.isGameWon, isTrue);
});
});
// Run through a canonical game of Solitaire where the player doesn't win.
group("Solitaire - Loss", () {
SolitaireGame game = new SolitaireGame();
// Note: This could have been a non-file (in-memory), but it's fine to use a file too.
KeepGoingCb runCommand =
makeCommandReader(game, "test/game_log_solitaire_test_loss.txt");
test("Solitaire Commands", () {
bool keepGoing = true;
for (int i = 0; keepGoing; i++) {
if (i == 0) {
expect(game.phase, equals(SolitairePhase.deal));
} else if (!game.isGameWon) {
expect(game.phase, equals(SolitairePhase.play));
} else {
expect(game.phase, equals(SolitairePhase.score));
}
// Play the next step of the game until we run out.
keepGoing = runCommand(); // Must not error.
}
});
// Naturally, we should ensure that we haven't won the game.
test("Solitaire Win == False", () {
expect(game.isGameWon, isFalse);
expect(game.phase, equals(SolitairePhase.play));
});
});
// Run through a canonical game of Solitaire where the player does win.
group("Solitaire - Win", () {
SolitaireGame game = new SolitaireGame();
// Note: This could have been a non-file (in-memory), but it's fine to use a file too.
KeepGoingCb runCommand =
makeCommandReader(game, "test/game_log_solitaire_test_win.txt");
test("Solitaire Commands", () {
bool keepGoing = true;
for (int i = 0; keepGoing; i++) {
if (i == 0) {
expect(game.phase, equals(SolitairePhase.deal));
} else if (!game.isGameWon) {
expect(game.phase, equals(SolitairePhase.play));
} else {
expect(game.phase, equals(SolitairePhase.score));
}
// Play the next step of the game until we run out.
keepGoing = runCommand(); // Must not error.
}
});
// Check that we won the game.
test("Solitaire Win == True", () {
expect(game.isGameWon, isTrue);
expect(game.phase, equals(SolitairePhase.score));
});
});
group("Card Manipulation - Error Cases", () {
test("Dealing - wrong phase", () {
expect(() {
SolitaireGame game = new SolitaireGame();
game.phase = SolitairePhase.score;
game.gamelog
.add(new SolitaireCommand.deal(new List<Card>.from(Card.all)));
}, throwsA(new isInstanceOf<StateError>()));
});
test("Dealing - fake cards", () {
expect(() {
SolitaireGame game = new SolitaireGame();
game.gamelog.add(
new SolitaireCommand.deal(<Card>[new Card("fake", "not real")]));
}, throwsA(new isInstanceOf<StateError>()));
});
test("Dealing - wrong number of cards dealt", () {
// 2x as many cards
expect(() {
SolitaireGame game = new SolitaireGame();
game.gamelog.add(new SolitaireCommand.deal(
new List<Card>.from(Card.all)..addAll(Card.all)));
}, throwsA(new isInstanceOf<StateError>()));
// missing cards
expect(() {
SolitaireGame game = new SolitaireGame();
game.gamelog.add(new SolitaireCommand.deal(
new List<Card>.from(Card.all.getRange(0, 40))));
}, throwsA(new isInstanceOf<StateError>()));
});
// Set up an arbitrary game state (manually) that allows testing of many
// error cases.
test("Playing - cannot move", () {
// The setup will be this:
// s2 _ h2 _ d2 c11
// _ c3 h3 d1 s13 d3 d12
Card c3 = Card.all[0 + 2];
Card c11 = Card.all[0 + 10];
Card d1 = Card.all[13 + 0];
Card d2 = Card.all[13 + 1];
Card d3 = Card.all[13 + 2];
Card h2 = Card.all[26 + 1];
Card h3 = Card.all[26 + 2];
Card d12 = Card.all[13 + 11];
Card s2 = Card.all[39 + 1];
Card s13 = Card.all[39 + 12];
SolitaireGame _makeArbitrarySolitaireGame() {
SolitaireGame g = new SolitaireGame();
// Top row
g.cardCollections[SolitaireGame.offsetAces].add(s2);
g.cardCollections[SolitaireGame.offsetAces + 2].add(h2);
g.cardCollections[SolitaireGame.offsetDiscard].add(d2);
g.cardCollections[SolitaireGame.offsetDraw].add(c11);
// Bottom row
g.cardCollections[SolitaireGame.offsetUp + 1].add(c3);
g.cardCollections[SolitaireGame.offsetUp + 2].add(h3);
g.cardCollections[SolitaireGame.offsetUp + 3].add(d1);
g.cardCollections[SolitaireGame.offsetUp + 4].add(s13);
g.cardCollections[SolitaireGame.offsetUp + 5].add(d3);
g.cardCollections[SolitaireGame.offsetUp + 6].add(d12);
g.phase = SolitairePhase.play;
return g;
}
// Cannot move d1 up to empty ACES slot if not in Play phase.
expect(() {
SolitaireGame game = _makeArbitrarySolitaireGame();
game.phase = SolitairePhase.deal;
game.gamelog
.add(new SolitaireCommand.move(d1, SolitaireGame.offsetAces + 1));
}, throwsA(new isInstanceOf<StateError>()));
// Cannot move d1 to nonexistent slot.
expect(() {
SolitaireGame game = _makeArbitrarySolitaireGame();
game.gamelog.add(new SolitaireCommand.move(d1, -1));
}, throwsA(new isInstanceOf<StateError>()));
// Cannot move d1 to ACES slot that is used.
expect(() {
SolitaireGame game = _makeArbitrarySolitaireGame();
game.gamelog
.add(new SolitaireCommand.move(d1, SolitaireGame.offsetAces + 2));
}, throwsA(new isInstanceOf<StateError>()));
// However, we can move d1 to the unused ACES slot in the Play phase.
// see the end.
// We cannot move c11 to d12 because it's in the DRAW pile still.
expect(() {
SolitaireGame game = _makeArbitrarySolitaireGame();
game.gamelog
.add(new SolitaireCommand.move(c11, SolitaireGame.offsetUp + 6));
}, throwsA(new isInstanceOf<StateError>()));
// We cannot move d2 (discard) to the DRAW pile.
expect(() {
SolitaireGame game = _makeArbitrarySolitaireGame();
game.gamelog
.add(new SolitaireCommand.move(d2, SolitaireGame.offsetDraw));
}, throwsA(new isInstanceOf<StateError>()));
// We cannot move d1 (up) to the DISCARD pile either.
expect(() {
SolitaireGame game = _makeArbitrarySolitaireGame();
game.gamelog
.add(new SolitaireCommand.move(d1, SolitaireGame.offsetDiscard));
}, throwsA(new isInstanceOf<StateError>()));
// There are restrictions on what can be played on ACES piles.
// First, suit mismatch: (h3 to spade ACE)
expect(() {
SolitaireGame game = _makeArbitrarySolitaireGame();
game.gamelog
.add(new SolitaireCommand.move(h3, SolitaireGame.offsetAces));
}, throwsA(new isInstanceOf<StateError>()));
// Next, non-ace on empty ACES slot. (c3 to empty ACE)
expect(() {
SolitaireGame game = _makeArbitrarySolitaireGame();
game.gamelog
.add(new SolitaireCommand.move(c3, SolitaireGame.offsetAces + 3));
}, throwsA(new isInstanceOf<StateError>()));
// Below, we'll show that moving to ACES works for various cases.
// There are restrictions on what can be played on UP piles.
// First, color that matches. (h2 to d3)
expect(() {
SolitaireGame game = _makeArbitrarySolitaireGame();
game.gamelog
.add(new SolitaireCommand.move(h2, SolitaireGame.offsetAces + 5));
}, throwsA(new isInstanceOf<StateError>()));
// Next, number that isn't 1 lower. (c3 to h3)
expect(() {
SolitaireGame game = _makeArbitrarySolitaireGame();
game.gamelog
.add(new SolitaireCommand.move(c3, SolitaireGame.offsetAces + 2));
}, throwsA(new isInstanceOf<StateError>()));
// Last, an empty that doesn't receive a king. (c3 to empty UP)
expect(() {
SolitaireGame game = _makeArbitrarySolitaireGame();
game.gamelog
.add(new SolitaireCommand.move(c3, SolitaireGame.offsetAces));
}, throwsA(new isInstanceOf<StateError>()));
// Below, we'll show that moving to UP works for various cases.
// Success cases:
// Going to ACES (empty) with an ace. (d1 to empty)
// Going to ACES with a suit match. (h3 to h2, d2 to d1)
// Going to UP with the color mismatch + 1 number lower. (d12 to s13, s2 to d3, d2 to c3)
// Going to UP (empty) with a king. (s13 to empty)
SolitaireGame game = _makeArbitrarySolitaireGame();
game.gamelog
.add(new SolitaireCommand.move(d1, SolitaireGame.offsetAces + 1));
game.gamelog
.add(new SolitaireCommand.move(h3, SolitaireGame.offsetAces + 2));
game.gamelog
.add(new SolitaireCommand.move(d2, SolitaireGame.offsetAces + 1));
game.gamelog
.add(new SolitaireCommand.move(d12, SolitaireGame.offsetUp + 4));
game.gamelog
.add(new SolitaireCommand.move(s2, SolitaireGame.offsetUp + 5));
game.gamelog
.add(new SolitaireCommand.move(d2, SolitaireGame.offsetUp + 1));
game.gamelog
.add(new SolitaireCommand.move(s13, SolitaireGame.offsetUp + 0));
});
// Consider various situations in which you cannot flip.
test("Playing - cannot flip", () {
// Wrong phase.
expect(() {
SolitaireGame game = new SolitaireGame();
game.gamelog.add(new SolitaireCommand.flip(3));
}, throwsA(new isInstanceOf<StateError>()));
// Bad index (low)
expect(() {
SolitaireGame game = new SolitaireGame();
// Deal first.
game.dealCardsUI();
// Remove some cards...
game.cardCollections[SolitaireGame.offsetUp + 1].clear();
// Try to flip a pile that doesn't exist.
game.gamelog.add(new SolitaireCommand.flip(-1));
}, throwsA(new isInstanceOf<StateError>()));
// Bad index (high)
expect(() {
SolitaireGame game = new SolitaireGame();
// Deal first.
game.dealCardsUI();
// Remove some cards...
game.cardCollections[SolitaireGame.offsetUp + 1].clear();
// Try to flip a pile that doesn't exist.
game.gamelog.add(new SolitaireCommand.flip(7));
}, throwsA(new isInstanceOf<StateError>()));
// No down card to flip.
expect(() {
SolitaireGame game = new SolitaireGame();
// Deal first.
game.dealCardsUI();
// Remove some cards...
game.cardCollections[SolitaireGame.offsetUp + 1].clear();
game.cardCollections[SolitaireGame.offsetDown + 1].clear();
// Try to flip a pile that doesn't exist.
game.gamelog.add(new SolitaireCommand.flip(1));
}, throwsA(new isInstanceOf<StateError>()));
// Up card is in the way.
expect(() {
SolitaireGame game = new SolitaireGame();
// Deal first.
game.dealCardsUI();
// Do not clear out the area for pile 1.
game.gamelog.add(new SolitaireCommand.flip(1));
}, throwsA(new isInstanceOf<StateError>()));
// This scenario should work though.
SolitaireGame game = new SolitaireGame();
// Deal. Clear away pile 1. Flip pile 1.
game.dealCardsUI();
game.cardCollections[SolitaireGame.offsetUp + 1].clear();
game.gamelog.add(new SolitaireCommand.flip(1));
});
// Consider various situations in which you cannot draw.
test("Playing - cannot draw", () {
// Wrong phase.
expect(() {
SolitaireGame game = new SolitaireGame();
game.gamelog.add(new SolitaireCommand.draw());
}, throwsA(new isInstanceOf<StateError>()));
// No draw cards remain.
expect(() {
SolitaireGame game = new SolitaireGame();
game.dealCardsUI();
// Remove all draw cards.
game.cardCollections[SolitaireGame.offsetDraw].clear();
game.gamelog.add(new SolitaireCommand.draw());
}, throwsA(new isInstanceOf<StateError>()));
// But it should be fine to draw just after dealing.
SolitaireGame game = new SolitaireGame();
// Deal first.
game.dealCardsUI();
game.gamelog.add(new SolitaireCommand.draw());
});
});
}