Bridge Training

Hands

Hand shapes, validation, queries, transforms, distribution, honour counts, PBN hand strings and HCP analysis.

Two ways in:

AccessWhat 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

TypeShapeUsed by
HandsDataRecord<Player, Record<Suit, string[]>> — one rank per array entry; HandsData<Card> holds full cardsEverything on this page
HandsRecord<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).

FunctionSignatureDescription
handsToHandsData(hands: Hands) => HandsDataSplit rank strings into arrays.
handsDataToHands(hands: HandsData) => HandsJoin arrays back into rank strings.
hands.ts
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 Hands

Engine module

hands.ts
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
MemberSignature (engine)Description
configCoreConfigThe mode's rules (card count, valid ranks, denominations).
areComplete(hands) => booleanEvery seat holds exactly nbCardsPerHand.
isHandComplete(hand: HandData) => booleanOne seat holds exactly nbCardsPerHand.
hasTooManyCards(hands) => booleanAny seat above nbCardsPerHand.
completeHands(hands, seed?: number) => HandsDataDeal the missing cards of the mode's deck to short hands, at random.
distributeCards(hands, seed?: number) => HandsDataMove excess cards from over-full seats to short ones.
redistributeHands(seed?: number) => HandsDataFresh random deal of the mode's deck.
filterHandsByConfig(hands) => HandsDataDrop ranks outside validRanks.
fillHands(hands, seed?: number) => HandsDataNormalise any input to a legal distribution (see Filling a partial deal).

seed defaults to Date.now(); the same seed yields the same deal.

Validation

FunctionSignatureDescription
areEmpty(hands: HandsData) => booleanAll four hands empty.

Completeness depends on the mode's card count: use engine.hands.areComplete and engine.hands.isHandComplete.

hands.ts
const classic = createBridgeEngine(createBridgeContext({ teachingMode: "auction" }));
classic.hands.isHandComplete(hands.N); // => true
classic.hands.areComplete(hands); // => true
areEmpty(clearHands()); // => true

Queries

FunctionSignatureDescription
findDuplicatedCards(hands: HandsData) => Set<string>Card keys ("SA") appearing in more than one hand.
handsAreDifferent(a: HandsData, b: HandsData) => booleanOrder-sensitive comparison per suit.
hands.ts
findDuplicatedCards(hands); // => Set {} (empty when the deal is consistent)
handsAreDifferent(hands, deepCopyHands(hands)); // => false

Transforms

All transforms return new objects; inputs are never mutated.

FunctionSignatureDescription
deepCopyHands(hands: HandsData) => HandsDataFresh arrays for every suit.
clearHands() => HandsDataFour empty hands.
removeCardFromHand(card: CardInput, player: Player, hands: HandsData) => HandsDataDeep copy without the card; stored ranks are matched regardless of case.
rotateHandsClockwise(hands: HandsData) => HandsDataN→E, E→S, S→W, W→N.
hands.ts
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 hand

Filling 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.

hands.ts
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 random

Analysis

analyzeHands(hands) returns a DealAnalysis: high-card points and suit lengths per seat.

Prop

Type

hands.ts
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.

ExportSignatureDescription
HCP_VALUESReadonly<Record<string, number>>A: 4, K: 3, Q: 2, J: 1.
THT_VALUESReadonly<Record<string, number>>Top honour tricks: A: 1, K: 1, Q: 0.5.
computeHandHCP(hand: HandData) => numberHigh-card points of one hand.
HONOR_COMPUTERSRecord<HonorMetric, (hand: HandData) => number>hcp is computeHandHCP, tht counts top honour tricks (THT_VALUES); index it by metric instead of branching.
computeCardCounts(hands: HandsData) => CardCountsFor each player, { points, suitCount }.
CardCountstypeReturn shape of computeCardCounts. Marked deprecated in favour of DealAnalysis from @workspace/bridge/types, which has the same shape.
hands.ts
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.

FunctionSignatureDescription
handToPBN(hand: HandData) => stringOne 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.

hands.ts
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"