contra-renderer/www/js/interpreterCommon.ts

681 lines
23 KiB
TypeScript

import { DanceRole, DancerIdentity, Rotation } from "./danceCommon.js";
import { Hand } from "./rendererConstants.js";
export enum PositionKind {
Circle = "Circle",
ShortLines = "ShortLines",
};
// Which of the four corners of a circle.
// Note that "Left" is the left side of the set, not left from the dancer's PoV,
// so, in improper, the Robin starts in the BottomLeft.
enum CirclePositionEnum {
TopLeft = "TopLeft",
BottomLeft = "BottomLeft",
BottomRight = "BottomRight",
TopRight = "TopRight",
}
export enum CircleSide {
Top = "Top",
Bottom = "Bottom",
Left = "Left",
Right = "Right",
}
export type CircleSideOrCenter = CircleSide | "Center";
export function isLeftRightCircleSide(side: CircleSideOrCenter): side is CircleSide.Left | CircleSide.Right {
return side === CircleSide.Left || side === CircleSide.Right;
}
export function isTopBottomCircleSide(side: CircleSideOrCenter): side is CircleSide.Top | CircleSide.Bottom {
return side === CircleSide.Top || side === CircleSide.Bottom;
}
export class CirclePosition {
public static readonly TopLeft = new CirclePosition(CirclePositionEnum.TopLeft);
public static readonly BottomLeft = new CirclePosition(CirclePositionEnum.BottomLeft);
public static readonly BottomRight = new CirclePosition(CirclePositionEnum.BottomRight);
public static readonly TopRight = new CirclePosition(CirclePositionEnum.TopRight);
private readonly enumValue: CirclePositionEnum;
private constructor(enumValue: CirclePositionEnum) {
this.enumValue = enumValue;
}
public static fromSides(leftRightSide: CircleSide.Left | CircleSide.Right, topBottomSide: CircleSide.Bottom | CircleSide.Top) {
return leftRightSide === CircleSide.Left
? topBottomSide === CircleSide.Top
? CirclePosition.TopLeft
: CirclePosition.BottomLeft
: topBottomSide === CircleSide.Top
? CirclePosition.TopRight
: CirclePosition.BottomRight;
}
public static fromSwing(side: CircleSide, swingRole: DanceRole, facing: "In" | "Out" | "Across" | Facing.Up | Facing.Down) {
if (isLeftRightCircleSide(side)) {
if (facing === Facing.Up || facing === Facing.Down) {
throw new Error("Ending a swing in a circle on the left/right side facing up/down doesn't make sense.");
} else if (facing === "Across") {
facing = "In";
}
return this.fromSides(side,
(side === CircleSide.Left) !== (swingRole === DanceRole.Lark) !== (facing === "In") ? CircleSide.Bottom : CircleSide.Top);
} else {
if (facing === Facing.Up || facing === Facing.Down) {
facing = (facing === Facing.Up) === (side === CircleSide.Top) ? "Out" : "In";
} else if (facing === "Across") {
throw new Error("Ending a swing in a circle on the top/bottom side facing across doesn't make sense.");
}
return this.fromSides(
(side === CircleSide.Top) !== (swingRole === DanceRole.Lark) !== (facing === "In") ? CircleSide.Left : CircleSide.Right,
side);
}
}
private static enumValueToNumber(enumValue: CirclePositionEnum) : number {
switch (enumValue) {
case CirclePositionEnum.TopLeft:
return 0;
case CirclePositionEnum.BottomLeft:
return 1;
case CirclePositionEnum.BottomRight:
return 2;
case CirclePositionEnum.TopRight:
return 3;
}
}
private static numberToEnumValue(num: number) : CirclePositionEnum {
if (num < 0 || num > 3) {
num %= 4;
if (num < 0) num += 4;
}
return [
CirclePositionEnum.TopLeft,
CirclePositionEnum.BottomLeft,
CirclePositionEnum.BottomRight,
CirclePositionEnum.TopRight,
][num];
}
private static get(enumValue: CirclePositionEnum) : CirclePosition {
return [
CirclePosition.TopLeft,
CirclePosition.BottomLeft,
CirclePosition.BottomRight,
CirclePosition.TopRight,
][this.enumValueToNumber(enumValue)];
}
public circleRight(places?: number) : CirclePosition {
return CirclePosition.get(CirclePosition.numberToEnumValue(CirclePosition.enumValueToNumber(this.enumValue) - (places ?? 1)));
}
public circleLeft(places?: number) : CirclePosition {
return CirclePosition.get(CirclePosition.numberToEnumValue(CirclePosition.enumValueToNumber(this.enumValue) + (places ?? 1)));
}
public swapAcross() : CirclePosition {
return [
CirclePosition.TopRight,
CirclePosition.BottomRight,
CirclePosition.BottomLeft,
CirclePosition.TopLeft,
][CirclePosition.enumValueToNumber(this.enumValue)];
}
public swapUpAndDown() : CirclePosition {
return [
CirclePosition.BottomLeft,
CirclePosition.TopLeft,
CirclePosition.TopRight,
CirclePosition.BottomRight,
][CirclePosition.enumValueToNumber(this.enumValue)];
}
public swapOnSide(side: CircleSide) : CirclePosition {
if (side === CircleSide.Bottom || side === CircleSide.Top) {
return this.swapAcross();
} else {
return this.swapUpAndDown();
}
}
public toShortLines(slideTo: Hand) : ShortLinesPosition {
return slideTo === Hand.Left
? [
ShortLinesPosition.FarLeft,
ShortLinesPosition.MiddleLeft,
ShortLinesPosition.FarRight,
ShortLinesPosition.MiddleRight,
][CirclePosition.enumValueToNumber(this.enumValue)]
: [
ShortLinesPosition.MiddleLeft,
ShortLinesPosition.FarLeft,
ShortLinesPosition.MiddleRight,
ShortLinesPosition.FarRight,
][CirclePosition.enumValueToNumber(this.enumValue)];
}
public unfoldToShortLines(center: CircleSide.Bottom | CircleSide.Top) : ShortLinesPosition {
return center === CircleSide.Bottom
? [
ShortLinesPosition.FarLeft,
ShortLinesPosition.MiddleLeft,
ShortLinesPosition.MiddleRight,
ShortLinesPosition.FarRight,
][CirclePosition.enumValueToNumber(this.enumValue)]
: [
ShortLinesPosition.MiddleLeft,
ShortLinesPosition.FarLeft,
ShortLinesPosition.FarRight,
ShortLinesPosition.MiddleRight,
][CirclePosition.enumValueToNumber(this.enumValue)];
}
public swapDiagonal() : CirclePosition {
return this.swapAcross().swapUpAndDown();
}
public facingCenterRotation() : Rotation {
return (45 + 90 * CirclePosition.enumValueToNumber(this.enumValue)) % 360;
}
public topBottomSide() : CircleSide.Top | CircleSide.Bottom {
return this.enumValue === CirclePositionEnum.TopLeft || this.enumValue === CirclePositionEnum.TopRight
? CircleSide.Top
: CircleSide.Bottom;
}
public leftRightSide() : CircleSide.Left | CircleSide.Right {
return this.enumValue === CirclePositionEnum.TopLeft || this.enumValue === CirclePositionEnum.BottomLeft
? CircleSide.Left
: CircleSide.Right;
}
public isTop() : boolean {
return this.topBottomSide() === CircleSide.Top;
}
public isLeft() : boolean {
return this.leftRightSide() === CircleSide.Left;
}
public isOnLeftLookingAcross() : boolean {
return this.enumValue === CirclePositionEnum.TopRight || this.enumValue === CirclePositionEnum.BottomLeft;
}
public isOnLeftLookingUpAndDown() : boolean {
return this.enumValue === CirclePositionEnum.TopLeft || this.enumValue === CirclePositionEnum.BottomRight;
}
public isOnLeft(around: CircleSide, facing: Facing.Down | Facing.Up | Facing.Left | Facing.Right | Facing.CenterOfCircle | "Out") {
if (facing === Facing.CenterOfCircle) {
facing = facingInAround(around);
} else if (facing === "Out") {
facing = oppositeFacing(facingInAround(around));
}
if (around === CircleSide.Bottom || around === CircleSide.Top) {
if (facing === Facing.Left || facing === Facing.Right) {
throw new Error("Cannot face " + facing + " and have a left/right side around " + around);
}
return (around === CircleSide.Bottom) !== (facing === Facing.Down) !== (this.isLeft());
} else {
if (facing === Facing.Up || facing === Facing.Down) {
throw new Error("Cannot face " + facing + " and have a left/right side around " + around);
}
return (around === CircleSide.Left) !== (facing === Facing.Right) !== (this.isTop());
}
}
public facingAcross() : Facing.Left | Facing.Right {
return this.isLeft() ? Facing.Right : Facing.Left;
}
public facingOut() : Facing.Left | Facing.Right {
return this.isLeft() ? Facing.Left : Facing.Right;
}
public facingUpOrDown() : Facing.Up | Facing.Down {
return this.isTop() ? Facing.Down : Facing.Up;
}
public toString() : string {
return this.enumValue.toString();
}
}
enum ShortLinesPositionEnum {
FarLeft = "FarLeft",
MiddleLeft = "MiddleLeft",
MiddleRight = "MiddleRight",
FarRight = "FarRight",
}
export class ShortLinesPosition {
public static readonly FarLeft = new ShortLinesPosition(ShortLinesPositionEnum.FarLeft);
public static readonly MiddleLeft = new ShortLinesPosition(ShortLinesPositionEnum.MiddleLeft);
public static readonly MiddleRight = new ShortLinesPosition(ShortLinesPositionEnum.MiddleRight);
public static readonly FarRight = new ShortLinesPosition(ShortLinesPositionEnum.FarRight);
private readonly enumValue: ShortLinesPositionEnum;
private constructor(enumValue: ShortLinesPositionEnum) {
this.enumValue = enumValue;
}
public static fromSide(side: CircleSide.Left | CircleSide.Right, which: "Far" | "Middle") {
if (side === CircleSide.Left) {
return which === "Far" ? ShortLinesPosition.FarLeft : ShortLinesPosition.MiddleLeft;
} else {
return which === "Far" ? ShortLinesPosition.FarRight : ShortLinesPosition.MiddleRight;
}
}
public static fromSwing(side: "Center" | CircleSide.Left | CircleSide.Right, swingRole: DanceRole, facing: Facing.Up | Facing.Down) {
if (side === "Center") {
return this.fromSide(
(swingRole === DanceRole.Lark) !== (facing === Facing.Down) ? CircleSide.Right : CircleSide.Left,
"Middle");
} else {
return this.fromSide(side,
(side === CircleSide.Left) !== (swingRole === DanceRole.Lark) !== (facing === Facing.Down) ? "Far" : "Middle");
}
}
private static enumValueToNumber(enumValue: ShortLinesPositionEnum) : number {
switch (enumValue) {
case ShortLinesPositionEnum.FarLeft:
return 0;
case ShortLinesPositionEnum.MiddleLeft:
return 1;
case ShortLinesPositionEnum.MiddleRight:
return 2;
case ShortLinesPositionEnum.FarRight:
return 3;
}
}
private static numberToEnumValue(num: number) : ShortLinesPositionEnum {
if (num < 0 || num > 3) {
num %= 4;
if (num < 0) num += 4;
}
return [
ShortLinesPositionEnum.FarLeft,
ShortLinesPositionEnum.MiddleLeft,
ShortLinesPositionEnum.MiddleRight,
ShortLinesPositionEnum.FarRight,
][num];
}
private static get(enumValue: ShortLinesPositionEnum) : ShortLinesPosition {
return [
ShortLinesPosition.FarLeft,
ShortLinesPosition.MiddleLeft,
ShortLinesPosition.MiddleRight,
ShortLinesPosition.FarRight,
][this.enumValueToNumber(enumValue)];
}
public swapOnSide() : ShortLinesPosition {
return [
ShortLinesPosition.MiddleLeft,
ShortLinesPosition.FarLeft,
ShortLinesPosition.FarRight,
ShortLinesPosition.MiddleRight,
][ShortLinesPosition.enumValueToNumber(this.enumValue)];
}
public swapSides() : ShortLinesPosition {
return [
ShortLinesPosition.FarRight,
ShortLinesPosition.MiddleRight,
ShortLinesPosition.MiddleLeft,
ShortLinesPosition.FarLeft,
][ShortLinesPosition.enumValueToNumber(this.enumValue)];
}
public shift(dir: Hand, facing: Facing.Up | Facing.Down): ShortLinesPosition {
const { newPos, wrap } = this.shiftWithWrap(dir, facing);
if (wrap) {
throw new Error("Invalid shift: " + this + " facing " + facing + " to " + dir + ".");
}
return newPos;
}
public shiftWithWrap(dir: Hand, facing: Facing.Up | Facing.Down): { newPos: ShortLinesPosition, wrap?: -1 | 1 } {
const shift = (dir === Hand.Left) === (facing === Facing.Down) ? -1 : +1;
const newNum = ShortLinesPosition.enumValueToNumber(this.enumValue) + shift;
const newPos = ShortLinesPosition.get(ShortLinesPosition.numberToEnumValue(newNum))
if (newNum < 0) {
return { newPos, wrap: -1 };
} else if (newNum > 3) {
return { newPos, wrap: +1 };
} else {
return { newPos };
}
}
public isMiddle() : boolean {
return this.enumValue === ShortLinesPositionEnum.MiddleRight || this.enumValue === ShortLinesPositionEnum.MiddleLeft;
}
public leftRightSide() : CircleSide.Left | CircleSide.Right {
return this.enumValue === ShortLinesPositionEnum.FarLeft || this.enumValue === ShortLinesPositionEnum.MiddleLeft
? CircleSide.Left
: CircleSide.Right;
}
public isLeft() : boolean {
return this.leftRightSide() === CircleSide.Left;
}
// Of the two positions on the same leftRightSide() is this the one further to the left?
public isLeftOfSide() : boolean {
return this.enumValue === ShortLinesPositionEnum.FarLeft || this.enumValue === ShortLinesPositionEnum.MiddleRight;
}
public facingSide() : Facing.Left | Facing.Right {
return this.isLeft() === this.isMiddle() ? Facing.Left : Facing.Right;
}
public facingAcross() : Facing.Left | Facing.Right {
return this.isLeft() ? Facing.Right : Facing.Left;
}
public isToLeftOf(otherPos: ShortLinesPosition): boolean {
return this.enumValue < otherPos.enumValue;
}
public toString() : string {
return this.enumValue.toString();
}
}
export enum Facing {
CenterOfCircle = "CenterOfCircle",
// for star left/right
LeftInCircle = "LeftInCircle",
RightInCircle = "RightInCircle",
Up = "Up",
Down = "Down",
Left = "Left",
Right = "Right",
}
export function oppositeFacing(facing: Facing.Up | Facing.Down) : Facing.Up | Facing.Down;
export function oppositeFacing(facing: Facing.Left | Facing.Right) : Facing.Left | Facing.Right;
export function oppositeFacing(facing: Facing.LeftInCircle | Facing.RightInCircle) : Facing.LeftInCircle | Facing.RightInCircle;
export function oppositeFacing(facing: Facing.Up | Facing.Down | Facing.Left | Facing.Right) : Facing.Up | Facing.Down | Facing.Left | Facing.Right;
export function oppositeFacing(facing: Facing.Up | Facing.Down | Facing.Left | Facing.Right | Facing.LeftInCircle | Facing.RightInCircle) : Facing {
switch (facing) {
case Facing.LeftInCircle:
return Facing.RightInCircle;
case Facing.RightInCircle:
return Facing.LeftInCircle;
case Facing.Up:
return Facing.Down;
case Facing.Down:
return Facing.Up;
case Facing.Left:
return Facing.Right;
case Facing.Right:
return Facing.Left;
}
}
export function isFacingUpOrDown(facing: Facing | "Across"): facing is Facing.Up | Facing.Down {
return facing === Facing.Up || facing === Facing.Down;
}
export function facingInAround(side: CircleSide) : Facing.Down | Facing.Up | Facing.Left | Facing.Right {
switch (side) {
case CircleSide.Bottom:
return Facing.Up;
break;
case CircleSide.Top:
return Facing.Down;
break;
case CircleSide.Left:
return Facing.Right;
break;
case CircleSide.Right:
return Facing.Left;
break;
}
}
export enum StarGrip {
HandsAcross = "HandsAcross",
WristGrip = "WristGrip",
}
export enum HandTo {
LeftInCircle = "LeftInCircle",
RightInCircle = "RightInCircle",
AcrossCircle = "AcrossCircle",
DiagonalAcrossCircle = "DiagonalAcrossCircle",
LeftDiagonalAcrossCircle = "LeftDiagonalAcrossCircle",
RightDiagonalAcrossCircle = "RightDiagonalAcrossCircle",
DancerForward = "DancerForward",
DancerLeft = "DancerLeft",
DancerRight = "DancerRight",
}
export interface HandConnection {
to: HandTo,
hand: Hand,
}
export enum BalanceWeight {
Forward = "Forward",
Backward = "Backward",
Left = "Left",
Right = "Right",
}
export enum DancerDistance {
Normal = "Normal",
Compact = "Compact", // allemande, etc.
// Swings asymmetrical, but some dances may have the Lark do a swing as a Robin or vice versa,
// especially any dance with Larks Swing or Robins Swing.
SwingLark = "SwingLark",
SwingRobin = "SwingRobin",
}
export enum LongLines {
// Walked forward into center.
Forward = "Forward",
// Only a little offset (has walked almost all the way from the other side after a give and take).
Near = "Near",
// Actually in center. May be slightly offset for wavy lines.
Center = "Center",
}
export type SemanticPosition = {
kind: PositionKind.Circle,
setOffset?: number,
lineOffset?: number,
which: CirclePosition,
facing: Facing,
hands?: Map<Hand, HandConnection>,
balance?: BalanceWeight,
dancerDistance?: DancerDistance,
longLines?: LongLines,
} | {
kind: PositionKind.ShortLines,
setOffset?: number,
lineOffset?: number,
which: ShortLinesPosition,
facing: Facing,
hands?: Map<Hand, HandConnection>,
balance?: BalanceWeight,
dancerDistance?: DancerDistance,
longLines?: undefined,
};
export const handsInCircle = new Map<Hand, HandConnection>([
[Hand.Left, {
to: HandTo.LeftInCircle,
hand: Hand.Right,
}],
[Hand.Right, {
to: HandTo.RightInCircle,
hand: Hand.Left,
}],
]);
export const handsFourImproper: Map<DancerIdentity, SemanticPosition & { kind: PositionKind.Circle }> = new Map<DancerIdentity, SemanticPosition & { kind: PositionKind.Circle }>([
[DancerIdentity.OnesLark, {
kind: PositionKind.Circle,
which: CirclePosition.TopLeft,
facing: Facing.CenterOfCircle,
hands: handsInCircle,
}],
[DancerIdentity.OnesRobin, {
kind: PositionKind.Circle,
which: CirclePosition.TopRight,
facing: Facing.CenterOfCircle,
hands: handsInCircle,
}],
[DancerIdentity.TwosLark, {
kind: PositionKind.Circle,
which: CirclePosition.BottomRight,
facing: Facing.CenterOfCircle,
hands: handsInCircle,
}],
[DancerIdentity.TwosRobin, {
kind: PositionKind.Circle,
which: CirclePosition.BottomLeft,
facing: Facing.CenterOfCircle,
hands: handsInCircle,
}],
]);
export function handsInLongLines(wavy: boolean) {
return new Map<Hand, HandConnection>([
[Hand.Left, { hand: wavy ? Hand.Right : Hand.Left, to: HandTo.DancerLeft }],
[Hand.Right, { hand: wavy ? Hand.Left : Hand.Right, to: HandTo.DancerRight }],
]);
}
export function handsInShortLine({ which, facing, wavy }: { which: ShortLinesPosition; facing: Facing.Up | Facing.Down; wavy: boolean; }): Map<Hand, HandConnection> {
return which.isMiddle()
? handsInLongLines(wavy)
: new Map<Hand, HandConnection>([
which.isLeft() === (facing === Facing.Up)
? [Hand.Left, { hand: wavy ? Hand.Right : Hand.Left, to: HandTo.DancerLeft }]
: [Hand.Right, { hand: wavy ? Hand.Left : Hand.Right, to: HandTo.DancerRight }]
]);
}
export function handsInLine(args: { wavy: boolean, which: ShortLinesPosition | CirclePosition, facing?: Facing }) {
if (args.which instanceof ShortLinesPosition && (args.facing === Facing.Up || args.facing === Facing.Down)) {
return handsInShortLine({ wavy: args.wavy, which: args.which, facing: args.facing });
} else {
return handsInLongLines(args.wavy);
}
}
export function handToDancerToSideInCircleFacingAcross(which: CirclePosition): Map<Hand, HandConnection> {
return new Map<Hand, HandConnection>([
which.isOnLeftLookingAcross()
? [Hand.Right, { hand: Hand.Left, to: HandTo.DancerRight }]
: [Hand.Left, { hand: Hand.Right, to: HandTo.DancerLeft }]
]);
}
export function handToDancerToSideInCircleFacingUpOrDown(which: CirclePosition): Map<Hand, HandConnection> {
return new Map<Hand, HandConnection>([
which.isOnLeftLookingUpAndDown()
? [Hand.Right, { hand: Hand.Left, to: HandTo.DancerRight }]
: [Hand.Left, { hand: Hand.Right, to: HandTo.DancerLeft }]
]);
}
export function facingAdjacent(pos: SemanticPosition, otherPos: SemanticPosition): Facing | undefined {
if (pos.kind !== otherPos.kind) {
return undefined;
}
if (pos.kind === PositionKind.ShortLines) {
if (otherPos.kind !== PositionKind.ShortLines) {
return undefined;
}
if ((pos.setOffset ?? 0) !== (otherPos.setOffset ?? 0)) {
return undefined;
}
if ((pos.lineOffset ?? 0) !== (otherPos.lineOffset ?? 0)) {
if (pos.which === ShortLinesPosition.FarLeft && otherPos.which === ShortLinesPosition.FarRight
&& ((pos.lineOffset ?? 0) - 1) === (otherPos.lineOffset ?? 0)) {
return Facing.Left;
} else if (pos.which === ShortLinesPosition.FarRight && otherPos.which === ShortLinesPosition.FarLeft
&& ((pos.lineOffset ?? 0) + 1) === (otherPos.lineOffset ?? 0)) {
return Facing.Right;
} else {
return undefined;
}
}
if (otherPos.which.isToLeftOf(pos.which)) {
return Facing.Left;
} else {
return Facing.Right;
}
} else if (pos.kind === PositionKind.Circle) {
if (otherPos.kind !== PositionKind.Circle) {
return undefined;
}
if ((pos.lineOffset ?? 0) !== (otherPos.lineOffset ?? 0)) {
// TODO
return undefined;
}
if ((pos.setOffset ?? 0) !== (otherPos.setOffset ?? 0)) {
// TODO
return undefined;
}
if (pos.which.leftRightSide() === otherPos.which.leftRightSide()) {
if (pos.which.topBottomSide() === otherPos.which.topBottomSide()) {
return undefined;
}
if ((pos.lineOffset ?? 0) !== (otherPos.lineOffset ?? 0)) {
return undefined;
}
if ((pos.setOffset ?? 0) === (otherPos.setOffset ?? 0)) {
return pos.which.facingUpOrDown();
} else if (((pos.setOffset ?? 0) + 1) === (otherPos.setOffset ?? 0) && pos.which.isTop()) {
return Facing.Up;
} else if (((pos.setOffset ?? 0) - 1) === (otherPos.setOffset ?? 0) && !pos.which.isTop()) {
return Facing.Down;
}
} else if (pos.which.topBottomSide() === otherPos.which.topBottomSide()) {
if ((pos.setOffset ?? 0) === (otherPos.setOffset ?? 0)) {
return undefined;
}
if ((pos.lineOffset ?? 0) !== (otherPos.lineOffset ?? 0)) {
return pos.which.facingAcross();
} else if (((pos.lineOffset ?? 0) + 1) === (otherPos.lineOffset ?? 0) && pos.which.isLeft()) {
return Facing.Left;
} else if (((pos.lineOffset ?? 0) - 1) === (otherPos.lineOffset ?? 0) && !pos.which.isLeft()) {
return Facing.Right;
}
} else {
// Opposite corners of circle.
return undefined;
}
} else {
throw new Error("Unexpected PositionKind: " + otherPos.kind);
}
}
export function facingRequireAdjacent(pos: SemanticPosition, otherPos: SemanticPosition, moveName: string): Facing {
const res = facingAdjacent(pos, otherPos);
if (!res) {
throw new Error("Cannot " + moveName + " when not adjacent to paired dancer.");
}
return res;
}