Skip to content

Commit

Permalink
[puzzles] Support puzzle-specific keyboard mappings.
Browse files Browse the repository at this point in the history
Also make keyboard mapping independent of keyboard layout.
  • Loading branch information
lgarron committed Aug 3, 2024
1 parent f400310 commit ee3c092
Show file tree
Hide file tree
Showing 10 changed files with 138 additions and 61 deletions.
1 change: 1 addition & 0 deletions script/test/src/import-restrictions/allowedImports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ export const mainAllowedImports: AllowedImports = {
"src/cubing/vendor/public-domain/unsafe-raw-aes",
"three",
],
dynamic: ["src/cubing/puzzles"],
},
"src/cubing/kpuzzle": { static: ["src/cubing/alg", "src/cubing/kpuzzle"] },
"src/cubing/notation": {
Expand Down
54 changes: 12 additions & 42 deletions src/cubing/alg/keyboard.ts
Original file line number Diff line number Diff line change
@@ -1,52 +1,22 @@
import { Pause } from "./alg-nodes";
import type { KeyMapping } from "cubing/puzzles/PuzzleLoader";
import type { AlgLeaf } from "./alg-nodes/AlgNode";
import { Move } from "./alg-nodes/leaves/Move";

const cubeKeyMapping: { [key: number]: AlgLeaf } = {
73: new Move("R"),
75: new Move("R'"),
87: new Move("B"),
79: new Move("B'"),
83: new Move("D"),
76: new Move("D'"),
68: new Move("L"),
69: new Move("L'"),
74: new Move("U"),
70: new Move("U'"),
72: new Move("F"),
71: new Move("F'"),

78: new Move("x'"),
67: new Move("l"),
82: new Move("l'"),
85: new Move("r"),
77: new Move("r'"),

88: new Move("d"),
188: new Move("d'"),

84: new Move("x"),
89: new Move("x"),
66: new Move("x'"),
186: new Move("y"),
59: new Move("y"),
65: new Move("y'"), // 186 is WebKit, 59 is Mozilla; see http://unixpapa.com/js/key.html
80: new Move("z"),
81: new Move("z'"),

90: new Move("M'"),
190: new Move("M'"),

192: new Pause(),
};

// TODO: options about whether to ignore modifier keys (e.g. alt, ctrl).
// TODO: Support different mappings.
// TODO: Return BaseMove instead?
export function keyToMove(e: KeyboardEvent): AlgLeaf | null {
export function keyToMove(
keyMapping: KeyMapping | undefined,
e: KeyboardEvent,
): AlgLeaf | null {
if (e.altKey || e.ctrlKey) {
return null;
}

return cubeKeyMapping[e.keyCode] || null;
// https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/code
// > The KeyboardEvent.code property represents a physical key on the keyboard
// > (as opposed to the character generated by pressing the key). In other
// > words, this property returns a value that isn't altered by keyboard
// > layout or the state of the modifier keys.
const layoutIndepdendentCode = e.code;
return keyMapping?.[layoutIndepdendentCode] ?? null;
}
33 changes: 25 additions & 8 deletions src/cubing/bluetooth/keyboard.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,45 @@
import { Alg, keyToMove } from "../alg";
import type { KPuzzle } from "../kpuzzle";
import type { KPattern } from "../kpuzzle/KPattern";
import { puzzles } from "../puzzles";
import type { PuzzleLoader } from "../puzzles";
import type { KeyMapping } from "../puzzles/cubing-private";
import type { PuzzleID } from "../twisty";
import { BluetoothPuzzle } from "./smart-puzzle/bluetooth-puzzle";

/** @category Keyboard Puzzles */
export class KeyboardPuzzle extends BluetoothPuzzle {
private puzzle: Promise<KPuzzle> = puzzles["3x3x3"].kpuzzle();
private pattern: Promise<KPattern> = (async () =>
(await this.puzzle).defaultPattern())();
private keyMappingPromise: Promise<KeyMapping | undefined>;
private pattern: Promise<KPattern>;

listener: (e: KeyboardEvent) => Promise<void>;

// TODO: Decide on the right arguments.
constructor(private target: Element) {
// TODO: support tying the puzzle to a TwistyPlayer.
constructor(
private target: Element,
puzzle: PuzzleID | PuzzleLoader = "3x3x3",
) {
super();
// TODO: Filter out repeated keydown?
this.listener = this.onKeyDown.bind(this);
target.addEventListener("keydown", this.listener);
this.setPuzzle(puzzle);
}

public name(): string | undefined {
return "Keyboard Input";
}

public async setPuzzle(puzzle: PuzzleID | PuzzleLoader) {
const puzzlePromise = (async () =>
typeof puzzle === "string"
? (await import("../puzzles")).puzzles[puzzle]
: puzzle)();
const kpuzzlePromise = (async () => (await puzzlePromise).kpuzzle())();
this.keyMappingPromise = (async () =>
(await puzzlePromise).keyMapping?.())();
this.pattern = (async () => (await kpuzzlePromise).defaultPattern())();
}

disconnect() {
this.target.removeEventListener("keydown", this.listener);
}
Expand All @@ -37,7 +53,7 @@ export class KeyboardPuzzle extends BluetoothPuzzle {
return;
}

const algLeaf = keyToMove(e);
const algLeaf = keyToMove(await this.keyMappingPromise, e);
if (algLeaf) {
const newPattern = (await this.pattern).applyAlg(new Alg([algLeaf])); // TODO
this.pattern = Promise.resolve(newPattern);
Expand All @@ -55,6 +71,7 @@ export class KeyboardPuzzle extends BluetoothPuzzle {
/** @category Keyboard Puzzles */
export async function debugKeyboardConnect(
target: any = window,
puzzle: PuzzleID | PuzzleLoader = "3x3x3",
): Promise<KeyboardPuzzle> {
return new KeyboardPuzzle(target);
return new KeyboardPuzzle(target, puzzle);
}
5 changes: 4 additions & 1 deletion src/cubing/puzzles/PuzzleLoader.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import type { PuzzleSpecificSimplifyOptions } from "../alg";
import type { AlgLeaf, PuzzleSpecificSimplifyOptions } from "../alg";
import type { AppendOptions } from "../alg/simplify";
import type { KPuzzle } from "../kpuzzle";
import type { PuzzleGeometry } from "../puzzle-geometry";
import type { ExperimentalStickering } from "../twisty";
import type { StickeringMask } from "./stickerings/mask";

export type KeyMapping = { [keyCode: string]: AlgLeaf };

export interface PuzzleLoader {
id: string;
// shortName?: string;
Expand All @@ -24,6 +26,7 @@ export interface PuzzleLoader {
stickerings?: () => Promise<ExperimentalStickering[]>;
puzzleSpecificSimplifyOptions?: PuzzleSpecificSimplifyOptions;
puzzleSpecificSimplifyOptionsPromise?: Promise<PuzzleSpecificSimplifyOptions>; // TODO
keyMapping?: () => Promise<KeyMapping>; // TODO: async getter
}

// TODO: consolidate the `puzzleSpecificSimplifyOptionsPromise` with `puzzleSpecificSimplifyOptions` somehow, so that we don't have to do this.
Expand Down
14 changes: 8 additions & 6 deletions src/cubing/puzzles/cubing-private/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,15 @@ export {
} from "../implementations/dynamic/2x2x2/puzzle-orientation"; // TODO: Actually dynamic

export { customPGPuzzleLoader as experimentalCustomPGPuzzleLoader } from "../customPGPuzzleLoader";
export { getFaceletStickeringMask as experimentalGetFaceletStickeringMask } from "../stickerings/mask";
export type {
PieceStickeringMask as ExperimentalPieceStickeringMask,
FaceletMeshStickeringMask as ExperimentalFaceletMeshStickeringMask,
StickeringMask as ExperimentalStickeringMask,
} from "../stickerings/mask";
export {
getFaceletStickeringMask as experimentalGetFaceletStickeringMask,
getPieceStickeringMask as experimentalGetPieceStickeringMask,
PieceStickering as ExperimentalPieceStickering,
} from "../stickerings/mask";
export type {
FaceletMeshStickeringMask as ExperimentalFaceletMeshStickeringMask,
PieceStickeringMask as ExperimentalPieceStickeringMask,
StickeringMask as ExperimentalStickeringMask,
} from "../stickerings/mask";

export type { KeyMapping } from "../PuzzleLoader";
41 changes: 41 additions & 0 deletions src/cubing/puzzles/implementations/3x3x3/cube3x3x3keyMapping.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { Move, Pause, type AlgLeaf } from "../../../alg";

// See: https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/code

export const cube3x3x3KeyMapping: { [key: number | string]: AlgLeaf } = {
KeyI: new Move("R"),
KeyK: new Move("R'"),
KeyW: new Move("B"),
KeyO: new Move("B'"),
KeyS: new Move("D"),
KeyL: new Move("D'"),
KeyD: new Move("L"),
KeyE: new Move("L'"),
KeyJ: new Move("U"),
KeyF: new Move("U'"),
KeyH: new Move("F"),
KeyG: new Move("F'"),

KeyN: new Move("x'"),
KeyC: new Move("l"),
KeyR: new Move("l'"),
KeyU: new Move("r"),
KeyM: new Move("r'"),

KeyX: new Move("d"),
Comma: new Move("d'"),

KeyT: new Move("x"),
KeyY: new Move("x"),
KeyV: new Move("x'"),
Semicolon: new Move("y"),
KeyA: new Move("y'"),
KeyP: new Move("z"),
KeyQ: new Move("z'"),

KeyZ: new Move("M'"),
KeyB: new Move("M"),
Period: new Move("M'"),

Backquote: new Pause(),
};
6 changes: 4 additions & 2 deletions src/cubing/puzzles/implementations/3x3x3/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@ import { asyncGetPuzzleGeometry } from "../../async/async-pg3d";
import { getCached } from "../../async/lazy-cached";
import { experimental3x3x3KPuzzle } from "../../cubing-private";
import type { PuzzleLoader } from "../../PuzzleLoader";
import type { StickeringMask } from "../../stickerings/mask";
import {
cubeLikeStickeringMask,
cubeLikeStickeringList,
cubeLikeStickeringMask,
} from "../../stickerings/cube-like-stickerings";
import type { StickeringMask } from "../../stickerings/mask";
import { cube3x3x3KeyMapping } from "./cube3x3x3KeyMapping";
import { puzzleSpecificSimplifyOptions333 } from "./puzzle-specific-simplifications";

/** @category Specific Puzzles */
Expand Down Expand Up @@ -40,4 +41,5 @@ export const cube3x3x3 = {
): Promise<StickeringMask> => cubeLikeStickeringMask(cube3x3x3, stickering),
stickerings: () => cubeLikeStickeringList("3x3x3"),
puzzleSpecificSimplifyOptions: puzzleSpecificSimplifyOptions333,
keyMapping: async () => cube3x3x3KeyMapping,
} satisfies PuzzleLoader;
39 changes: 39 additions & 0 deletions src/cubing/puzzles/implementations/fto/ftoKeyMapping.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { Move, Pause, type AlgLeaf } from "../../../alg";

export const ftoKeyMapping: { [key: number | string]: AlgLeaf } = {
KeyI: new Move("R"),
KeyK: new Move("R'"),
KeyW: new Move("B"),
KeyO: new Move("B'"),
KeyS: new Move("D"),
KeyL: new Move("D'"),
KeyD: new Move("L"),
KeyE: new Move("L'"),
KeyJ: new Move("U"),
KeyF: new Move("U'"),
KeyH: new Move("F"),
KeyG: new Move("F'"),

KeyN: new Move("Rv'"),
KeyC: new Move("l"),
KeyR: new Move("l'"),
KeyU: new Move("r"),
KeyM: new Move("r'"),

KeyX: new Move("d"),
Comma: new Move("d'"),

KeyT: new Move("Lv'"),
KeyY: new Move("Rv"),
KeyV: new Move("Lv"),
Semicolon: new Move("Uv"),
KeyA: new Move("Uv'"),
KeyP: new Move("BR'"),
KeyQ: new Move("BL"),

KeyZ: new Move("BL'"),
KeyB: new Move("T"),
Period: new Move("BR"),

Backquote: new Pause(),
};
4 changes: 3 additions & 1 deletion src/cubing/puzzles/implementations/fto/index.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import type { ExperimentalStickering } from "../../../twisty";
import { PGPuzzleLoader } from "../../async/async-pg3d";
import { getCached } from "../../async/lazy-cached";
import type { StickeringMask } from "../../stickerings/mask";
import {
ftoStickering,
ftoStickerings,
} from "../../stickerings/fto-stickerings";
import type { StickeringMask } from "../../stickerings/mask";
import { ftoKeyMapping } from "./ftoKeyMapping";

class FTOPuzzleLoader extends PGPuzzleLoader {
constructor() {
Expand All @@ -25,6 +26,7 @@ class FTOPuzzleLoader extends PGPuzzleLoader {
return (await import("../dynamic/unofficial/puzzles-dynamic-unofficial"))
.ftoSVG;
});
keyMapping = async () => ftoKeyMapping;
}

export const fto = new FTOPuzzleLoader();
2 changes: 1 addition & 1 deletion src/sites/experiments.cubing.net/cubing.js/play/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,7 @@ const fn = async (
swipeyPuzzle.showGrid();
}

const kbp = await debugKeyboardConnect(document.body);
const kbp = await debugKeyboardConnect(document.body, getPuzzleID());
kbp.addAlgLeafListener(algLeafListener);

window.removeEventListener("keydown", keyboardCallback);
Expand Down

0 comments on commit ee3c092

Please sign in to comment.