Bridge Training

Rotation

Rotate whole deals around the table, including player names inside comments.

Rotation moves everything clockwise around the table so a different seat ends up South (or North), without changing the deal itself.

Import from @workspace/bridge/rotation. It exposes createRotater, which rotates whole deals and comment text, and the language-table types. Rotating hands, auction state or vulnerability on their own is internal to the package; createRotater applies all of them together.

Rotating deals and comments

A Deal carries seats in many places: cards, auction.first, play.first, metadata.dealer, metadata.declarer, metadata.vulnerable, contract.declarer, player, and player names written inside review and move comments. createRotater bundles all of it behind one object.

The package ships no language table

The package holds no direction names. You pass the entries (one per language, plus an optional language-agnostic entry for single letters). Without entries, comment text is left untouched.

Language entries

Prop

Type

  • names are the four direction names in N, E, S, W order.
  • code is a free-form language code, or ANY_LANGUAGE ("*") for an entry that is active whatever language scope is requested.
  • matcher builds the regex that finds a name. matchers.word(opts?) uses Unicode word boundaries with optional per-position negative lookaheads (guards, keyed 0–3). matchers.strict() also rejects digits, _, \ and apostrophes as neighbours, for single-letter tokens that must not match NT, S5 or S'est. Default is matchers.word().
  • elision lists ElisionRules (baseForm, elidedForm, vowelDirections) re-applied after rotation, e.g. French de Est → d'Est.
rotation.ts
const entries: readonly LanguageEntry[] = [
  { code: "en", names: ["North", "East", "South", "West"] },
  {
    code: "fr",
    names: ["Nord", "Est", "Sud", "Ouest"],
    // "de Est" → "d'Est", "d'Sud" → "de Sud"
    elision: [{ baseForm: "de", elidedForm: "d", vowelDirections: [1, 3] }],
    // skip the French verb "est" (and "est-ce", "est-il", …)
    matcher: matchers.word({ guards: { 1: "(e|-ce|-elle|-il|-on)" } }),
  },
  // single-letter seats, always active whatever the language scope
  { code: ANY_LANGUAGE, names: ["N", "E", "S", "W"], matcher: matchers.strict() },
];

Rotater

createRotater(config: RotaterConfig): Rotater pre-binds the entries and an optional defaultLanguage. On each call the language resolves as: options.language, else config.defaultLanguage, else scan every entry. A requested language absent from entries leaves the text untouched. The ANY_LANGUAGE entry is always active alongside the chosen one.

MethodSignatureDescription
rotateText(text: string, count: number, options?: RotaterCallOptions) => stringRotate direction names and standalone seat letters.
rotateTextWithLog(text, count, options?) => RotateWithLogResultSame, plus every Substitution applied (from, to, entryIndex).
rotateDealNTimes(deal: Deal, count: number, options?) => DealRotate every seat-bearing field and comment. Returns the same reference when count % 4 === 0.
rotateDealToTarget(deal: Deal, target?: Player, options?) => RotatedDealRotate so the declarer (from contract, else metadata) sits at target (default South). Unchanged when the declarer is unknown or already there.

Prop

Type

rotation.ts
const rotater = createRotater({ entries, defaultLanguage: "fr" });

rotater.rotateText("Entame de Est, Nord est en main", 2);
// => "Entame d'Ouest, Sud est en main"

rotater.rotateText("North leads", 2, { language: "en" }); // => "South leads"
rotater.rotateText("North leads", 2); // => "North leads" (fr scope: English names untouched)

const { text, substitutions } = rotater.rotateTextWithLog("N leads", 1);
text; // => "E leads"
substitutions; // => [{ from: "N", to: "E", entryIndex: 2 }]
rotation.ts
const deal = makeDeal({
  contract: { level: 3, suit: "NT", trump: null, declarer: "N", doubled: "none" },
  review: "Nord joue 3SA.",
});

const rotated = rotater.rotateDealToTarget(deal); // target defaults to South
rotated.rotationCount; // => 2
rotated.originalDeclarer; // => "N"
rotated.deal.contract?.declarer; // => "S"
rotated.deal.review; // => "Sud joue 3SA."

rotater.rotateDealNTimes(deal, 4); // => same reference (full turn)