v0.4.4 — New Game dialog for the opening hand and the three revenue rates, Yard renamed Interchange, and two movement bugs behind a mirrored consist
This commit is contained in:
+48
-8
@@ -28,6 +28,7 @@ import {
|
||||
} from '../src/engine/content.ts';
|
||||
import { createRng } from '../src/engine/rng.ts';
|
||||
import { buildDeck, buildRollingStock, createGame } from '../src/engine/setup.ts';
|
||||
import type { StartingHand } from '../src/engine/content.ts';
|
||||
import type { GameConfig } from '../src/engine/state.ts';
|
||||
import { subdivisions } from '../src/engine/state.ts';
|
||||
|
||||
@@ -46,6 +47,15 @@ const solitaireConfig: GameConfig = {
|
||||
const newSolitaireGame = (seed = 1234) =>
|
||||
createGame({ id: 'g1', seed, config: solitaireConfig, playerNames: ['Jesse'] });
|
||||
|
||||
/** A game dealt under one named `StartingHand`, for the tests that are about the deal itself. */
|
||||
const gameDealtWith = (startingHand: StartingHand, seed = 1234) =>
|
||||
createGame({
|
||||
id: 'g1',
|
||||
seed,
|
||||
config: { ...solitaireConfig, houseRules: { startingHand } },
|
||||
playerNames: ['Jesse'],
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('card catalogue (component 1)', () => {
|
||||
@@ -380,26 +390,40 @@ describe('game setup (component 2)', () => {
|
||||
assert.equal(area.adOccupancy.length, 0);
|
||||
});
|
||||
|
||||
it('deals three track and three other cards, and starts three Department piles', () => {
|
||||
// The opening deal comes from two separately shuffled piles, so the district you can build is
|
||||
// dealt rather than waited for. Six against a hand limit of three is deliberate: the first turn
|
||||
// is spent choosing which of them to keep.
|
||||
it('deals three random cards by default, and starts three Department piles', () => {
|
||||
// The prototype rule, and the New Game dialog's default: three off one deck, already at the hand
|
||||
// limit, with no guarantee of anything. The other two shapes are below.
|
||||
const g = newSolitaireGame();
|
||||
assert.equal(g.decks.hands.get(0)!.length, 3);
|
||||
|
||||
assert.equal(g.decks.departments.length, 3);
|
||||
assert.ok(g.decks.departments.every((pile) => pile.length === 1), 'each Department starts with one face-up card');
|
||||
});
|
||||
|
||||
it('deals six random cards when that is the rule, over the hand limit on purpose', () => {
|
||||
// Six against a hand limit of three is deliberate: the first turn is spent choosing which of
|
||||
// them to keep. What it does NOT do is guarantee track, which is the difference from the split
|
||||
// deal below.
|
||||
const g = gameDealtWith('sixRandom');
|
||||
assert.equal(g.decks.hands.get(0)!.length, 6);
|
||||
});
|
||||
|
||||
it('deals three track and three other cards when that is the rule', () => {
|
||||
// From two separately shuffled piles, so the district you can build is dealt rather than waited
|
||||
// for — a run-around needs five specific pieces, and drawing for them took eight games.
|
||||
const g = gameDealtWith('threeTrackThreeOther');
|
||||
const hand = g.decks.hands.get(0)!;
|
||||
assert.equal(hand.length, OPENING_TRACK + OPENING_OTHER);
|
||||
|
||||
const track = hand.filter((id) => g.cards.get(id)!.kind.kind === 'track');
|
||||
assert.equal(track.length, OPENING_TRACK, 'the opening hand is not three track cards');
|
||||
assert.equal(hand.length - track.length, OPENING_OTHER, 'the opening hand is not three other cards');
|
||||
|
||||
assert.equal(g.decks.departments.length, 3);
|
||||
assert.ok(g.decks.departments.every((pile) => pile.length === 1), 'each Department starts with one face-up card');
|
||||
});
|
||||
|
||||
it('shuffles the leftover track back into one deck for the rest of the game', () => {
|
||||
// The split is an opening-deal device only. If the leftover track stayed out, every draw after
|
||||
// the first turn would be drawn from a deck with no track in it at all.
|
||||
const g = newSolitaireGame();
|
||||
const g = gameDealtWith('threeTrackThreeOther');
|
||||
const rest = [...g.decks.homeOffice, ...g.decks.departments.flat()];
|
||||
const track = rest.filter((id) => g.cards.get(id)!.kind.kind === 'track');
|
||||
assert.equal(
|
||||
@@ -408,6 +432,22 @@ describe('game setup (component 2)', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('leaves every card accounted for whichever shape it was dealt in', () => {
|
||||
// The single-deck path and the two-pile path deal from different piles into the same game; a
|
||||
// card lost or duplicated by either would be a deck that quietly runs short mid-game.
|
||||
for (const shape of ['threeRandom', 'sixRandom', 'threeTrackThreeOther'] as const) {
|
||||
const g = gameDealtWith(shape);
|
||||
const all = [
|
||||
...g.decks.homeOffice,
|
||||
...g.decks.departments.flat(),
|
||||
...g.decks.salvageYard,
|
||||
...[...g.decks.hands.values()].flat(),
|
||||
];
|
||||
assert.equal(all.length, SOLITAIRE_DECK_SIZE, `cards lost or duplicated dealing ${shape}`);
|
||||
assert.equal(new Set(all).size, all.length, `duplicate card dealing ${shape}`);
|
||||
}
|
||||
});
|
||||
|
||||
it('accounts for every card exactly once', () => {
|
||||
const g = newSolitaireGame();
|
||||
const all = [
|
||||
|
||||
Reference in New Issue
Block a user