import { CoupleRole, DanceRole, DancerIdentity, ExtendedDancerIdentity } from "../danceCommon.js"; import { CirclePosition, CircleSideOrCenter, PositionKind, SemanticPosition } from "../interpreterCommon.js"; import { Move, chooser_pairz } from "../libfigureMapper.js"; import { LowLevelMove, SemanticAnimation, SemanticAnimationKind } from "../lowLevelMove.js"; type MoveName = string & Move["move"]; export type MoveInterpreterCtor = new (args: MoveInterpreterCtorArgs) => MoveInterpreter; export const moveInterpreters: Map> = new Map>(); export interface MoveInterpreterCtorArgs { move: Move & { move: N }; nextMove: Move; numProgessions: number; } export type SemanticPositionsForAllDancers = Map; export interface MoveAsLowLevelMovesArgs { startingPos: SemanticPositionsForAllDancers; } export type LowLevelMovesForAllDancers = Map; export interface Variant { previousMoveVariant?: string, lowLevelMoves: LowLevelMovesForAllDancers, }; export type VariantCollection = Map; export type PartialLowLevelMove = { remarks?: string, beats: number, startPosition?: SemanticPosition, endPosition: SemanticPosition, movementPattern: SemanticAnimation, }; export interface ISingleVariantMoveInterpreter { moveAsLowLevelMoves: () => LowLevelMovesForAllDancers; moveAsVariants: () => VariantCollection; }; export abstract class SingleVariantMoveInterpreter, N extends MoveName> implements ISingleVariantMoveInterpreter { protected readonly moveInterpreter: T; protected readonly startingPos: SemanticPositionsForAllDancers; constructor(moveInterpreter: T, startingPos: SemanticPositionsForAllDancers) { this.moveInterpreter = moveInterpreter; this.startingPos = startingPos; } get move() : Move & { move: N } { return this.moveInterpreter.move; } abstract moveAsLowLevelMoves(): LowLevelMovesForAllDancers; moveAsVariants(): VariantCollection { return new Map([ ["default", { lowLevelMoves: this.moveAsLowLevelMoves() }] ]); } static append(moves: LowLevelMove[], newMove: LowLevelMove | PartialLowLevelMove | ((prevEnd: SemanticPosition) => PartialLowLevelMove)): LowLevelMove[] { const lastMove = moves.at(-1)!; const prevEnd = lastMove.endPosition; if (typeof newMove === 'function') { newMove = newMove(prevEnd); } if (!newMove.startPosition) { newMove.startPosition = prevEnd; } moves.push({ ...newMove, startPosition: newMove.startPosition ?? prevEnd, move: lastMove.move, startBeat: lastMove.startBeat + lastMove.beats, }); return moves; } combine(moves: ((LowLevelMove | PartialLowLevelMove | ((prevEnd: SemanticPosition) => PartialLowLevelMove))[]), startPos?: SemanticPosition): LowLevelMove[] { const res: LowLevelMove[] = []; if (moves.length === 0) return res; let firstMove = moves[0]; if ('move' in firstMove) { res.push(firstMove); } else { if (typeof firstMove === 'function') { firstMove = firstMove(startPos!); } res.push({...firstMove, move: this.move, startBeat: 0, startPosition: firstMove.startPosition ?? startPos!, }); } for (const move of moves.slice(1)) { SingleVariantMoveInterpreter.append(res, move); } if (res[0].startPosition === undefined) { throw new Error("combine() called without a startPosition."); } return res; } findPairOpposite(who: chooser_pairz, id: DancerIdentity): ExtendedDancerIdentity | null { const pos = this.getPosFor(id.asExtendedDancerIdentity()); const inSameSet = (proposedId: ExtendedDancerIdentity) => { const proposedPos = this.getPosFor(proposedId); return { ...proposedId, // Get the same role dancer in the set the dancer is currently in. relativeSet: proposedId.relativeSet + (pos.setOffset - proposedPos.setOffset) } } switch (who) { case "partners": return id.partner().asExtendedDancerIdentity(); case "neighbors": case "next neighbors": case "3rd neighbors": // TODO This isn't quite right... especially if it's "next" to intentionally progress... return inSameSet(id.neighbor().asExtendedDancerIdentity()); // These three might get used when not with neighbors? case "gentlespoons": case "ladles": case "same roles": if (who === "gentlespoons" && id.danceRole === DanceRole.Robin || who === "ladles" && id.danceRole === DanceRole.Lark) { return null; } return inSameSet(id.oppositeSameRole().asExtendedDancerIdentity()); case "ones": if (id.coupleRole === CoupleRole.Twos) return null; return id.partner().asExtendedDancerIdentity(); case "twos": if (id.coupleRole === CoupleRole.Ones) return null; return id.partner().asExtendedDancerIdentity(); case "shadows": throw new Error("Not sure shadow is consistently the same."); case "first corners": case "second corners": throw new Error("Contra corners are unsupported."); default: throw new Error("Unsupported who: " + who); } } getPosFor(id: ExtendedDancerIdentity): SemanticPosition & { setOffset: number, lineOffset: number } { const basePos = this.startingPos.get(id.setIdentity)!; return {...basePos, setOffset: (basePos.setOffset ?? 0) + id.relativeSet, lineOffset: (basePos.lineOffset ?? 0) + id.relativeLine, }; } handleMove(dancerFunc: ((arg: { id: DancerIdentity, startPos: SemanticPosition }) => LowLevelMove[])): Map { const res = new Map(); let anyProgressed = false; for (const [id, startPos] of this.startingPos.entries()) { const lowLevelMoves = dancerFunc({ id, startPos }); if (this.move.progression) { const startingPos: SemanticPosition = lowLevelMoves.at(0)?.startPosition!; const endPos: SemanticPosition = lowLevelMoves.at(-1)?.endPosition!; if (startingPos.setOffset !== endPos.setOffset) { anyProgressed = true; } } res.set(id, lowLevelMoves); } if (this.move.progression && !anyProgressed) { for (const [id, lowLevelMoves] of res.entries()) { const startingPos: SemanticPosition = lowLevelMoves.at(0)?.startPosition!; const endPos: SemanticPosition = lowLevelMoves.at(-1)?.endPosition!; if (startingPos.setOffset === endPos.setOffset && endPos.kind === PositionKind.Circle) { const endSetOffset = (endPos.setOffset ?? 0) + (endPos.which.isTop() ? -0.5 : +0.5); const endWhich = endPos.which.swapUpAndDown(); lowLevelMoves[lowLevelMoves.length - 1] = { ...lowLevelMoves[lowLevelMoves.length - 1], endPosition: { ...endPos, setOffset: endSetOffset, which: endWhich, } }; } } } return res; } handleCircleMove(dancerFunc: ((arg: { id: DancerIdentity, startPos: SemanticPosition & { kind: PositionKind.Circle } }) => LowLevelMove[])): Map { return this.handleMove(({ id, startPos }) => { if (startPos.kind !== PositionKind.Circle) { throw new Error(this.move.move + " must start in a circle, but " + id + " is at " + startPos); } return dancerFunc({ id, startPos }); }); } handlePairedMove(who: chooser_pairz, dancerFunc: ((arg: { id: DancerIdentity, startPos: SemanticPosition, withPos: SemanticPosition & { setOffset: number, lineOffset: number }, withId: ExtendedDancerIdentity, around: CircleSideOrCenter, }) => LowLevelMove[]), meanwhileFunc?: ((arg: { id: DancerIdentity, startPos: SemanticPosition, }) => LowLevelMove[])): Map { return this.handleMove(({ id, startPos }) => { const withId = this.findPairOpposite(who, id); if (!withId) { if (meanwhileFunc) { return meanwhileFunc({ id, startPos }); } else { return this.combine([{ beats: this.move.beats, startPosition: { ...startPos, hands: undefined }, endPosition: { ...startPos, hands: undefined }, // TODO Not sure this is actually a good default... movementPattern: { kind: SemanticAnimationKind.StandStill }, }]); } } const withPos = this.getPosFor(withId); const setDifference = withPos.setOffset - (startPos.setOffset ?? 0); let startPosAdjusted = startPos; if (setDifference !== 0) { // TODO Can move be with a different short line or just a different circle? // PassBy can probably be with the next short line... if (startPos.kind === PositionKind.Circle && (setDifference === 1 || setDifference === -1)) { startPosAdjusted = { ...startPos, setOffset: (startPos.setOffset ?? 0) + setDifference / 2, which: startPos.which.swapUpAndDown(), } } else { throw new Error("Not near dancer to " + this.move.move + " with."); } } const startWhich = startPosAdjusted.which; // TODO Can swing be across the set (top or bottom)? const around = withPos.which.leftRightSide() === startWhich.leftRightSide() ? startWhich.leftRightSide() : withPos.kind === PositionKind.Circle ? (startWhich instanceof CirclePosition && withPos.which.topBottomSide() === startWhich.topBottomSide() ? startWhich.topBottomSide() : "Center") : "Center"; return dancerFunc({ id, startPos: startPosAdjusted, withId, withPos, around }); }); } handleCirclePairedMove(who: chooser_pairz, dancerFunc: ((arg: { id: DancerIdentity, startPos: SemanticPosition & { kind: PositionKind.Circle }, withPos: SemanticPosition & { setOffset: number, lineOffset: number }, withId: ExtendedDancerIdentity, around: CircleSideOrCenter, }) => LowLevelMove[]), meanwhileFunc?: ((arg: { id: DancerIdentity, startPos: SemanticPosition & { kind: PositionKind.Circle }, }) => LowLevelMove[])): Map { return this.handlePairedMove(who, ({ id, startPos, withId, withPos, around }) => { if (startPos.kind !== PositionKind.Circle) { throw new Error(this.move.move + " must start in a circle, but " + id + " is at " + startPos); } return dancerFunc({ id, startPos, withId, withPos, around }); }, meanwhileFunc ? ({id, startPos}) => { if (startPos.kind !== PositionKind.Circle) { throw new Error(this.move.move + " must start in a circle, but " + id + " is at " + startPos); } return meanwhileFunc({id, startPos}); } : undefined); } errorStandStill() { return this.handleMove(({ startPos }) => { return [{ interpreterError: "UNKNOWN MOVE '" + this.move.move + "': standing still", move: this.move, startBeat: 0, beats: this.move.beats, startPosition: startPos, endPosition: startPos, movementPattern: { kind: SemanticAnimationKind.StandStill, }, }]; }); } } export abstract class MoveInterpreter { public readonly move: Move & { move: N }; public readonly nextMove: Move; public readonly numProgressions: number; constructor({ move, nextMove, numProgessions }: MoveInterpreterCtorArgs) { this.move = move; this.nextMove = nextMove; // TODO Should be able to get rid of this using variants. this.numProgressions = numProgessions; } abstract buildSingleVariantMoveInterpreter(startingPos: SemanticPositionsForAllDancers): ISingleVariantMoveInterpreter; moveAsLowLevelMoves({ startingPos }: MoveAsLowLevelMovesArgs): LowLevelMovesForAllDancers { return this.buildSingleVariantMoveInterpreter(startingPos).moveAsLowLevelMoves(); } } class DefaultSingleVariantMoveInterpreter extends SingleVariantMoveInterpreter { moveAsLowLevelMoves(): LowLevelMovesForAllDancers { return this.errorStandStill(); } } class DefaultMoveInterpreter extends MoveInterpreter { buildSingleVariantMoveInterpreter(startingPos: SemanticPositionsForAllDancers): ISingleVariantMoveInterpreter { throw new Error("Method not implemented."); } } export const errorMoveInterpreterCtor: MoveInterpreterCtor = DefaultMoveInterpreter;