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
namesare the four direction names in N, E, S, W order.codeis a free-form language code, orANY_LANGUAGE("*") for an entry that is active whatever language scope is requested.matcherbuilds 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 matchNT,S5orS'est. Default ismatchers.word().elisionlistsElisionRules (baseForm,elidedForm,vowelDirections) re-applied after rotation, e.g. Frenchde Est→d'Est.
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.
| Method | Signature | Description |
|---|---|---|
rotateText | (text: string, count: number, options?: RotaterCallOptions) => string | Rotate direction names and standalone seat letters. |
rotateTextWithLog | (text, count, options?) => RotateWithLogResult | Same, plus every Substitution applied (from, to, entryIndex). |
rotateDealNTimes | (deal: Deal, count: number, options?) => Deal | Rotate every seat-bearing field and comment. Returns the same reference when count % 4 === 0. |
rotateDealToTarget | (deal: Deal, target?: Player, options?) => RotatedDeal | Rotate so the declarer (from contract, else metadata) sits at target (default South). Unchanged when the declarer is unknown or already there. |
Prop
Type
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 }]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)