Bridge Training

Auction

Bid parsing, auction validation and state, contract derivation, and expected-result resolution.

Naming Convention

Auction = the complete sequence of bids for a deal (domain entity). Bid = an individual call. Bidding = UI/process (lives in ui-bridge, not here).

Two ways in:

AccessWhat you get
import { … } from "@workspace/bridge/auction"Every function below as a plain, context-free export, except isAuctionPassedOut (engine only).
engine.auction.*The view, state, validation, contract, opener and counter-status helpers bound to an engine, plus isEnabled.

engine.auction adds one piece of configuration: isEnabled is false for teaching modes without an auction phase (no-auction, no-dummy, petit-bridge). The functions themselves are the same pure helpers. Bid parsing, contract level, getBidPadFlags, the derive*FromAuction helpers and the expected-result helpers are only reachable through @workspace/bridge/auction.

auction.ts
const engine = createBridgeEngine(
  createBridgeContext({ teachingMode: "auction", locale: "fr" }),
);

engine.auction.isEnabled; // true — false for "no-auction", "no-dummy", "petit-bridge"
engine.auction.isAuctionComplete(new AuctionBuilder().fromCompact("N / 1H pass 2H AP").build()); // => true

Bid parsing

A Bid is either a RegularBid ("1C" … "7NT") or a SpecialBid ("Pass", "X", "XX", "Alert"). Denomination is Suit | "NT". RegularBidParts is { level: BidLevel; suit: Denomination } and BidInput is Bid | RegularBidParts: the functions that take a bid accept either form and store the string. CallInput is BidInput without "Alert": the calls that take a seat in the sequence. Both are imported from @workspace/bridge/auction.

FunctionSignatureDescription
isBid(value: unknown) => value is BidAny valid bid string, regular or special.
parseBid(value: string) => Bid | nullThe entry for untrusted strings: typed bid, or null when invalid. No casing is applied ("Pass" is mixed case).
toBid(input: BidInput) => BidCanonical string from either form, for values the caller vouches for. A valid string is returned as is (bids have no alternate spelling to normalise). Throws InvalidNotationError (INVALID_BID) when the string or the parts (level 1-7, denomination) denote no bid.
splitBid(bid: Bid) => RegularBidParts | nullReader: { level, suit } for a regular bid, null for special bids.
toTrumpSuit(denomination: Denomination) => Suit | nullnull for NT.
auction.ts
isBid("1H"); // => true

parseBid("3NT"); // => "3NT" (typed Bid)
parseBid("1Z"); // => null

toBid({ level: 3, suit: "NT" }); // => "3NT"
toBid("Pass"); // => "Pass"
toBid("8H" as Bid); // throws InvalidNotationError (INVALID_BID): use parseBid for untrusted input

splitBid("3NT"); // => { level: 3, suit: "NT" }
splitBid("Pass"); // => null

toTrumpSuit("NT"); // => null
toTrumpSuit("H"); // => "H"

The auction and its view

Every rule takes and returns an Auction: { first, moves }, the stored shape of Deal.auction, where first is the dealer and moves are the calls in order ({ bid, isAlerted?, comment? }). A call's seat is never stored: the call at index i belongs to the seat i steps clockwise from first, and the seat on turn is the one after the last call.

auctionEntries produces the per-call view for displays and lists, the way buildTricks produces tricks from a Play. It is computed, never stored. Both functions below are imported from @workspace/bridge/auction and bound on engine.auction.

FunctionSignatureDescription
auctionEntries<TComment>(auction: Auction<TComment>) => BidInfo<TComment>[]Each call with its seat and 0-based position; alert and comment are kept.
auctionTurn(auction: Auction) => PlayerThe seat whose turn it is to call.

Prop

Type

auction.ts
const played = new AuctionBuilder().fromCompact("E / pass 1S X 2S AP").build();

played; // => { first: "E", moves: [{ bid: "Pass" }, { bid: "1S" }, { bid: "X" }, …] }
auctionEntries(played);
// => [{ bid: "Pass", player: "E", position: 0 }, { bid: "1S", player: "S", position: 1 }, …]
auctionTurn(new AuctionBuilder().fromCompact("E / pass 1S").build()); // => "W"

Validation

The predicates judge the call of the seat on turn. An auction is complete after three consecutive passes following a contract bid, or when all four players pass.

FunctionSignatureDescription
isValidBid(bid: BidInput, auction: Auction) => booleanLegal now? Higher level, or same level and higher denomination; X/XX delegate to the predicates below; Alert needs a previous bid. A string that is no bid is false; parts that denote no bid throw InvalidNotationError, as in toBid.
isAuctionComplete(auction) => booleanThree passes after a bid, or passed out.
isAuctionPassedOut(auction) => booleanFour passes and nothing else. On engine.auction only.
isAuctionCloseable(auction) => { canClose: false } | { canClose: true; passesNeeded: number }How many passes would end the auction. canClose is false on an empty or already complete auction.
isDoubleable(auction) => booleanThe opponents of the seat on turn hold the last contract bid and nobody has doubled it yet.
isRedoubleable(auction) => booleanThe side on turn holds the last contract bid, doubled by the opponents and not yet redoubled.
isBidAPass(bid: BidInput | BidComposite) => booleanAccepts the string, the parts or the composite form.
auction.ts
const open = new AuctionBuilder().fromCompact("N / 1H pass 2H").build();

auctionTurn(open); // => "W"
isValidBid("2S", open); // => true (higher than 2H, for West on turn)
isValidBid("1S", open); // => false
isDoubleable(open); // => true (West's opponents hold the last bid)
isAuctionComplete(open); // => false
isAuctionCloseable(open); // => { canClose: true, passesNeeded: 3 }

Building an auction

Every function returns a new Auction and keeps the comment type.

FunctionSignatureDescription
createEmptyAuction(dealer?: Player) => AuctionNo call yet; the dealer defaults to South.
addBids(auction, ...bids: CallInput[]) => AuctionAppends the canonical bid strings in order. Returns the same auction when no bid is given.
alertLastBid(auction) => AuctionFlags the last call as alerted. Returns the same auction when it is empty.
closeAuction(auction) => AuctionAppends the passes reported by isAuctionCloseable.
setDealerPlayer(auction, dealer: Player) => AuctionSets the dealer; every call moves with the seats.
rotateToDeclarer(auction, target: Player) => AuctionChanges the dealer so the declarer of the confirmed contract sits at target. No-op while the auction is open.
auction.ts
let built = addBids(createEmptyAuction("N"), "1H", "Pass", { level: 2, suit: "H" }); // the raise is stored as "2H"
built = alertLastBid(built); // flags the 2H bid
built = closeAuction(built); // appends the 3 closing passes

built.moves.length; // => 6

Bid pad flags

getBidPadFlags(auction) composes the predicates above into the facts a bid input needs for the seat on turn. The result maps 1:1 to the optional props of ui-bridge's BidPad.Root.

Prop

Type

auction.ts
const flags = getBidPadFlags(built);
flags.isComplete; // => true
flags.lastContractBid; // => "2H"

Contract

calculateContract returns a ContractResult: type is "confirmed" once the auction is complete, "candidate" while it is still open. The declarer is the first player of the winning partnership to have named the final denomination.

Prop

Type

FunctionSignatureDescription
calculateContract(auction: Auction) => ContractResult | nullnull when no contract bid has been made.
getDeclarer(auction: Auction) => PlayerFirst player of the last bidder's partnership to have named the final denomination; South when there is no contract bid.
auction.ts
const result = calculateContract(new AuctionBuilder().fromCompact("N / 1H pass 2H AP").build());

if (result?.type === "confirmed") {
  result.value.declarer; // => "N" (first to bid hearts in the NS partnership)
  result.value.doubled; // => "none"
}

From a stored deal

deriveContractResultFromAuction and deriveFinalDeclarerFromAuction also accept unbranded bid strings, so persisted rows can be checked without parsing them first.

FunctionSignatureDescription
deriveContractResultFromAuction(auction: { first: Player; moves: readonly { bid: string }[] }) => ContractResult | nullTypes the calls as bids and delegates to calculateContract.
deriveFinalDeclarerFromAuction(auction, storedContract: string | null | undefined) => Player | nullA complete auction is authoritative. A truncated one (closing passes omitted) only counts when the stored contract's denomination matches the candidate's.
auction.ts
const auction = new AuctionBuilder().fromCompact("N / 1H pass 2H").build(); // truncated: closing passes omitted

deriveFinalDeclarerFromAuction(auction, "4H"); // => "N" (stored strain confirms the candidate)
deriveFinalDeclarerFromAuction(auction, "3NT"); // => null (strain mismatch)

Contract level

FunctionSignatureDescription
determineContractLevel(contract: string | null) => ContractLevel"partial", "game", "small_slam" or "grand_slam"; unparsable input is "partial".
auction.ts
determineContractLevel("3NT"); // => "game"
determineContractLevel("6S"); // => "small_slam"

Opener and counter status

FunctionSignatureDescription
determineOpener(auction: Auction, playerCount?: 2 | 4) => Player | nullFirst seat whose call is neither a pass nor the legacy "SKIP" placeholder; with 2, only N and S count.
getCounterStatus(auction: Auction) => CounterStatus"none", "X" or "XX": the strongest counter after the last contract bid.
auctionCalls(auction: Auction) => Bid[]The calls in order, as stored in the search index.
isCounterStatus(value: unknown) => value is CounterStatusExact membership in "none" | "X" | "XX".

The empty call some legacy rows hold is ignored by the three auction readers.

auction.ts
determineOpener(played); // => "S"
getCounterStatus(played); // => "none" (the double was overcalled)
isCounterStatus("XX"); // => true

Expected result

A deal's metadata.result is stored verbatim: a trick count ("10") or one of the RESULT_EXPECTED sentinels ("MAKE_CONTRACT", "DEFEAT_CONTRACT", "PAR", or "" for unset). These helpers resolve it at consumer boundaries. Trick counts are in the N/S frame.

FunctionSignatureDescription
getRequiredTricks(contract: { level: number }) => numberlevel + 6.
resolveExpectedResult(rawResult: string | number | null | undefined, contract: { level: number } | null | undefined) => number | nullMAKE_CONTRACT → level + 6, DEFEAT_CONTRACT → level + 5, PAR and unset → null.
isResultReachable(tricks: number, hint: NSParHint) => booleanWith robots playing the other three seats optimally, NS is capped at par when the human is NS and floored at par when the human is EW.
formatResultExpectedLabel(rawResult: string | number, labels: ResultExpectedLabels) => stringLocalised label for a sentinel, or labels.word(count) for a number.

Prop

Type

Prop

Type

auction.ts
getRequiredTricks({ level: 3 }); // => 9

resolveExpectedResult("MAKE_CONTRACT", { level: 3 }); // => 9
resolveExpectedResult("DEFEAT_CONTRACT", { level: 3 }); // => 8
resolveExpectedResult("PAR", { level: 3 }); // => null
resolveExpectedResult("10", null); // => 10

isResultReachable(9, { tricks: 10, partnership: "NS" }); // => true (NS capped at par)
isResultReachable(11, { tricks: 10, partnership: "NS" }); // => false

formatResultExpectedLabel("MAKE_CONTRACT", {
  makeContract: "Make the contract",
  defeatContract: "Defeat the contract",
  par: "Par",
  word: (count) => `${count} tricks`,
}); // => "Make the contract"