Card
Card and rank parsing, normalisation, rank sorting and suit display order.
Import from @workspace/bridge/card. There is no engine.card module; the engine exposes card-related configuration elsewhere:
| Engine member | What it tells you |
|---|---|
engine.hands.config.validRanks | The ranks in the mode's deck (CoreConfig.validRanks). |
engine.hands.config.nbCardsPerHand | Cards per seat. |
engine.ctx.cardSet | Which card-face set the mode renders with. |
engine.ctx.validDenominations | Denominations that can be bid or used as trump. |
engine.notation.* | Locale-aware rank parsing and formatting (see Notation). |
const engine = createBridgeEngine(createBridgeContext({ teachingMode: "petit-bridge" }));
engine.hands.config.validRanks; // the ranks of the mode's deck (10 per suit for petit-bridge)
engine.ctx.cardSet; // which card-face set the mode renders with
engine.ctx.validDenominations; // denominations that can be bid / used as trumpTypes
Suit is "S" | "H" | "D" | "C". Rank is "A" | "K" | "Q" | "J" | "T" | "9" … "2"; ten is always "T" internally. Card is the template `${Suit}${Rank}` ("SA", "H7", "DT"): the canonical form, stored in Deal, PBN, LIN and the database, and the one every function returns.
Two more types describe what the public API accepts:
| Type | Definition | Description |
|---|---|---|
CardParts | Readonly<{ suit: Suit; rank: Rank }> | A card as its two fields. |
CardInput | Card | CardParts | Either form. Functions that take a card accept it and normalise through toCard, so they throw on a value that denotes no card; internal loops keep working on Card strings. |
CardInput is a TypeScript convenience: the Zod schemas and Deal keep the string form, so the trust boundary does not move. Both types are imported from @workspace/bridge/card.
Type guards
| Function | Signature | Description |
|---|---|---|
isRank | (value: string) => value is Rank | Internal notation only: "10" is not a rank. |
isCard | (value: unknown) => value is Card | Suit letter followed by a rank. |
isRank("T"); // => true
isRank("10"); // => false (internal notation is "T")
isCard("SA"); // => true
isCard("XY"); // => falseParsing and construction
| Function | Signature | Description |
|---|---|---|
parseSuit | (value: string) => Suit | null | Exact letter. |
parseRank | (value: string) => Rank | null | Single canonical character; "10" is not accepted. |
parseCard | (value: string) => Card | null | The entry for untrusted strings: uppercases, collapses "10" to "T", then validates. null when invalid. |
toCard | (input: CardInput) => Card | Canonical string from either form, for values the caller vouches for. A canonical string is returned as is; any other string is uppercased, its "10" collapsed, then validated. Throws InvalidNotationError (INVALID_CARD) when the string or the parts denote no card. |
suitOf | (card: Card) => Suit | Reader; use it instead of slicing the string. |
rankOf | (card: Card) => Rank | Reader. |
Two contracts: parse* takes untrusted input and returns null when it is invalid, while to* takes a value the caller vouches for and throws InvalidNotationError (see Errors) when it is not.
parseSuit("D"); // => "D"
parseRank("1"); // => null
parseCard("H7"); // => "H7" (typed Card)
parseCard("h10"); // => "HT" (casing and the PBN ten are normalised first)
parseCard("H1"); // => null
toCard({ suit: "S", rank: "A" }); // => "SA" (canonical string from parts)
toCard("sa" as Card); // => "SA" (casing normalised for values cast from storage)
toCard("H1" as Card); // throws InvalidNotationError (INVALID_CARD): use parseCard for untrusted input
suitOf("H7"); // => "H"
rankOf("H7"); // => "7"Rank normalisation
PBN and display use "10"; the package uses "T".
| Function | Signature | Description |
|---|---|---|
normalizeRankString | (ranks: string) => string | Replace every "10" in a joined rank string. |
parseRankString | (raw: string) => Rank[] | Uppercase, collapse "10", drop anything that is not a rank. |
normalizeRankString("AKQ1093"); // => "AKQT93"
parseRankString("a k 10 x 7"); // => ["A", "K", "T", "7"] (invalid chars dropped)Sorting
| Function | Signature | Description |
|---|---|---|
DEFAULT_RANK_ORDER | readonly Rank[] | A, K, Q, J, T, 9 … 2. |
sortRanksDefault | (ranks: readonly Rank[]) => Rank[] | Default order, highest first; unknown ranks go last. |
sortRankString | (ranks: string) => string | Sort a joined rank string (assumes "T" notation). |
sortCardsByRank | <T extends string>(cards: readonly T[], order?: readonly Rank[]) => T[] | Sort cards ("SA") or bare ranks ("A") by their last character, highest first; unknown ranks go last. |
sortRanksDefault(["3", "A", "K"]); // => ["A", "K", "3"]
sortRankString("Q3KA"); // => "AKQ3"
sortCardsByRank(["S3", "SA", "SK"]); // => ["SA", "SK", "S3"]Suit display order
How a fanned hand reads: trump first, then the other suits arranged to alternate colours, with the longest remaining suit at the far end. priority picks the criterion: "length" (default) or "alternation", which maximises black/red flips over the suits actually held and can align the hero's fan on the dummy's.
| Function | Signature | Description |
|---|---|---|
suitDisplayOrder | (trump: Suit | null, distribution?: SuitDistribution, priority?: SuitOrderPriority, leadSuit?: Suit | null, referenceOrder?: readonly Suit[]) => readonly Suit[] | Without a distribution: canonical S, H, C, D shifted so trump leads. |
suitLengths | (hand: Readonly<Record<Suit, string>>) => SuitDistribution | Suit lengths of a rank-string hand ("10" counted once). |
Prop
Type
const distribution = suitLengths({ S: "AKQ2", H: "KQ3", D: "A54", C: "K32" }); // => { S: 4, H: 3, D: 3, C: 3 }
suitDisplayOrder(null); // => ["S", "H", "C", "D"] (canonical, no trump, no distribution)
suitDisplayOrder("H", distribution); // trump first, then arranged for colour alternation
suitDisplayOrder(null, distribution, "alternation", "D"); // NT: the lead suit opens the fan