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:
| Access | What 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.
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()); // => trueBid 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.
| Function | Signature | Description |
|---|---|---|
isBid | (value: unknown) => value is Bid | Any valid bid string, regular or special. |
parseBid | (value: string) => Bid | null | The entry for untrusted strings: typed bid, or null when invalid. No casing is applied ("Pass" is mixed case). |
toBid | (input: BidInput) => Bid | Canonical 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 | null | Reader: { level, suit } for a regular bid, null for special bids. |
toTrumpSuit | (denomination: Denomination) => Suit | null | null for NT. |
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.
| Function | Signature | Description |
|---|---|---|
auctionEntries | <TComment>(auction: Auction<TComment>) => BidInfo<TComment>[] | Each call with its seat and 0-based position; alert and comment are kept. |
auctionTurn | (auction: Auction) => Player | The seat whose turn it is to call. |
Prop
Type
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.
| Function | Signature | Description |
|---|---|---|
isValidBid | (bid: BidInput, auction: Auction) => boolean | Legal 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) => boolean | Three passes after a bid, or passed out. |
isAuctionPassedOut | (auction) => boolean | Four 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) => boolean | The opponents of the seat on turn hold the last contract bid and nobody has doubled it yet. |
isRedoubleable | (auction) => boolean | The side on turn holds the last contract bid, doubled by the opponents and not yet redoubled. |
isBidAPass | (bid: BidInput | BidComposite) => boolean | Accepts the string, the parts or the composite form. |
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.
| Function | Signature | Description |
|---|---|---|
createEmptyAuction | (dealer?: Player) => Auction | No call yet; the dealer defaults to South. |
addBids | (auction, ...bids: CallInput[]) => Auction | Appends the canonical bid strings in order. Returns the same auction when no bid is given. |
alertLastBid | (auction) => Auction | Flags the last call as alerted. Returns the same auction when it is empty. |
closeAuction | (auction) => Auction | Appends the passes reported by isAuctionCloseable. |
setDealerPlayer | (auction, dealer: Player) => Auction | Sets the dealer; every call moves with the seats. |
rotateToDeclarer | (auction, target: Player) => Auction | Changes the dealer so the declarer of the confirmed contract sits at target. No-op while the auction is open. |
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; // => 6Bid 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
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
| Function | Signature | Description |
|---|---|---|
calculateContract | (auction: Auction) => ContractResult | null | null when no contract bid has been made. |
getDeclarer | (auction: Auction) => Player | First player of the last bidder's partnership to have named the final denomination; South when there is no contract bid. |
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.
| Function | Signature | Description |
|---|---|---|
deriveContractResultFromAuction | (auction: { first: Player; moves: readonly { bid: string }[] }) => ContractResult | null | Types the calls as bids and delegates to calculateContract. |
deriveFinalDeclarerFromAuction | (auction, storedContract: string | null | undefined) => Player | null | A complete auction is authoritative. A truncated one (closing passes omitted) only counts when the stored contract's denomination matches the candidate's. |
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
| Function | Signature | Description |
|---|---|---|
determineContractLevel | (contract: string | null) => ContractLevel | "partial", "game", "small_slam" or "grand_slam"; unparsable input is "partial". |
determineContractLevel("3NT"); // => "game"
determineContractLevel("6S"); // => "small_slam"Opener and counter status
| Function | Signature | Description |
|---|---|---|
determineOpener | (auction: Auction, playerCount?: 2 | 4) => Player | null | First 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 CounterStatus | Exact membership in "none" | "X" | "XX". |
The empty call some legacy rows hold is ignored by the three auction readers.
determineOpener(played); // => "S"
getCounterStatus(played); // => "none" (the double was overcalled)
isCounterStatus("XX"); // => trueExpected 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.
| Function | Signature | Description |
|---|---|---|
getRequiredTricks | (contract: { level: number }) => number | level + 6. |
resolveExpectedResult | (rawResult: string | number | null | undefined, contract: { level: number } | null | undefined) => number | null | MAKE_CONTRACT → level + 6, DEFEAT_CONTRACT → level + 5, PAR and unset → null. |
isResultReachable | (tricks: number, hint: NSParHint) => boolean | With 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) => string | Localised label for a sentinel, or labels.word(count) for a number. |
Prop
Type
Prop
Type
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"