contra-renderer/www/js/moves/_moveInterpreter.ts
Daniel Perelman 9fbf7d18ac [WIP] Refactor to split interpreter into one file per move.
Currently just copied over the existing code and applied the quick
fixes to get it to compile. Each move should be refactored to be handle
its parameters earlier where applicable. But variants support should
probably be added first so both refactors can happen together.
2023-10-15 05:25:06 -07:00

341 lines
13 KiB
TypeScript

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<N extends MoveName> = new (args: MoveInterpreterCtorArgs<N>) => MoveInterpreter<N>;
export const moveInterpreters: Map<MoveName, MoveInterpreterCtor<MoveName>> = new Map<MoveName, MoveInterpreterCtor<MoveName>>();
export interface MoveInterpreterCtorArgs<N extends MoveName> {
move: Move & { move: N };
nextMove: Move;
numProgessions: number;
}
export type SemanticPositionsForAllDancers = Map<DancerIdentity, SemanticPosition>;
export interface MoveAsLowLevelMovesArgs {
startingPos: SemanticPositionsForAllDancers;
}
export type LowLevelMovesForAllDancers = Map<DancerIdentity, LowLevelMove[]>;
export interface Variant {
previousMoveVariant?: string,
lowLevelMoves: LowLevelMovesForAllDancers,
};
export type VariantCollection = Map<string, Variant>;
export type PartialLowLevelMove = {
remarks?: string,
beats: number,
startPosition?: SemanticPosition,
endPosition: SemanticPosition,
movementPattern: SemanticAnimation,
};
export interface ISingleVariantMoveInterpreter {
moveAsLowLevelMoves: () => LowLevelMovesForAllDancers;
moveAsVariants: () => VariantCollection;
};
export abstract class SingleVariantMoveInterpreter<T extends MoveInterpreter<N>, 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<string, Variant>([
["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<DancerIdentity, LowLevelMove[]> {
const res = new Map<DancerIdentity, LowLevelMove[]>();
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<DancerIdentity, LowLevelMove[]> {
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<DancerIdentity, LowLevelMove[]> {
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<DancerIdentity, LowLevelMove[]> {
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<N extends MoveName> {
public readonly move: Move & { move: N };
public readonly nextMove: Move;
public readonly numProgressions: number;
constructor({ move, nextMove, numProgessions }: MoveInterpreterCtorArgs<N>) {
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<DefaultMoveInterpreter, MoveName> {
moveAsLowLevelMoves(): LowLevelMovesForAllDancers {
return this.errorStandStill();
}
}
class DefaultMoveInterpreter extends MoveInterpreter<MoveName> {
buildSingleVariantMoveInterpreter(startingPos: SemanticPositionsForAllDancers): ISingleVariantMoveInterpreter {
throw new Error("Method not implemented.");
}
}
export const errorMoveInterpreterCtor: MoveInterpreterCtor<MoveName> = DefaultMoveInterpreter;