Hands
Hand shapes, validation, queries, transforms, distribution, honour counts, PBN hand strings and HCP analysis.
Two ways in:
| Access | What you get |
|---|---|
import { … } from "@workspace/bridge/hands" | Context-free helpers: shape conversion, areEmpty, queries, transforms, analyzeHands, honour counts, PBN hand strings. |
engine.hands.* | findDuplicatedCards, handsAreDifferent, areEmpty and the transforms (except removeCardFromHand, which sits on engine.play), plus every function that depends on the mode's CoreConfig, pre-bound to it. |
engine.hands knows the mode's CoreConfig (nbCardsPerHand, validRanks, …) and exposes it as engine.hands.config. Anything that reads the card count or the deck depends on the mode, so it is only available on the engine: areComplete, isHandComplete, hasTooManyCards, completeHands, distributeCards, redistributeHands, filterHandsByConfig and fillHands. analyzeHands, the shape converters and the PBN hand strings are only in @workspace/bridge/hands; the honour counts are also on engine.stats (see Utils).
Two hand shapes
| Type | Shape | Used by |
|---|---|---|
HandsData | Record<Player, Record<Suit, string[]>> — one rank per array entry; HandsData<Card> holds full cards | Everything on this page |
Hands | Record<Player, Hand>, Hand being Record<Suit, string> — ranks joined ("AKQ2") | Deal.cards, PBN |
HandsData is the canonical in-memory form and Hands the persisted one; the two functions below are the only crossing points (see Types for the inventory of aggregate shapes).
| Function | Signature | Description |
|---|---|---|
handsToHandsData | (hands: Hands) => HandsData | Split rank strings into arrays. |
handsDataToHands | (hands: HandsData) => Hands | Join arrays back into rank strings. |
const cards = new HandsBuilder().fromCompact(COMPACT).build(); // Hands: { N: { S: "AKQ2", H: "KQ3", … }, … }
const hands = handsToHandsData(cards); // HandsData: { N: { S: ["A","K","Q","2"], … }, … }
handsDataToHands(hands); // back to HandsEngine module
const engine = createBridgeEngine(
createBridgeContext({ teachingMode: "petit-bridge" }),
);
engine.hands.config.nbCardsPerHand; // => 10
engine.hands.areComplete(hands); // => false (13-card hands against a 10-card config)
engine.hands.filterHandsByConfig(hands); // drops ranks outside the petit-bridge deck
engine.hands.redistributeHands(42); // fresh seeded 4 × 10-card deal| Member | Signature (engine) | Description |
|---|---|---|
config | CoreConfig | The mode's rules (card count, valid ranks, denominations). |
areComplete | (hands) => boolean | Every seat holds exactly nbCardsPerHand. |
isHandComplete | (hand: HandData) => boolean | One seat holds exactly nbCardsPerHand. |
hasTooManyCards | (hands) => boolean | Any seat above nbCardsPerHand. |
completeHands | (hands, seed?: number) => HandsData | Deal the missing cards of the mode's deck to short hands, at random. |
distributeCards | (hands, seed?: number) => HandsData | Move excess cards from over-full seats to short ones. |
redistributeHands | (seed?: number) => HandsData | Fresh random deal of the mode's deck. |
filterHandsByConfig | (hands) => HandsData | Drop ranks outside validRanks. |
fillHands | (hands, seed?: number) => HandsData | Normalise any input to a legal distribution (see Filling a partial deal). |
seed defaults to Date.now(); the same seed yields the same deal.
Validation
| Function | Signature | Description |
|---|---|---|
areEmpty | (hands: HandsData) => boolean | All four hands empty. |
Completeness depends on the mode's card count: use engine.hands.areComplete and engine.hands.isHandComplete.
const classic = createBridgeEngine(createBridgeContext({ teachingMode: "auction" }));
classic.hands.isHandComplete(hands.N); // => true
classic.hands.areComplete(hands); // => true
areEmpty(clearHands()); // => trueQueries
| Function | Signature | Description |
|---|---|---|
findDuplicatedCards | (hands: HandsData) => Set<string> | Card keys ("SA") appearing in more than one hand. |
handsAreDifferent | (a: HandsData, b: HandsData) => boolean | Order-sensitive comparison per suit. |
findDuplicatedCards(hands); // => Set {} (empty when the deal is consistent)
handsAreDifferent(hands, deepCopyHands(hands)); // => falseTransforms
All transforms return new objects; inputs are never mutated.
| Function | Signature | Description |
|---|---|---|
deepCopyHands | (hands: HandsData) => HandsData | Fresh arrays for every suit. |
clearHands | () => HandsData | Four empty hands. |
removeCardFromHand | (card: CardInput, player: Player, hands: HandsData) => HandsData | Deep copy without the card; stored ranks are matched regardless of case. |
rotateHandsClockwise | (hands: HandsData) => HandsData | N→E, E→S, S→W, W→N. |
const afterLead = removeCardFromHand("SA", "N", hands); // fresh copy, North's SA gone
removeCardFromHand({ suit: "S", rank: "A" }, "N", hands); // same, from CardParts
rotateHandsClockwise(hands).E; // => North's original handFilling a partial deal
engine.hands.fillHands(hands, seed?) normalises any input to a legal distribution for the engine's mode: it dedupes (first occurrence wins, scanning N→E→S→W then S→H→D→C), moves excess cards to short hands, then deals the cards still missing from the deck. Every seat ends at engine.hands.config.nbCardsPerHand with no duplicates.
const partial = clearHands();
partial.N.S.push("A", "K", "Q");
partial.S.H.push("A", "A"); // duplicate: fillHands keeps the first occurrence
const complete = engine.hands.fillHands(partial, 42); // deterministic for a given seed
engine.hands.areComplete(complete); // => true
findDuplicatedCards(complete).size; // => 0
new HandsBuilder().withHand("N", "AKQ2.KQ3.A54.K32").toRankRecord(); // HandsData<Rank>, other seats dealt at randomAnalysis
analyzeHands(hands) returns a DealAnalysis: high-card points and suit lengths per seat.
Prop
Type
analyzeHands(hands).N; // => { points: 21, suitCount: { S: 4, H: 3, D: 3, C: 3 } }Honour counts
Works on HandData (Record<Suit, string[]>); each entry may be a bare rank ("A") or a card ("SA"), whose rank is read with rankOf.
| Export | Signature | Description |
|---|---|---|
HCP_VALUES | Readonly<Record<string, number>> | A: 4, K: 3, Q: 2, J: 1. |
THT_VALUES | Readonly<Record<string, number>> | Top honour tricks: A: 1, K: 1, Q: 0.5. |
computeHandHCP | (hand: HandData) => number | High-card points of one hand. |
HONOR_COMPUTERS | Record<HonorMetric, (hand: HandData) => number> | hcp is computeHandHCP, tht counts top honour tricks (THT_VALUES); index it by metric instead of branching. |
computeCardCounts | (hands: HandsData) => CardCounts | For each player, { points, suitCount }. |
CardCounts | type | Return shape of computeCardCounts. Marked deprecated in favour of DealAnalysis from @workspace/bridge/types, which has the same shape. |
const north: HandData = { S: ["A", "K"], H: ["Q"], D: ["T", "9"], C: ["J"] };
computeHandHCP(north); // => 10 (4 + 3 + 2 + 1)
HONOR_COMPUTERS.tht(north); // => 2.5 top honour tricks (A = 1, K = 1, Q = 0.5)
HCP_VALUES.A; // => 4
const dealt: HandsData = {
N: north,
E: { S: ["J"], H: ["A", "K"], D: ["8"], C: ["A", "K"] },
S: { S: ["Q", "T"], H: ["J"], D: ["A"], C: ["Q"] },
W: { S: ["9"], H: ["T", "9"], D: ["K", "Q"], C: ["T"] },
};
computeCardCounts(dealt).N; // => { points: 10, suitCount: { S: 2, H: 1, D: 2, C: 1 } }PBN hand strings
Builds PBN Deal tag values starting from any player; pbn.serializeHand is the North form.
| Function | Signature | Description |
|---|---|---|
handToPBN | (hand: HandData) => string | One hand as S.H.D.C: ranks sorted highest first, a void suit left empty ("AKQ2..A54.K32"). Each entry contributes its last character, so suit-prefixed cards ("SA") read as their rank. |
handsToPBN | <TPlayer extends Player>(hands: HandsData, startPlayer: TPlayer) => PBN<TPlayer> | handToPBN for the four hands, clockwise from startPlayer. From North it equals pbn.serializeHand, and it is the form the double-dummy solver reads. |
Both take the canonical HandsData shape. Rank-string hands (Hands, as stored in Deal.cards) go through handsToHandsData first.
handToPBN({ S: ["2", "Q", "A", "K"], H: [], D: ["A", "5", "4"], C: ["K", "3", "2"] }); // => "AKQ2..A54.K32"
handsToPBN(dealt, "N"); // => "N:AK.Q.T9.J J.AK.8.AK QT.J.A.Q 9.T9.KQ.T"
handsToPBN(
handsToHandsData({
N: { S: "AK", H: "Q", D: "T9", C: "J" },
E: { S: "J", H: "AK", D: "8", C: "AK" },
S: { S: "QT", H: "J", D: "A", C: "Q" },
W: { S: "9", H: "T9", D: "KQ", C: "T" },
}),
"E",
); // => "E:J.AK.8.AK QT.J.A.Q 9.T9.KQ.T AK.Q.T9.J"