Play
Trick winner, follow-suit rules, legal cards, trick building from a flat play, and engine-side replay validation.
Two ways in:
| Access | What you get |
|---|---|
import { … } from "@workspace/bridge/play" | Pure trick helpers: winner, follow-suit, legal cards, trick building. |
engine.play.* | The same helpers plus validateTricks, a mode-aware replay validator, and a few hand helpers (deepCopyHands, removeCardFromHand). |
engine.play is configured with the mode's validRanks and validDenominations. That configuration only matters for validateTricks: a played rank outside the mode's deck is reported, and in modes whose denominations have no playable suit (NT-only) the trump argument is neutralised. buildTricks, buildTricksFromDeal and getLegalCards are only in @workspace/bridge/play.
Play shape
A Play is the stored form: { first: Player; moves: [{ card, comment? }] } in chronological order, first being the opening leader. Trick T+1 starts with the winner of trick T, so seats are derived while walking, never stored. buildTricks turns that flat list into Trick[], the in-memory view.
Prop
Type
Trick winner
| Function | Signature | Description |
|---|---|---|
determineTrickWinner | (entries: PlayEntry[], trump: Suit | null, leadSuit: Suit | null) => PlayEntry | null | Highest trump wins; otherwise highest card of the lead suit. null for an empty trick. |
const trick = new TrickBuilder().fromCompact("N / SA SK H2 S3").build();
determineTrickWinner(trick.cards, null, "S"); // => { player: "N", card: "SA" }
determineTrickWinner(trick.cards, "H", "S"); // => { player: "S", card: "H2" } (trump beats the lead suit)
determineTrickWinner([], null, null); // => nullFollow-suit rules
| Function | Signature | Description |
|---|---|---|
validateCardFollowsRules | (card: CardInput, player: Player, leadSuit: Suit | null, hands: HandsData) => boolean | Legal if leading, if following suit, or if void in the lead suit. |
getLegalCards | (hand: HandData, leadSuit: Suit | null) => Card[] | Set form of the rule above. A single result means the play is forced. |
validateCardFollowsRules("H2", "S", "D", hands); // => false (South still holds diamonds)
validateCardFollowsRules("DT", "S", "D", hands); // => true
validateCardFollowsRules({ suit: "D", rank: "T" }, "S", "D", hands); // => true (CardParts form)
validateCardFollowsRules("H2", "S", null, hands); // => true (leading: anything goes)
getLegalCards(hands.W, "D"); // => ["D7", "D6", "D3", "D2"]
getLegalCards(hands.W, null).length; // => 13 (leading)Building and reading tricks
| Function | Signature | Description |
|---|---|---|
buildTricks | (play: { first: Player; moves: { card: string }[] }, trump: Suit | null) => Trick[] | Chunks by four, assigns seats from the leader, computes each winner. A partial last trick has winner: null. |
buildTricksFromDeal | (deal: Deal) => Trick[] | buildTricks with deal.play and deal.contract?.trump. |
extractLeadFromTricks | (tricks: Trick[]) => Card | null | The opening lead. |
hasPlayedSuit | (tricks: Trick[], player: Player, suit: Suit) => boolean | Whether a seat has played a card of that suit. |
PlayMoveLike ({ card: string }) is the minimal move shape buildTricks accepts. A Trick is built by buildTricks; form data reaches the same shape through trickSchema (Schemas).
const play = new PlayBuilder().fromCompact("W / D7 DA DK DT SA SJ S7 S3").build();
const tricks = buildTricks(play, null);
tricks[0]?.winner; // => "N" (DA wins the first trick)
tricks[1]?.lead; // => "N" (winner leads the next trick)
tricks.length; // => 2
extractLeadFromTricks(tricks); // => "D7"
hasPlayedSuit(tricks, "E", "S"); // => true
buildTricksFromDeal(makeDeal({ cards, play })); // trump taken from deal.contract (null here)Replay validation (engine only)
engine.play.validateTricks(moves, first, cards, trump) replays a flat play against the original Hands and returns a CorePlayValidationResult. Every occurrence of a failure is collected:
Error type | Meaning |
|---|---|
CARD_NOT_OWNED | The seat does not hold the card at that point of the play. |
REVOKE | A follower discarded while still holding the lead suit. |
INVALID_CARD_RANK | The rank is outside the mode's validRanks. |
A partial last trick is accepted as long as everything so far is consistent. Non-chronological or otherwise illegal stored play is rejected at the write gate; there is no auto-repair.
Prop
Type
const engine = createBridgeEngine(createBridgeContext({ teachingMode: "auction" }));
const ok = engine.play.validateTricks(play.moves, play.first, cards, null);
ok.isValid; // => true
ok.parsedTricks.length; // => 2
const revoke = new PlayBuilder().fromCompact("W / D7 DA DK S7").build(); // South discards a spade while holding diamonds
const bad = engine.play.validateTricks(revoke.moves, revoke.first, cards, null);
bad.isValid; // => false
bad.errors[0]?.type; // => "REVOKE"