Compare commits

..

2 Commits

2 changed files with 231 additions and 381 deletions

View File

@ -48,6 +48,7 @@ export const dances: ContraDBDance[] = [
{ "title": "Magnetic Waves", "choreographer_name": "Isaac Banner", "start_type": "Becket", "preamble": "", "figures": [{ "parameter_values": ["gentlespoons", "", false, "ladles%%2", "across", false, false, false, false, 14], "move": "hey" }, { "parameter_values": ["ladles", false, 90, 2], "move": "allemande", "note": "to short waves" }, { "parameter_values": [false, "across", true, "ladles", false, "partners", 4], "move": "form an ocean wave" }, { "parameter_values": ["partners", true, 180, 2], "move": "allemande" }, { "parameter_values": ["gentlespoons", false, false, 2], "move": "pull by dancers" }, { "parameter_values": ["neighbors", "none", 8], "move": "swing" }, { "parameter_values": [true, "across", true, "ladles", false, "neighbors", 8], "move": "form an ocean wave" }, { "parameter_values": ["along", true, 2], "move": "pass through", "progression": 1 }, { "parameter_values": ["ladles", 360, 6], "move": "mad robin" }, { "parameter_values": ["single-file circle left 1 place", 2], "move": "custom" }, { "parameter_values": ["partners", "meltdown", 14], "move": "swing" }], "notes": "", "hook": "Pass the ocean to mad robin progression", "id": 1987 }, { "title": "Magnetic Waves", "choreographer_name": "Isaac Banner", "start_type": "Becket", "preamble": "", "figures": [{ "parameter_values": ["gentlespoons", "", false, "ladles%%2", "across", false, false, false, false, 14], "move": "hey" }, { "parameter_values": ["ladles", false, 90, 2], "move": "allemande", "note": "to short waves" }, { "parameter_values": [false, "across", true, "ladles", false, "partners", 4], "move": "form an ocean wave" }, { "parameter_values": ["partners", true, 180, 2], "move": "allemande" }, { "parameter_values": ["gentlespoons", false, false, 2], "move": "pull by dancers" }, { "parameter_values": ["neighbors", "none", 8], "move": "swing" }, { "parameter_values": [true, "across", true, "ladles", false, "neighbors", 8], "move": "form an ocean wave" }, { "parameter_values": ["along", true, 2], "move": "pass through", "progression": 1 }, { "parameter_values": ["ladles", 360, 6], "move": "mad robin" }, { "parameter_values": ["single-file circle left 1 place", 2], "move": "custom" }, { "parameter_values": ["partners", "meltdown", 14], "move": "swing" }], "notes": "", "hook": "Pass the ocean to mad robin progression", "id": 1987 },
{ "title": "March for Andrea", "choreographer_name": "Isaac Banner", "start_type": "Becket", "preamble": "Andrea Nettleton told me she wished callers would do more down the halls, but all the options in my box were pretty basic and I wanted something that used it as less of an \"away we go\" and more of a buildup. ", "figures": [{ "parameter_values": ["ladles", true, "partners", 8], "move": "revolving door" }, { "parameter_values": ["neighbors", "none", 8], "move": "swing", "note": "and end facing down" }, { "parameter_values": ["everyone", "all", "forward", "turn-alone", 8], "move": "down the hall" }, { "parameter_values": ["everyone", "all", "forward", "circle", 8], "move": "up the hall" }, { "parameter_values": [4], "move": "balance the ring" }, { "parameter_values": ["partners", 4], "move": "California twirl", "progression": 1 }, { "parameter_values": [4], "move": "balance the ring" }, { "parameter_values": ["gentlespoons", "neighbors", true, 4], "move": "roll away" }, { "parameter_values": [4], "move": "balance the ring" }, { "parameter_values": ["gentlespoons", "partners", false, 4], "move": "give & take" }, { "parameter_values": ["partners", "none", 8], "move": "swing" }], "notes": "", "hook": "Down the hall without the default circle left", "id": 685 }, { "title": "March for Andrea", "choreographer_name": "Isaac Banner", "start_type": "Becket", "preamble": "Andrea Nettleton told me she wished callers would do more down the halls, but all the options in my box were pretty basic and I wanted something that used it as less of an \"away we go\" and more of a buildup. ", "figures": [{ "parameter_values": ["ladles", true, "partners", 8], "move": "revolving door" }, { "parameter_values": ["neighbors", "none", 8], "move": "swing", "note": "and end facing down" }, { "parameter_values": ["everyone", "all", "forward", "turn-alone", 8], "move": "down the hall" }, { "parameter_values": ["everyone", "all", "forward", "circle", 8], "move": "up the hall" }, { "parameter_values": [4], "move": "balance the ring" }, { "parameter_values": ["partners", 4], "move": "California twirl", "progression": 1 }, { "parameter_values": [4], "move": "balance the ring" }, { "parameter_values": ["gentlespoons", "neighbors", true, 4], "move": "roll away" }, { "parameter_values": [4], "move": "balance the ring" }, { "parameter_values": ["gentlespoons", "partners", false, 4], "move": "give & take" }, { "parameter_values": ["partners", "none", 8], "move": "swing" }], "notes": "", "hook": "Down the hall without the default circle left", "id": 685 },
{ "title": "Napkin #11", "choreographer_name": "Isaac Banner", "start_type": "improper", "preamble": "", "figures": [{ "parameter_values": ["neighbors", false, false, 4], "move": "box the gnat" }, { "parameter_values": ["ladles", false, true, 4], "move": "pull by dancers" }, { "parameter_values": ["partners", "none", 8], "move": "swing" }, { "parameter_values": ["gentlespoons", false, 540, 8], "move": "allemande" }, { "parameter_values": ["neighbors", "none", 8], "move": "swing" }, { "parameter_values": ["ladles", true, "across", 8], "move": "chain" }, { "parameter_values": [false, 4], "move": "long lines" }, { "parameter_values": ["ladles", "partners", false, 4], "move": "roll away", "note": "on the way back" }, { "parameter_values": [true, 270, 8], "move": "circle" }, { "parameter_values": ["neighbors", false, true, 4], "move": "box the gnat" }, { "parameter_values": ["neighbors", false, true, 4], "move": "pull by dancers", "progression": 1 }], "notes": "", "hook": "No-balance box the gnat to swat the flea w/ next", "id": 2050 }, { "title": "Napkin #11", "choreographer_name": "Isaac Banner", "start_type": "improper", "preamble": "", "figures": [{ "parameter_values": ["neighbors", false, false, 4], "move": "box the gnat" }, { "parameter_values": ["ladles", false, true, 4], "move": "pull by dancers" }, { "parameter_values": ["partners", "none", 8], "move": "swing" }, { "parameter_values": ["gentlespoons", false, 540, 8], "move": "allemande" }, { "parameter_values": ["neighbors", "none", 8], "move": "swing" }, { "parameter_values": ["ladles", true, "across", 8], "move": "chain" }, { "parameter_values": [false, 4], "move": "long lines" }, { "parameter_values": ["ladles", "partners", false, 4], "move": "roll away", "note": "on the way back" }, { "parameter_values": [true, 270, 8], "move": "circle" }, { "parameter_values": ["neighbors", false, true, 4], "move": "box the gnat" }, { "parameter_values": ["neighbors", false, true, 4], "move": "pull by dancers", "progression": 1 }], "notes": "", "hook": "No-balance box the gnat to swat the flea w/ next", "id": 2050 },
{ "title": "Napkin #12", "choreographer_name": "Isaac Banner", "start_type": "improper", "preamble": "", "figures": [{ "parameter_values": ["neighbors", true, true, 8], "move": "box the gnat" }, { "parameter_values": ["gentlespoons", false, 540, 8], "move": "allemande" }, { "parameter_values": ["partners", "ladles", true, "half", "across", false, true, false, false, 8], "move": "hey" }, { "parameter_values": ["ladles", false, "across", 8], "move": "chain", "note": "to partners" }, { "parameter_values": ["robins ricochet, larks slide left", 4], "move": "custom", "note": "" }, { "parameter_values": ["partners", "none", 12], "move": "swing" }, { "parameter_values": ["gentlespoons", "", false, "half", "across", false, true, false, false, 8], "move": "hey", "note": "and catch left hands" }, { "parameter_values": [false, 360, "", 8], "move": "star", "note": ", larks fall in behind neighbors", "progression": 1 }], "notes": "", "hook": "", "id": 2112 },
{ "title": "Napkin #8", "choreographer_name": "Isaac Banner", "start_type": "improper", "preamble": "", "figures": [{ "parameter_values": [true, 8], "move": "petronella", "note": "right, end facing shadows" }, { "parameter_values": [true, 8], "move": "petronella", "note": "left" }, { "parameter_values": ["neighbors", "balance", 16], "move": "swing" }, { "parameter_values": ["gentlespoons", false, false, 4], "move": "pull by dancers", "note": "along right diagonal", "progression": 1 }, { "parameter_values": ["partners", "meltdown", 12], "move": "swing" }, { "parameter_values": [true, 270, 8], "move": "circle" }, { "parameter_values": ["neighbors", true, 360, 8], "move": "do si do" }], "notes": "", "hook": "Petronella right to shadow petronella left", "id": 1981 }, { "title": "Napkin #8", "choreographer_name": "Isaac Banner", "start_type": "improper", "preamble": "", "figures": [{ "parameter_values": [true, 8], "move": "petronella", "note": "right, end facing shadows" }, { "parameter_values": [true, 8], "move": "petronella", "note": "left" }, { "parameter_values": ["neighbors", "balance", 16], "move": "swing" }, { "parameter_values": ["gentlespoons", false, false, 4], "move": "pull by dancers", "note": "along right diagonal", "progression": 1 }, { "parameter_values": ["partners", "meltdown", 12], "move": "swing" }, { "parameter_values": [true, 270, 8], "move": "circle" }, { "parameter_values": ["neighbors", true, 360, 8], "move": "do si do" }], "notes": "", "hook": "Petronella right to shadow petronella left", "id": 1981 },
{ "title": "New Year, Same Old Charlie", "choreographer_name": "Isaac Banner", "start_type": "improper", "preamble": "", "figures": [{ "parameter_values": ["neighbors", "balance", 16], "move": "swing", "note": ", end facing down the hall" }, { "parameter_values": ["everyone", "all", "forward", "turn-alone", 8], "move": "down the hall" }, { "parameter_values": ["everyone", "all", "forward", "", 8], "move": "up the hall", "note": "and ones cast down one couple to new rings", "progression": 1 }, { "parameter_values": [true, 8], "move": "petronella" }, { "parameter_values": ["partners", "none", 8], "move": "swing" }, { "parameter_values": [true, 8], "move": "petronella" }, { "parameter_values": [4], "move": "balance the ring" }, { "parameter_values": ["partners", 4], "move": "California twirl", "progression": 1 }], "notes": "A2 cast down is mirrored with the active couple turning over their outside shoulders to drop down into a new ring of foot while the inactive couples finish moving up the hall.", "hook": "Cast down to rings balance, petronella to swing", "id": 1935 }, { "title": "New Year, Same Old Charlie", "choreographer_name": "Isaac Banner", "start_type": "improper", "preamble": "", "figures": [{ "parameter_values": ["neighbors", "balance", 16], "move": "swing", "note": ", end facing down the hall" }, { "parameter_values": ["everyone", "all", "forward", "turn-alone", 8], "move": "down the hall" }, { "parameter_values": ["everyone", "all", "forward", "", 8], "move": "up the hall", "note": "and ones cast down one couple to new rings", "progression": 1 }, { "parameter_values": [true, 8], "move": "petronella" }, { "parameter_values": ["partners", "none", 8], "move": "swing" }, { "parameter_values": [true, 8], "move": "petronella" }, { "parameter_values": [4], "move": "balance the ring" }, { "parameter_values": ["partners", 4], "move": "California twirl", "progression": 1 }], "notes": "A2 cast down is mirrored with the active couple turning over their outside shoulders to drop down into a new ring of foot while the inactive couples finish moving up the hall.", "hook": "Cast down to rings balance, petronella to swing", "id": 1935 },
{ "title": "Not My Second Rodeo", "choreographer_name": "Isaac Banner", "start_type": "improper", "preamble": "", "figures": [{ "parameter_values": ["neighbors", "balance", 16], "move": "swing" }, { "parameter_values": ["gentlespoons", "", false, "half", "across", false, false, false, false, 8], "move": "hey" }, { "parameter_values": ["gentlespoons", false, 540, 8], "move": "allemande" }, { "parameter_values": ["partners", "meltdown", 16], "move": "swing" }, { "parameter_values": [true, 270, 8], "move": "circle" }, { "parameter_values": ["along", true, 0], "move": "pass through", "progression": 1 }, { "parameter_values": ["next neighbors", true, 360, 8], "move": "do si do" }], "notes": "Kind of but not really The Second Time Around by Jim Kitch. Hey-to-allemande offers a refreshing broken-hey-ish redux of allemande-to-hey and creates an illusion of less inactivity in the robin's role.", "hook": "", "id": 413 }, { "title": "Not My Second Rodeo", "choreographer_name": "Isaac Banner", "start_type": "improper", "preamble": "", "figures": [{ "parameter_values": ["neighbors", "balance", 16], "move": "swing" }, { "parameter_values": ["gentlespoons", "", false, "half", "across", false, false, false, false, 8], "move": "hey" }, { "parameter_values": ["gentlespoons", false, 540, 8], "move": "allemande" }, { "parameter_values": ["partners", "meltdown", 16], "move": "swing" }, { "parameter_values": [true, 270, 8], "move": "circle" }, { "parameter_values": ["along", true, 0], "move": "pass through", "progression": 1 }, { "parameter_values": ["next neighbors", true, 360, 8], "move": "do si do" }], "notes": "Kind of but not really The Second Time Around by Jim Kitch. Hey-to-allemande offers a refreshing broken-hey-ish redux of allemande-to-hey and creates an illusion of less inactivity in the robin's role.", "hook": "", "id": 413 },

View File

@ -201,7 +201,7 @@ function moveAsLowLevelMoves(move: Move, startingPos: Map<DancerIdentity, Semant
} }
} }
function getPosFor(id: common.ExtendedDancerIdentity) { function getPosFor(id: common.ExtendedDancerIdentity): SemanticPosition & { setOffset: number, lineOffset: number } {
const basePos = startingPos.get(id.setIdentity)!; const basePos = startingPos.get(id.setIdentity)!;
return {...basePos, return {...basePos,
setOffset: (basePos.setOffset ?? 0) + id.relativeSet, setOffset: (basePos.setOffset ?? 0) + id.relativeSet,
@ -231,19 +231,92 @@ function moveAsLowLevelMoves(move: Move, startingPos: Map<DancerIdentity, Semant
} }
} }
const res = new Map<DancerIdentity, LowLevelMove[]>(); function handleMove(dancerFunc: ((arg: { id: DancerIdentity, startPos: SemanticPosition }) => LowLevelMove[])): Map<DancerIdentity, LowLevelMove[]> {
const res = new Map<DancerIdentity, LowLevelMove[]>();
for (const [id, startPos] of startingPos.entries()) {
res.set(id, dancerFunc({ id, startPos }));
}
return res;
}
function handleCircleMove(dancerFunc: ((arg: { id: DancerIdentity, startPos: SemanticPosition & { kind: PositionKind.Circle } }) => LowLevelMove[])): Map<DancerIdentity, LowLevelMove[]> {
return handleMove(({ id, startPos }) => {
if (startPos.kind !== PositionKind.Circle) {
throw move.move + " must start in a circle, but " + id + " is at " + startPos;
}
return dancerFunc({ id, startPos });
});
}
function handlePairedMove(who: chooser_pairz, dancerFunc: ((arg: {
id: DancerIdentity,
startPos: SemanticPosition,
withPos: SemanticPosition & { setOffset: number, lineOffset: number },
withId: common.ExtendedDancerIdentity,
around: CircleSideOrCenter,
}) => LowLevelMove[])): Map<DancerIdentity, LowLevelMove[]> {
return handleMove(({ id, startPos }) => {
const withId = findPairOpposite(who, id);
if (!withId) {
return combine([{
beats: move.beats,
startPosition: { ...startPos, hands: undefined },
endPosition: { ...startPos, hands: undefined },
movementPattern: { kind: SemanticAnimationKind.StandStill },
}]);
}
const withPos = 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 "Not near dancer to " + move.move + " with.";
}
}
const startWhich = startPosAdjusted.which;
// TODO Can swing be across the set (top or bottom)?
const around = withPos.kind === PositionKind.Circle
? (withPos.which.leftRightSide() === startWhich.leftRightSide()
? startWhich.leftRightSide()
: startWhich instanceof CirclePosition && withPos.which.topBottomSide() === startWhich.topBottomSide()
? startWhich.topBottomSide()
: "Center")
: "Center";
return dancerFunc({ id, startPos: startPosAdjusted, withId, withPos, around });
});
}
function handleCirclePairedMove(who: chooser_pairz, dancerFunc: ((arg: {
id: DancerIdentity,
startPos: SemanticPosition & { kind: PositionKind.Circle },
withPos: SemanticPosition & { setOffset: number, lineOffset: number },
withId: common.ExtendedDancerIdentity,
around: CircleSideOrCenter,
}) => LowLevelMove[])): Map<DancerIdentity, LowLevelMove[]> {
return handlePairedMove(who, ({ id, startPos, withId, withPos, around }) => {
if (startPos.kind !== PositionKind.Circle) {
throw move.move + " must start in a circle, but " + id + " is at " + startPos;
}
return dancerFunc({ id, startPos, withId, withPos, around });
});
}
switch (move.move) { switch (move.move) {
case "balance the ring": case "balance the ring":
for (const [id, startPos] of startingPos.entries()) { return handleCircleMove(({ startPos }) => balanceCircleInAndOut(move, startPos));
res.set(id, balanceCircleInAndOut(move, startPos));
}
return res;
case "petronella": case "petronella":
for (const [id, startPos] of startingPos.entries()) { return handleCircleMove(({ startPos }) => {
if (startPos.kind !== PositionKind.Circle) {
throw move.move + " must start in a circle, but " + id + " is at " + startPos;
}
// TODO These should be actual parameters, not parsing the notes... // TODO These should be actual parameters, not parsing the notes...
const rightShoulder: boolean = !(move.note?.includes('left') ?? false); const rightShoulder: boolean = !(move.note?.includes('left') ?? false);
const newCircle: boolean = move.note?.includes('end facing') ?? false; const newCircle: boolean = move.note?.includes('end facing') ?? false;
@ -262,52 +335,37 @@ function moveAsLowLevelMoves(move: Move, startingPos: Map<DancerIdentity, Semant
} }
} }
const spin: ((prevEnd: SemanticPosition) => PartialLowLevelMove) = prevEnd => ({
beats: move.beats - (move.parameters.bal ? 4 : 0),
startPosition: {
...prevEnd,
facing: Facing.CenterOfCircle,
hands: undefined,
},
endPosition: finalPosition,
movementPattern: {
kind: SemanticAnimationKind.Linear,
minRotation: rightShoulder ? 180 : -180,
handsDuring: "None",
},
});
if (move.parameters.bal) { if (move.parameters.bal) {
const balance: LowLevelMove[] = balanceCircleInAndOut(move, startPos); const balance: LowLevelMove[] = balanceCircleInAndOut(move, startPos);
res.set(id, append([...balance], prevEnd => ({ return append([...balance], spin);
beats: move.beats - 4,
startPosition: {
...prevEnd,
hands: undefined,
},
endPosition: finalPosition,
movementPattern: {
kind: SemanticAnimationKind.Linear,
minRotation: rightShoulder ? 180 : -180,
handsDuring: "None",
},
})));
} else { } else {
res.set(id, combine([{ return combine([spin]);
beats: move.beats,
startPosition: {
...startPos,
facing: Facing.CenterOfCircle,
hands: undefined,
},
endPosition: finalPosition,
movementPattern: {
kind: SemanticAnimationKind.Linear,
minRotation: rightShoulder ? 180 : -180,
handsDuring: "None",
},
}]));
} }
} });
return res;
case "mad robin": case "mad robin":
for (const [id, startPos] of startingPos.entries()) { return handleCircleMove(({ startPos }) => {
if (startPos.kind !== PositionKind.Circle) {
throw move.move + " must start in a circle, but " + id + " is at " + startPos;
}
// TODO Read who of mad robin to decide direction? // TODO Read who of mad robin to decide direction?
const startAndEndPos = { const startAndEndPos = {
...startPos, ...startPos,
facing: startPos.which.isLeft() ? Facing.Right : Facing.Left, facing: startPos.which.isLeft() ? Facing.Right : Facing.Left,
hands: undefined, hands: undefined,
}; };
res.set(id, combine([{ return combine([{
beats: move.beats, beats: move.beats,
startPosition: startAndEndPos, startPosition: startAndEndPos,
endPosition: startAndEndPos, endPosition: startAndEndPos,
@ -316,27 +374,10 @@ function moveAsLowLevelMoves(move: Move, startingPos: Map<DancerIdentity, Semant
amount: move.parameters.circling, amount: move.parameters.circling,
around: startPos.which.leftRightSide(), around: startPos.which.leftRightSide(),
}, },
}])); }]);
} });
return res;
case "do si do": case "do si do":
for (const [id, startPos] of startingPos.entries()) { return handleCirclePairedMove(move.parameters.who, ({ startPos, around }) => {
if (startPos.kind !== PositionKind.Circle) {
throw move.move + " must start in a circle, but " + id + " is at " + startPos;
}
const withId = findPairOpposite(move.parameters.who, id);
if (!withId) {
res.set(id, combine([{
beats: move.beats,
startPosition: { ...startPos, hands: undefined },
endPosition: { ...startPos, hands: undefined },
movementPattern: { kind: SemanticAnimationKind.StandStill },
}]));
continue;
}
const around = findCenterBetween(id, withId);
// TODO Use other parameters? // TODO Use other parameters?
const startAndEndPos = { const startAndEndPos = {
...startPos, ...startPos,
@ -347,7 +388,7 @@ function moveAsLowLevelMoves(move: Move, startingPos: Map<DancerIdentity, Semant
? (startPos.which.isTop() ? Facing.Down : Facing.Up) ? (startPos.which.isTop() ? Facing.Down : Facing.Up)
: (startPos.which.isLeft() ? Facing.Right : Facing.Left), : (startPos.which.isLeft() ? Facing.Right : Facing.Left),
}; };
res.set(id, combine([{ return combine([{
beats: move.beats, beats: move.beats,
startPosition: startAndEndPos, startPosition: startAndEndPos,
endPosition: startAndEndPos, endPosition: startAndEndPos,
@ -356,112 +397,66 @@ function moveAsLowLevelMoves(move: Move, startingPos: Map<DancerIdentity, Semant
amount: move.parameters.circling, amount: move.parameters.circling,
around, around,
}, },
}])); }]);
} });
return res;
case "swing": case "swing":
for (const [id, startPos] of startingPos.entries()) { return handleCirclePairedMove(move.parameters.who, ({ id, startPos, around, withId }) => {
if (excludedByWho(move.parameters.who, id)) { // TODO swing can start from non-circle positions.
res.set(id, combine([{
beats: move.beats,
startPosition: { ...startPos, hands: undefined },
endPosition: { ...startPos, hands: undefined },
movementPattern: { kind: SemanticAnimationKind.StandStill },
}]));
continue;
}
// TODO swing can start from other positions.
// TODO swing end is only in notes / looking at next move. // TODO swing end is only in notes / looking at next move.
if (startPos.kind !== PositionKind.Circle) {
throw move.move + " must start in a circle, but " + id + " is at " + startPos;
}
switch (move.parameters.who) {
case "ladles":
case "gentlespoons":
case "ones":
case "twos":
throw "Swing in middle is currently unsupported.";
case "first corners":
case "second corners":
throw "contra corners currently unsupported.";
case "same roles":
throw "same role swing currently unsupported."
case "neighbors":
case "partners":
case "shadows":
// TODO Make sure referenced dancer is using same center.
break;
}
const withId = findPairOpposite(move.parameters.who, id);
if (!withId) {
res.set(id, combine([{
beats: move.beats,
startPosition: { ...startPos, hands: undefined },
endPosition: { ...startPos, hands: undefined },
movementPattern: { kind: SemanticAnimationKind.StandStill },
}]));
continue;
}
const withPos = getPosFor(withId);
const setDifference = withPos.setOffset - (startPos.setOffset ?? 0);
let startPosAdjusted = startPos;
if (setDifference !== 0) {
if (setDifference === 1 || setDifference === -1) {
startPosAdjusted = {...startPos,
setOffset: (startPos.setOffset ?? 0) + setDifference / 2,
which: startPos.which.swapUpAndDown(),
}
} else {
throw "Not near dancer to swing with.";
}
}
const startWhich = startPosAdjusted.which;
// TODO Can swing be across the set (top or bottom)?
const around = withPos.kind === PositionKind.Circle
? (withPos.which.leftRightSide() === startWhich.leftRightSide()
? startWhich.leftRightSide()
: "Center")
: "Center";
const startWhich = startPos.which;
const startPosition: SemanticPosition = { const startPosition: SemanticPosition = {
...startPosAdjusted, ...startPos,
facing: startWhich.topBottomSide() === CircleSide.Bottom ? Facing.Up : Facing.Down, facing: startWhich.topBottomSide() === CircleSide.Bottom ? Facing.Up : Facing.Down,
}; };
const swingRole = id.danceRole != withId.setIdentity.danceRole
? id.danceRole
// Make some arbitrary choice for same-role swings
: id.coupleRole !== withId.setIdentity.coupleRole
? (id.coupleRole === CoupleRole.Ones ? DanceRole.Lark : DanceRole.Robin)
: withId.relativeSet !== 0
? (withId.relativeSet > 0 ? DanceRole.Lark : DanceRole.Robin)
: withId.relativeLine !== 0
? (withId.relativeLine > 0 ? DanceRole.Lark : DanceRole.Robin)
: /* should be unreachable as this means withId is equal to id */ DanceRole.Lark;
// TODO This assumes swing around right/left, not center or top/bottom.
const endPosition: SemanticPosition = { const endPosition: SemanticPosition = {
...startPosAdjusted, ...startPos,
which: startWhich.isOnLeftLookingAcross() === (id.danceRole === DanceRole.Lark) which: startWhich.isOnLeftLookingAcross() === (swingRole === DanceRole.Lark)
? startWhich ? startWhich
: startWhich.swapUpAndDown(), : startWhich.swapUpAndDown(),
facing: startWhich.leftRightSide() === CircleSide.Left ? Facing.Right : Facing.Left, facing: startWhich.leftRightSide() === CircleSide.Left ? Facing.Right : Facing.Left,
balance: undefined, balance: undefined,
dancerDistance: undefined, dancerDistance: undefined,
hands: new Map<Hand, HandConnection>([id.danceRole === DanceRole.Lark hands: new Map<Hand, HandConnection>([swingRole === DanceRole.Lark
? [Hand.Right, { to: HandTo.DancerRight, hand: Hand.Left }] ? [Hand.Right, { to: HandTo.DancerRight, hand: Hand.Left }]
: [Hand.Left, { to: HandTo.DancerLeft, hand: Hand.Right }]]), : [Hand.Left, { to: HandTo.DancerLeft, hand: Hand.Right }]]),
}; };
// TODO same role swing currently unsupported.
const swingRole = id.danceRole === DanceRole.Lark ? DancerDistance.SwingLark : DancerDistance.SwingRobin; const swingBeats = move.parameters.prefix === "none" ? move.beats
: move.parameters.prefix === "balance"
? move.beats > 8 ? 8 : move.beats - 4
: move.parameters.prefix === "meltdown"
? move.beats - 4
: (() => { throw "Unknown swing prefix: " + move.parameters.prefix })();
const swing: PartialLowLevelMove = {
beats: swingBeats,
endPosition: endPosition,
movementPattern: {
kind: SemanticAnimationKind.Swing,
minAmount: 360,
around,
endFacing: startWhich.leftRightSide() === CircleSide.Left ? Facing.Right : Facing.Left,
swingRole,
},
};
switch (move.parameters.prefix) { switch (move.parameters.prefix) {
case "none": case "none":
res.set(id, combine([ return combine([swing,], startPosition);
{
beats: move.beats,
endPosition: endPosition,
movementPattern: {
kind: SemanticAnimationKind.Swing,
minAmount: 360,
around,
endFacing: startWhich.leftRightSide() === CircleSide.Left ? Facing.Right : Facing.Left,
swingRole: id.danceRole,
},
},
], startPosition));
break;
case "balance": case "balance":
// TODO Right length for balance? // TODO Right length for balance?
const balancePartBeats = move.beats > 8 ? (move.beats - 8) / 2 : 2; const balancePartBeats = move.beats > 8 ? (move.beats - 8) / 2 : 2;
@ -472,7 +467,7 @@ function moveAsLowLevelMoves(move: Move, startingPos: Map<DancerIdentity, Semant
[Hand.Right, { to: HandTo.DancerForward, hand: Hand.Left }], [Hand.Right, { to: HandTo.DancerForward, hand: Hand.Left }],
]), ]),
}; };
res.set(id, combine([ return combine([
{ {
beats: balancePartBeats, beats: balancePartBeats,
startPosition: startWithBalHands, startPosition: startWithBalHands,
@ -491,22 +486,11 @@ function moveAsLowLevelMoves(move: Move, startingPos: Map<DancerIdentity, Semant
}, },
movementPattern: { kind: SemanticAnimationKind.Linear, }, movementPattern: { kind: SemanticAnimationKind.Linear, },
}), }),
{ swing,
beats: move.beats - 2 * balancePartBeats, ], startPosition);
endPosition: endPosition,
movementPattern: {
kind: SemanticAnimationKind.Swing,
minAmount: 360,
around,
endFacing: startWhich.leftRightSide() === CircleSide.Left ? Facing.Right : Facing.Left,
swingRole: id.danceRole,
},
},
], startPosition));
break;
case "meltdown": case "meltdown":
const meltdownBeats = 4; // TODO right number here? const meltdownBeats = 4; // TODO right number here?
res.set(id, combine([ return combine([
prevEnd => ({ prevEnd => ({
beats: meltdownBeats, beats: meltdownBeats,
endPosition: prevEnd, endPosition: prevEnd,
@ -517,52 +501,19 @@ function moveAsLowLevelMoves(move: Move, startingPos: Map<DancerIdentity, Semant
byHand: undefined, byHand: undefined,
}, },
}), }),
{ swing,
beats: move.beats - meltdownBeats, ], startPosition);
endPosition: endPosition,
movementPattern: {
kind: SemanticAnimationKind.Swing,
minAmount: 360,
around,
endFacing: startWhich.leftRightSide() === CircleSide.Left ? Facing.Right : Facing.Left,
swingRole: id.danceRole,
},
},
], startPosition));
break;
} }
} });
return res;
case "allemande": case "allemande":
case "gyre": case "gyre":
for (const [id, startPos] of startingPos.entries()) { return handlePairedMove(move.parameters.who, ({ startPos, around, withId }) => {
if (excludedByWho(move.parameters.who, id)) {
res.set(id, combine([{
beats: move.beats,
startPosition: { ...startPos, hands: undefined },
endPosition: { ...startPos, hands: undefined },
movementPattern: { kind: SemanticAnimationKind.StandStill },
}]));
continue;
}
// TODO allemande can start from other positions.
if (startPos.kind !== PositionKind.Circle) {
throw move.move + " must start in a circle, but " + id + " is at " + startPos;
}
const withId = findPairOpposite(move.parameters.who, id);
// findPairOpposite of null means this dancer doesn't participate in this move.
if (!withId) {
throw "fairPairOpposite and excludedByWho disagree on who does not act: who=" + move.parameters.who + ", id=" + id;
}
const around = findCenterBetween(id, withId);
// TODO Not sure if this is right. // TODO Not sure if this is right.
const byHandOrShoulder = (move.move === "allemande" ? move.parameters.hand : move.parameters.shoulder) ? Hand.Right : Hand.Left; const byHandOrShoulder = (move.move === "allemande" ? move.parameters.hand : move.parameters.shoulder) ? Hand.Right : Hand.Left;
const swap = move.parameters.circling % 360 === 180; const swap = move.parameters.circling % 360 === 180;
if (!swap && move.parameters.circling % 360 !== 0) { if (!swap && move.parameters.circling % 360 !== 0) {
// TODO Support allemande that's not a swap or no-op.
throw "Unsupported allemande circle amount: " + move.parameters.circling; throw "Unsupported allemande circle amount: " + move.parameters.circling;
} }
let endPosition : SemanticPosition = startPos; let endPosition : SemanticPosition = startPos;
@ -572,7 +523,7 @@ function moveAsLowLevelMoves(move: Move, startingPos: Map<DancerIdentity, Semant
endPosition.lineOffset = (endPosition.lineOffset ?? 0) + withId.relativeLine; endPosition.lineOffset = (endPosition.lineOffset ?? 0) + withId.relativeLine;
} }
res.set(id, combine([ return combine([
{ {
beats: move.beats, beats: move.beats,
endPosition, endPosition,
@ -583,20 +534,14 @@ function moveAsLowLevelMoves(move: Move, startingPos: Map<DancerIdentity, Semant
byHand: move.move === "allemande" ? byHandOrShoulder : undefined, byHand: move.move === "allemande" ? byHandOrShoulder : undefined,
}, },
}, },
], startPos)); ], startPos);
} });
return res;
case "circle": case "circle":
const places = move.parameters.places/90 * (move.parameters.turn ? 1 : -1); return handleCircleMove(({ startPos }) => {
const places = move.parameters.places / 90 * (move.parameters.turn ? 1 : -1);
for (const [id, startPos] of startingPos.entries()) { return combine([
if (startPos.kind !== PositionKind.Circle) {
throw move.move + " must start in a circle, but " + id + " is at " + startPos;
}
res.set(id, combine([
{ {
beats: move.beats, beats: move.beats,
endPosition: { endPosition: {
@ -610,32 +555,16 @@ function moveAsLowLevelMoves(move: Move, startingPos: Map<DancerIdentity, Semant
places, places,
} }
}, },
], { ...startPos, facing: Facing.CenterOfCircle })); ], { ...startPos, facing: Facing.CenterOfCircle });
} });
return res;
case "California twirl": case "California twirl":
for (const [id, startPos] of startingPos.entries()) { return handleCirclePairedMove(move.parameters.who, ({ startPos }) => {
if (excludedByWho(move.parameters.who, id)) {
res.set(id, combine([{
beats: move.beats,
startPosition: { ...startPos, hands: undefined },
endPosition: { ...startPos, hands: undefined },
movementPattern: { kind: SemanticAnimationKind.StandStill },
}]));
continue;
}
if (startPos.kind !== PositionKind.Circle) {
throw move.move + " must start in a circle, but " + id + " is at " + startPos;
}
// TODO does "who" matter here or is it entirely positional? At least need to know who to omit. // TODO does "who" matter here or is it entirely positional? At least need to know who to omit.
const onLeft : boolean = startPos.which.isOnLeftLookingUpAndDown(); const onLeft : boolean = startPos.which.isOnLeftLookingUpAndDown();
// TODO get rid of this 1 beat set up and make it part of TwirlSwap? // TODO get rid of this 1 beat set up and make it part of TwirlSwap?
res.set(id, combine([ return combine([
{ {
beats: 1, beats: 1,
endPosition: { endPosition: {
@ -661,23 +590,11 @@ function moveAsLowLevelMoves(move: Move, startingPos: Map<DancerIdentity, Semant
around: startPos.which.topBottomSide(), around: startPos.which.topBottomSide(),
hand: onLeft ? Hand.Right : Hand.Left, hand: onLeft ? Hand.Right : Hand.Left,
} }
}], startPos)); }], startPos);
} });
return res;
case "box the gnat": case "box the gnat":
for (const [id, startPos] of startingPos.entries()) { return handlePairedMove(move.parameters.who, ({ startPos, around, withPos }) => {
if (excludedByWho(move.parameters.who, id)) {
res.set(id, combine([{
beats: move.beats,
startPosition: { ...startPos, hands: undefined },
endPosition: { ...startPos, hands: undefined },
movementPattern: { kind: SemanticAnimationKind.StandStill },
}]));
continue;
}
const hand = move.parameters.hand ? Hand.Right : Hand.Left; const hand = move.parameters.hand ? Hand.Right : Hand.Left;
const balanceBeats = move.parameters.bal const balanceBeats = move.parameters.bal
? move.beats > 4 ? move.beats > 4
@ -690,18 +607,22 @@ function moveAsLowLevelMoves(move: Move, startingPos: Map<DancerIdentity, Semant
// TODO Adjust facing? // TODO Adjust facing?
const startPosition = { ...startPos, hands: new Map<Hand, HandConnection>([[hand, { hand, to: HandTo.DancerForward }]]) }; const startPosition = { ...startPos, hands: new Map<Hand, HandConnection>([[hand, { hand, to: HandTo.DancerForward }]]) };
const withId = findPairOpposite(move.parameters.who, id);
// findPairOpposite of null means this dancer doesn't participate in this move.
if (!withId) {
throw "fairPairOpposite and excludedByWho disagree on who does not act: who=" + move.parameters.who + ", id=" + id;
}
const around = findCenterBetween(id, withId);
if (around === "Center") { if (around === "Center") {
throw "TwirlSwap around center is unsupported."; throw "TwirlSwap around center is unsupported.";
} }
const twirl: PartialLowLevelMove = {
beats: twirlBeats,
endPosition: withPos,
movementPattern: {
kind: SemanticAnimationKind.TwirlSwap,
around,
hand,
}
};
if (move.parameters.bal) { if (move.parameters.bal) {
res.set(id, combine([ return combine([
{ {
beats: balancePartBeats, beats: balancePartBeats,
endPosition: { endPosition: {
@ -722,43 +643,14 @@ function moveAsLowLevelMoves(move: Move, startingPos: Map<DancerIdentity, Semant
kind: SemanticAnimationKind.Linear, kind: SemanticAnimationKind.Linear,
} }
}, },
{ twirl], startPosition);
beats: twirlBeats,
endPosition: getPosFor(withId),
movementPattern: {
kind: SemanticAnimationKind.TwirlSwap,
around,
hand,
}
}], startPosition));
} else { } else {
res.set(id, combine([ return combine([twirl], startPosition);
{
beats: twirlBeats,
endPosition: getPosFor(withId),
movementPattern: {
kind: SemanticAnimationKind.TwirlSwap,
around,
hand,
}
}], startPosition));
} }
} });
return res;
case "pull by dancers": case "pull by dancers":
for (const [id, startPos] of startingPos.entries()) { return handlePairedMove(move.parameters.who, ({ startPos, around, withPos }) => {
if (excludedByWho(move.parameters.who, id)) {
res.set(id, combine([{
beats: move.beats,
startPosition: { ...startPos, hands: undefined },
endPosition: { ...startPos, hands: undefined },
movementPattern: { kind: SemanticAnimationKind.StandStill },
}]));
continue;
}
const hand = move.parameters.hand ? Hand.Right : Hand.Left; const hand = move.parameters.hand ? Hand.Right : Hand.Left;
const balanceBeats = move.parameters.bal const balanceBeats = move.parameters.bal
? move.beats > 4 ? move.beats > 4
@ -768,13 +660,6 @@ function moveAsLowLevelMoves(move: Move, startingPos: Map<DancerIdentity, Semant
const balancePartBeats = balanceBeats / 2; const balancePartBeats = balanceBeats / 2;
const pullBeats = move.beats - balanceBeats; const pullBeats = move.beats - balanceBeats;
const withId = findPairOpposite(move.parameters.who, id);
// findPairOpposite of null means this dancer doesn't participate in this move.
if (!withId) {
throw "fairPairOpposite and excludedByWho disagree on who does not act: who=" + move.parameters.who + ", id=" + id;
}
const around = findCenterBetween(id, withId);
// TODO Adjust facing? // TODO Adjust facing?
const startPosition = { const startPosition = {
...startPos, ...startPos,
@ -785,9 +670,19 @@ function moveAsLowLevelMoves(move: Move, startingPos: Map<DancerIdentity, Semant
]]) ]])
}; };
const passBy: PartialLowLevelMove = {
beats: pullBeats,
endPosition: { ...withPos, facing: startPos.facing },
movementPattern: {
kind: SemanticAnimationKind.PassBy,
around,
side: hand,
withHands: true,
}
};
if (move.parameters.bal) { if (move.parameters.bal) {
res.set(id, combine([ return combine([
{ {
beats: balancePartBeats, beats: balancePartBeats,
endPosition: { endPosition: {
@ -808,43 +703,18 @@ function moveAsLowLevelMoves(move: Move, startingPos: Map<DancerIdentity, Semant
kind: SemanticAnimationKind.Linear, kind: SemanticAnimationKind.Linear,
} }
}, },
{ passBy], startPosition);
beats: pullBeats,
endPosition: getPosFor(withId),
movementPattern: {
kind: SemanticAnimationKind.PassBy,
around,
side: hand,
withHands: true,
}
}], startPosition));
} else { } else {
res.set(id, combine([ return combine([passBy], startPosition);
{
beats: pullBeats,
endPosition: getPosFor(withId),
movementPattern: {
kind: SemanticAnimationKind.PassBy,
around,
side: hand,
withHands: true,
}
}], startPosition));
} }
} });
return res;
case "chain": case "chain":
for (const [id, startPos] of startingPos.entries()) { const mainRole = move.parameters.who === "gentlespoons" ? DanceRole.Lark : DanceRole.Robin;
if (startPos.kind !== PositionKind.Circle) { const pullBeats = move.beats / 2;
throw move.move + " must start in a circle, but " + id + " is at " + startPos; const turnBeats = move.beats - pullBeats;
}
const mainRole = move.parameters.who === "gentlespoons" ? DanceRole.Lark : DanceRole.Robin;
const pullBeats = move.beats / 2;
const turnBeats = move.beats - pullBeats;
return handleCircleMove(({ id, startPos }) => {
if (id.danceRole === mainRole) { if (id.danceRole === mainRole) {
const hand = move.parameters.hand ? Hand.Right : Hand.Left; const hand = move.parameters.hand ? Hand.Right : Hand.Left;
const endWhich = startPos.which.swapDiagonal(); const endWhich = startPos.which.swapDiagonal();
@ -869,7 +739,7 @@ function moveAsLowLevelMoves(move: Move, startingPos: Map<DancerIdentity, Semant
const turnTo = hand === Hand.Right ? HandTo.DancerRight : HandTo.DancerLeft; const turnTo = hand === Hand.Right ? HandTo.DancerRight : HandTo.DancerLeft;
res.set(id, combine([ return combine([
{ {
beats: pullBeats, beats: pullBeats,
endPosition: { endPosition: {
@ -902,9 +772,9 @@ function moveAsLowLevelMoves(move: Move, startingPos: Map<DancerIdentity, Semant
kind: SemanticAnimationKind.CourtesyTurn, kind: SemanticAnimationKind.CourtesyTurn,
} }
}) })
], startPosition)); ], startPosition);
} else { } else {
res.set(id, combine([ return combine([
{ {
beats: pullBeats, beats: pullBeats,
endPosition: startPos, endPosition: startPos,
@ -923,18 +793,12 @@ function moveAsLowLevelMoves(move: Move, startingPos: Map<DancerIdentity, Semant
kind: SemanticAnimationKind.CourtesyTurn, kind: SemanticAnimationKind.CourtesyTurn,
} }
} }
], startPos)); ], startPos);
} }
} });
return res;
case "long lines": case "long lines":
for (const [id, startPos] of startingPos.entries()) { return handleCircleMove(({ startPos }) => {
if (startPos.kind !== PositionKind.Circle) {
throw move.move + " must start in a circle, but " + id + " is at " + startPos;
}
const startPosition: SemanticPosition = { const startPosition: SemanticPosition = {
...startPos, ...startPos,
longLines: undefined, longLines: undefined,
@ -949,7 +813,7 @@ function moveAsLowLevelMoves(move: Move, startingPos: Map<DancerIdentity, Semant
if (move.parameters.go) { if (move.parameters.go) {
const forwardBeats = move.beats / 2; const forwardBeats = move.beats / 2;
const backwardBeats = move.beats - forwardBeats; const backwardBeats = move.beats - forwardBeats;
res.set(id, combine([ return combine([
{ {
beats: forwardBeats, beats: forwardBeats,
endPosition: { ...startPosition, longLines: LongLines.Forward }, endPosition: { ...startPosition, longLines: LongLines.Forward },
@ -960,24 +824,19 @@ function moveAsLowLevelMoves(move: Move, startingPos: Map<DancerIdentity, Semant
endPosition: startPosition, endPosition: startPosition,
movementPattern: { kind: SemanticAnimationKind.Linear }, movementPattern: { kind: SemanticAnimationKind.Linear },
}, },
], startPosition)); ], startPosition);
} else { } else {
res.set(id, combine([{ return combine([{
beats: move.beats, beats: move.beats,
endPosition: { ...startPosition, longLines: LongLines.Forward }, endPosition: { ...startPosition, longLines: LongLines.Forward },
movementPattern: { kind: SemanticAnimationKind.Linear }, movementPattern: { kind: SemanticAnimationKind.Linear },
}], startPosition)); }], startPosition);
} }
} });
return res;
case "roll away": case "roll away":
for (const [id, startPos] of startingPos.entries()) { // TODO maybe can roll away in short lines?
if (startPos.kind !== PositionKind.Circle) { return handleCirclePairedMove(move.parameters.who, ({ id, startPos, withPos }) => {
throw move.move + " must start in a circle, but " + id + " is at " + startPos;
}
let isRoller: boolean; let isRoller: boolean;
switch (move.parameters.who) { switch (move.parameters.who) {
case "gentlespoons": case "gentlespoons":
@ -997,15 +856,8 @@ function moveAsLowLevelMoves(move: Move, startingPos: Map<DancerIdentity, Semant
throw "Roll away in contra corners is unsupported."; throw "Roll away in contra corners is unsupported.";
} }
let oppositeId = findPairOpposite(move.parameters.whom, id);
if (!oppositeId) {
// TODO
throw "Failed to identify dancer to roll away with.";
}
// TODO This isn't quite right if there's no 1/2 sash? // TODO This isn't quite right if there's no 1/2 sash?
const oppositePos = getPosFor(oppositeId); let swapPos = withPos;
let swapPos = oppositePos;
if (swapPos.kind === PositionKind.Circle && swapPos.longLines) { if (swapPos.kind === PositionKind.Circle && swapPos.longLines) {
swapPos = { ...swapPos, longLines: undefined }; swapPos = { ...swapPos, longLines: undefined };
} }
@ -1014,37 +866,35 @@ function moveAsLowLevelMoves(move: Move, startingPos: Map<DancerIdentity, Semant
if (isRoller) { if (isRoller) {
if (move.parameters["½sash"]) { if (move.parameters["½sash"]) {
// swap positions by sliding // swap positions by sliding
res.set(id, combine([{ return combine([{
beats: move.beats, beats: move.beats,
endPosition: swapPos, endPosition: swapPos,
movementPattern: { kind: SemanticAnimationKind.Linear }, movementPattern: { kind: SemanticAnimationKind.Linear },
}], startPos)); }], startPos);
} else { } else {
// just stand still // just stand still
res.set(id, combine([{ return combine([{
beats: move.beats, beats: move.beats,
endPosition: { ...startPos, longLines: undefined }, endPosition: { ...startPos, longLines: undefined },
movementPattern: { kind: SemanticAnimationKind.Linear }, movementPattern: { kind: SemanticAnimationKind.Linear },
}], startPos)); }], startPos);
} }
} else { } else {
// being rolled away, so do a spin // being rolled away, so do a spin
res.set(id, combine([{ return combine([{
beats: move.beats, beats: move.beats,
// TODO Is this the right end position logic? // TODO Is this the right end position logic?
endPosition: move.parameters["½sash"] ? swapPos : { ...startPos, which: startPos.which.swapDiagonal() }, endPosition: move.parameters["½sash"] ? swapPos : { ...startPos, which: startPos.which.swapDiagonal() },
movementPattern: { kind: SemanticAnimationKind.RollAway }, movementPattern: { kind: SemanticAnimationKind.RollAway },
}], startPos)); }], startPos);
} }
} });
return res;
} }
// XXX DEBUG Just leave out unsupported moves for now to allow viewing the known moves. // XXX DEBUG Just leave out unsupported moves for now to allow viewing the known moves.
//throw "Unknown move: " + move.move + ": " + JSON.stringify(move); //throw "Unknown move: " + move.move + ": " + JSON.stringify(move);
for (const [id, startPos] of startingPos.entries()) { return handleMove(({ startPos }) => {
res.set(id, [{ return [{
interpreterError: "UNKNOWN MOVE '" + move.move + "': standing still", interpreterError: "UNKNOWN MOVE '" + move.move + "': standing still",
move, move,
startBeat: 0, startBeat: 0,
@ -1054,9 +904,8 @@ function moveAsLowLevelMoves(move: Move, startingPos: Map<DancerIdentity, Semant
movementPattern: { movementPattern: {
kind: SemanticAnimationKind.StandStill, kind: SemanticAnimationKind.StandStill,
}, },
}]); }];
} });
return res;
} }
function danceAsLowLevelMoves(moves: Move[], startingPos: Map<DancerIdentity, SemanticPosition>): Map<DancerIdentity, LowLevelMove[]> { function danceAsLowLevelMoves(moves: Move[], startingPos: Map<DancerIdentity, SemanticPosition>): Map<DancerIdentity, LowLevelMove[]> {