contra-renderer/www/js/moves/_moveInterpreter.ts

439 lines
16 KiB
TypeScript

import { CoupleRole, DanceRole, DancerIdentity, ExtendedDancerIdentity } from "../danceCommon.js";
import { BalanceWeight, CirclePosition, CircleSideOrCenter, DancerDistance, LongLines, 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 AllVariantsForMoveArgs = Map<string, MoveAsLowLevelMovesArgs>;
export type PartialLowLevelMove = {
remarks?: string,
beats: number,
startPosition?: SemanticPosition,
endPosition: SemanticPosition,
movementPattern: SemanticAnimation,
};
export interface ISingleVariantMoveInterpreter {
moveAsVariants: (previousMoveVariant: string | undefined) => 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;
const checkDistance = !this.moveInterpreter.allowStartingClose();
const checkBalance = !this.moveInterpreter.allowStartingBalance();
const checkLongLines = !this.moveInterpreter.allowStartingLongLines();
if (checkBalance || checkDistance || checkLongLines) {
for (const [id, startPos] of this.startingPos) {
if (checkBalance && startPos.balance) {
throw new Error("Can not start " + this.move.move + " with balance weight " + startPos.balance);
}
if (checkDistance && startPos.dancerDistance && startPos.dancerDistance !== DancerDistance.Normal) {
throw new Error("Can not start " + this.move.move + " at dancerDistance " + startPos.dancerDistance);
}
if (checkLongLines && startPos.longLines) {
throw new Error("Can not start " + this.move.move + " at long lines " + startPos.longLines);
}
}
}
}
get move() : Move & { move: N } {
return this.moveInterpreter.move;
}
moveAsLowLevelMoves(): LowLevelMovesForAllDancers {
throw new Error("You must implement either moveAsLowLevelMoves() or moveAsVariants().");
}
// TODO It would make more sense for previousMoveVariant to get passed into the constructor...
// ... but that requires touching every subclass, so it's an annoying refactor.
moveAsVariants(previousMoveVariant: string): VariantCollection {
return new Map<string, Variant>([
["default", { lowLevelMoves: this.moveAsLowLevelMoves(), previousMoveVariant }]
]);
}
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(error?: string) {
return this.handleMove(({ startPos }) => {
return [{
interpreterError: error ?? ("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;
}
allowStartingClose(): boolean {
// Swings can end close, but most moves can't start close, so do this check by default for all moves.
return false;
}
allowStartingBalance(): boolean {
// Make moves that support balance beforehand explicitly opt-in.
return false;
}
allowStartingLongLines(): boolean {
// Long lines is used for tweaking positioning; most moves don't know about it.
return false;
}
abstract buildSingleVariantMoveInterpreter(startingPos: SemanticPositionsForAllDancers): ISingleVariantMoveInterpreter;
// TODO Better name?
moveAsVariants({ startingPos }: MoveAsLowLevelMovesArgs, previousMoveVariant: string): VariantCollection {
return this.buildSingleVariantMoveInterpreter(startingPos).moveAsVariants(previousMoveVariant);
}
allVariantsForMove(args: AllVariantsForMoveArgs): VariantCollection {
const res = new Map<string, Variant>();
let error;
for (const [variantName, variantArgs] of args.entries()) {
let newVariants: VariantCollection;
try {
newVariants = this.moveAsVariants(variantArgs, variantName);
} catch (ex) {
// TODO Maybe have a way to distinguish invalid start from error processing move?
error = ex;
// If this variant can't be processed, just continue.
continue;
}
for (const [newVariantName, variant] of newVariants) {
let combinedVariantName: string;
if (args.size === 1) {
combinedVariantName = newVariantName;
} else if (newVariants.size === 1) {
combinedVariantName = variantName;
} else {
combinedVariantName = newVariantName + "_from_" + variantName;
}
res.set(combinedVariantName, variant);
}
}
if (res.size === 0) throw error;
else if (res.size > 1) {
// TODO Try to reduce variants if possible.
// XXX TODO Simple hack: perfer starting improper in a circle.
if ([...res.values()].find(v => v.previousMoveVariant === "InitialCircle")) {
for (const key of [...res.entries()].filter(([k, v]) => v.previousMoveVariant !== "InitialCircle").map(([k, v]) => k)) {
res.delete(key);
}
}
}
return res;
}
}
class ErrorSingleVariantMoveInterpreter extends SingleVariantMoveInterpreter<ErrorMoveInterpreter, MoveName> {
moveAsLowLevelMoves(): LowLevelMovesForAllDancers {
return this.errorStandStill(this.moveInterpreter.error);
}
}
export class ErrorMoveInterpreter extends MoveInterpreter<MoveName> {
public readonly error?: string;
constructor(args: MoveInterpreterCtorArgs<MoveName>, error?: string) {
super(args);
this.error = error;
}
override allowStartingBalance(): boolean {
return true;
}
override allowStartingClose(): boolean {
return true;
}
override allowStartingLongLines(): boolean {
return true;
}
buildSingleVariantMoveInterpreter(startingPos: SemanticPositionsForAllDancers): ISingleVariantMoveInterpreter {
//throw new Error("Method not implemented.");
return new ErrorSingleVariantMoveInterpreter(this, startingPos);
}
}
export const errorMoveInterpreterCtor: MoveInterpreterCtor<MoveName> = ErrorMoveInterpreter;Error