Bridge Champ uses a small textual DSL to describe constraints on a generated bridge deal. An expression is compiled to CP-SAT constraints and solved repeatedly to produce deals that satisfy the predicate.
It is used by the generateBoards and applyBoardExpression endpoints, and by the Expression Templates feature, where named expressions can be stored, shared, and embedded in other expressions via ${<templateId>}.
condition shape(north, any 4333 + any 4432)
and hcp(north) >= 15 and hcp(north) <= 17
and not hascard(north, AC)
condition is optional.and / && (high precedence) and or / || (low precedence). Parentheses are allowed.# starts a line comment that runs to end-of-line.script := 'condition'? orExpr?
orExpr := andExpr (('or' | '||') andExpr)*
andExpr := primary (('and' | '&&') primary)*
primary := '(' orExpr ')' | atom
atom := ('not' | '!')? functionCall
| linearExpr CMP linearExpr
linearExpr := term (('+' | '-') term)*
term := INT | countFn '(' handSet ')'
countFn := 'hcp' | 'spades' | 'hearts' | 'diamonds' | 'clubs'
CMP := '>=' | '<=' | '>' | '<' | '==' | '!='
not / ! is only valid in front of hascard(...), see "Negation" below.
handSet)| Token | Meaning | Valid inside… |
|---|---|---|
north, n |
North | any function |
east, e |
East | any function |
south, s |
South | any function |
west, w |
West | any function |
ns |
North + South (pair total) | only hcp/spades/… comparisons |
ew |
East + West (pair total) | only hcp/spades/… comparisons |
Empty parentheses are accepted as a stylistic convenience: hcp(ns()) is equivalent to hcp(ns).
spades / spade / s, hearts / heart / h, diamonds / diamond / d, clubs / club / c.
Two characters identifying one card: one suit (S H D C) and one rank (A K Q J T 9 8 7 6 5 4 3 2). Either order works: AS and SA both mean the ace of spades.
A bare run of PBN rank characters, e.g. AK for ace+king, AKQJT for the top five. No separators, no quotes.
Four characters in S H D C order. Each character is either a digit 0–9 or x (wildcard). The four lengths must sum to 13.
4333, positional: exactly S=4, H=3, D=3, C=3.5xxx, exactly five spades; the other three lengths are any non-negative integers that sum to 8.4xx2, exactly 4 spades and 2 clubs; hearts and diamonds split the remaining 7.Shape literals can be combined with + (set union) and - (set difference), and the prefix any expands a literal into all of its unique permutations.
Each function below produces a single atom. Multiple atoms are combined with and / or.
hcp(seat | pair) OP n, high-card-point comparisonCounts honour points (A=4, K=3, Q=2, J=1) for a single seat or a pair.
hcp(north) >= 15 and hcp(north) <= 17 # NT-range opening
hcp(south) == 12
hcp(north) != 11 # any HCP except 11
hcp(ns) >= 25 # game-going pair count
hcp(ns) > hcp(ew) # NS holds the majority
hcp(ns) != hcp(ew) # one side strictly ahead
hcp(ns) - hcp(ew) >= 10 # 10+ point swing for NS
10 + hcp(ew) <= hcp(ns) # same thing, integers on the LHS
Allowed comparison operators: >=, <=, >, <, ==, !=.
spades / hearts / diamonds / clubs (seat | pair) OP n, suit-length comparisonSame shape as hcp, but counts cards in the named suit instead.
hearts(east) >= 6 # East has at least 6 hearts
clubs(west) == 0 # West is void in clubs
spades(north) != 4 # any spade length except four
spades(north) + spades(south) >= 8 # NS 8-card spade fit
hearts(south) > clubs(south) # more hearts than clubs for South
diamonds(ns) <= 6 # short diamond holding for NS pair
When both sides of the comparison reduce to a single seat against an integer and the operator is one of >=, <=, >, <, ==, the constraint is collapsed into a length range; combinations like hearts(east) >= 6 and hearts(east) <= 7 are stored as a single 6..7 range internally. != keeps its full linear-comparison representation.
shape(seat, shapeExpr), allowed length patternsRestricts the (S, H, D, C) length tuple for one seat. Pair operands are not allowed here.
shape(north, 4333) # exactly 4-3-3-3
shape(north, any 4333) # any permutation of 4-3-3-3
shape(north, any 4333 + any 4432) # balanced (union)
shape(south, any 5332 - 5xxx) # 5332 patterns, but not with 5 spades
shape(east, 5xxx) # exactly 5 spades, the rest free
shape(west, x5xx + x6xx) # 5+ hearts (5 or 6), other lengths free
+ is set union and - is set difference, applied left-to-right. Multiple shape(seat, …) atoms for the same seat are intersected, not unioned, express alternatives with + inside one shape(...).
hascard(seat, CARD), force a cardhascard(north, AS) # North has the ace of spades
hascard(south, TC) # South has the ten of clubs (T = 10)
hascard(east, SA) # suit-first form is also fine
This is the only function that can be negated directly:
not hascard(north, AC) # North does NOT hold the ace of clubs
!hascard(north, AC) # equivalent, '!' is an alias for 'not'
includeCards(seat, suit, RANKS), force several ranks of one suitincludeCards(north, spades, AK) # North holds A♠ and K♠
includeCards(south, hearts, AKQJT) # South holds all top-5 hearts
includeCards(east, clubs, Q) # equivalent to hascard(east, QC)
excludeCards(seat, suit, RANKS), forbid several ranks of one suitexcludeCards(north, clubs, AKQ) # North is missing A♣, K♣, Q♣
excludeCards(west, diamonds, AK) # West has neither the ace nor king of diamonds
includeCards and excludeCards cannot themselves be negated, use the opposite function instead.
and / &&, conjunction (high precedence)hcp(north) >= 12 and hcp(north) <= 14 and shape(north, any 4432)
spades(south) >= 5 && hcp(south) >= 10
or / ||, disjunction (low precedence)shape(north, any 4333) or shape(north, any 4432) or shape(north, any 5332)
hcp(north) >= 22 || (hcp(north) >= 20 and shape(north, any 5xxx))
or is normalized into DNF, then each disjunct is solved as a separate SAT problem and the results are interleaved.
Use parentheses to group lower-precedence operators:
(hcp(north) >= 15 and hcp(north) <= 17) or hcp(north) >= 22
not (or !) is only valid immediately in front of hascard(...). The following raise parse errors:
not hcp(...) OP n, not spades(...) OP n, etc., invert the operator instead (e.g. use != rather than not ==).not shape(...), use + / - inside the shape expression.not includeCards(...) / not excludeCards(...), use the opposite function.not (expr), not cannot wrap a parenthesized boolean expression; push the negation down to individual hascard atoms.# Strong NT opener
hcp(north) >= 15 and hcp(north) <= 17 # 15-17 HCP
and shape(north, any 4333 + any 4432 + any 5332)
# runs to end-of-line. Block comments are not supported.
A stored Expression Template can be embedded in any expression with ${<templateName>}, where <templateName> is the template's name as shown in the template list. The reference is replaced with the template's expression, wrapped in parentheses to preserve operator precedence.
# A template named "Strong NT" has body:
# hcp(north) >= 15 and hcp(north) <= 17 and shape(north, any 4333 + any 4432 + any 5332)
# Reuse it in a larger expression:
${Strong NT} and hearts(south) >= 5
# Combine two templates with `or`:
${Strong NT} or ${Weak Two}
# Templates can reference other templates; nesting is allowed up to 8 levels deep.
Rules:
Name (own) and Name (global); type the reference in that form to pick the one you mean, e.g. ${Strong NT (global)}.${Strong NT@180}. The rotation must be a multiple of 90.The following are not available:
tricks(...) evaluation.control, loser, quality, cccc, aces, kings, queens, jacks, tens, top2..5, c13 evaluators.pointcount redefinition.hcp / spades / hearts / diamonds / clubs comparisons documented above.hascard, and not over parenthesized boolean expressions.# 1NT opener (15-17 balanced) opposite a 5-card major in responder
hcp(north) >= 15 and hcp(north) <= 17
and shape(north, any 4333 + any 4432 + any 5332)
and (hearts(south) >= 5 or spades(south) >= 5)
and hcp(south) >= 8
# Strong-2C-ish: any 22+ HCP hand for South, or a 9-trick suit
hcp(south) >= 22
or (hcp(south) >= 17 and (spades(south) >= 7 or hearts(south) >= 7))
# Preempt drill: 7-card suit, weak hand, missing the ace of trumps
spades(west) == 7
and hcp(west) >= 5 and hcp(west) <= 10
and not hascard(west, AS)
# Misfit drill: NS and EW within one point of each other but not equal
hcp(ns) - hcp(ew) >= -1
and hcp(ns) - hcp(ew) <= 1
and hcp(ns) != hcp(ew)
# Forcing a slam-zone deal for NS with a specific control card
hcp(ns) >= 33
and hascard(north, AS)
and includeCards(south, hearts, AK)
and excludeCards(east, clubs, A)