diff --git a/client/package.json b/client/package.json index d4f12c8..c40301c 100644 --- a/client/package.json +++ b/client/package.json @@ -29,6 +29,7 @@ "socket.io-client": "^4.8.3", "stackmat-signal-processor": "^0.1.0", "uifx": "^2.0.7", + "letscube-scrambles": "0.1.0", "workbox-window": "^7.4.1" }, "scripts": { diff --git a/client/src/lib/events.js b/client/src/lib/events.js index 79bcb7f..db152ef 100644 --- a/client/src/lib/events.js +++ b/client/src/lib/events.js @@ -1,4 +1,4 @@ -import Events from './events.json'; +import Events from 'letscube-scrambles/events'; export { Events }; diff --git a/client/src/lib/events.json b/client/src/lib/events.json deleted file mode 100644 index 78cbcf5..0000000 --- a/client/src/lib/events.json +++ /dev/null @@ -1,134 +0,0 @@ -[ - { - "id": "333", - "scrambler": "333", - "name": "3x3", - "group": "WCA" - }, - { - "id": "222", - "scrambler": "222", - "name": "2x2", - "group": "WCA" - }, - { - "id": "444", - "scrambler": "444", - "name": "4x4", - "group": "WCA" - }, - { - "id": "555", - "scrambler": "555", - "name": "5x5", - "group": "WCA" - }, - { - "id": "666", - "scrambler": "666", - "name": "6x6", - "group": "WCA" - }, - { - "id": "777", - "scrambler": "777", - "name": "7x7", - "group": "WCA" - }, - { - "id": "333bf", - "scrambler": "333", - "name": "3x3 Blindfolded", - "group": "WCA" - }, - { - "id": "333oh", - "scrambler": "333", - "name": "3x3 One-Handed", - "group": "WCA" - }, - { - "id": "333ft", - "scrambler": "333", - "name": "3x3 With Feet", - "group": "WCA" - }, - { - "id": "minx", - "scrambler": "minx", - "name": "Megaminx", - "group": "WCA" - }, - { - "id": "pyram", - "scrambler": "pyram", - "name": "Pyraminx", - "group": "WCA" - }, - { - "id": "clock", - "scrambler": "clock", - "name": "Clock", - "group": "WCA" - }, - { - "id": "clock-optimal", - "scrambler": "clock-optimal", - "name": "Clock (Optimal)", - "group": "Other" - }, - { - "id": "skewb", - "scrambler": "skewb", - "name": "Skewb", - "group": "WCA" - }, - { - "id": "sq1", - "scrambler": "sq1", - "name": "Square-1", - "group": "WCA" - }, - { - "id": "444bf", - "scrambler": "444", - "name": "4x4 Blindfolded", - "group": "WCA" - }, - { - "id": "555bf", - "scrambler": "555", - "name": "5x5 Blindfolded", - "group": "WCA" - }, - { - "id": "fto", - "scrambler": "fto", - "name": "FTO", - "group": "Other" - }, - { - "id": "pll", - "scrambler": "pll", - "name": "PLL", - "group": "Other" - }, - { - "id": "zbll", - "scrambler": "zbll", - "name": "ZBLL", - "group": "Other" - }, - { - "id": "lse", - "scrambler": "lse", - "name": "Last Six Edges", - "group": "Other" - }, - { - "id": "ru", - "scrambler": "ru", - "name": "RU 2gen", - "group": "Other" - } -] diff --git a/package.json b/package.json index 5790db9..d722ac2 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,8 @@ "packageManager": "yarn@1.22.22", "workspaces": [ "client", - "server" + "server", + "packages/*" ], "resolutions": { "@hono/node-server": "1.19.13", diff --git a/packages/scrambles/.eslintrc.json b/packages/scrambles/.eslintrc.json new file mode 100644 index 0000000..078961d --- /dev/null +++ b/packages/scrambles/.eslintrc.json @@ -0,0 +1,16 @@ +{ + "env": { + "es6": true, + "node": true, + "jest": true + }, + "extends": [ + "airbnb-base" + ], + "parserOptions": { + "ecmaVersion": 2020 + }, + "rules": { + "no-console": "error" + } +} diff --git a/packages/scrambles/events.json b/packages/scrambles/events.json new file mode 100644 index 0000000..abbd215 --- /dev/null +++ b/packages/scrambles/events.json @@ -0,0 +1,24 @@ +[ + { "id": "333", "name": "3x3", "group": "WCA" }, + { "id": "222", "name": "2x2", "group": "WCA" }, + { "id": "444", "name": "4x4", "group": "WCA" }, + { "id": "555", "name": "5x5", "group": "WCA" }, + { "id": "666", "name": "6x6", "group": "WCA" }, + { "id": "777", "name": "7x7", "group": "WCA" }, + { "id": "333bf", "name": "3x3 Blindfolded", "group": "WCA" }, + { "id": "333oh", "name": "3x3 One-Handed", "group": "WCA" }, + { "id": "333ft", "name": "3x3 With Feet", "group": "WCA" }, + { "id": "minx", "name": "Megaminx", "group": "WCA" }, + { "id": "pyram", "name": "Pyraminx", "group": "WCA" }, + { "id": "clock", "name": "Clock", "group": "WCA" }, + { "id": "clock-optimal", "name": "Clock (Optimal)", "group": "Other" }, + { "id": "skewb", "name": "Skewb", "group": "WCA" }, + { "id": "sq1", "name": "Square-1", "group": "WCA" }, + { "id": "444bf", "name": "4x4 Blindfolded", "group": "WCA" }, + { "id": "555bf", "name": "5x5 Blindfolded", "group": "WCA" }, + { "id": "fto", "name": "FTO", "group": "Other" }, + { "id": "pll", "name": "PLL", "group": "Other" }, + { "id": "zbll", "name": "ZBLL", "group": "Other" }, + { "id": "lse", "name": "Last Six Edges", "group": "Other" }, + { "id": "ru", "name": "RU 2gen", "group": "Other" } +] diff --git a/packages/scrambles/index.js b/packages/scrambles/index.js new file mode 100644 index 0000000..10b7ad0 --- /dev/null +++ b/packages/scrambles/index.js @@ -0,0 +1,64 @@ +const events = require('./events.json'); +const { generateCubingScramble } = require('./providers/cubing'); +const { generateScrambowScramble } = require('./providers/scrambow'); + +const cubingEventIds = new Set([ + '222', + '333', + '333bf', + '333oh', + '333ft', + '444', + '444bf', + '555', + '555bf', + '666', + '777', + 'minx', + 'pyram', + 'clock', + 'skewb', + 'sq1', + 'fto', +]); + +const scrambowTypesByEventId = { + 333: '333', + 222: '222', + 444: '444', + 555: '555', + 666: '666', + 777: '777', + '333bf': '333', + '333oh': '333', + '333ft': '333', + minx: 'minx', + pyram: 'pyram', + clock: 'clock', + 'clock-optimal': 'clock-optimal', + skewb: 'skewb', + sq1: 'sq1', + '444bf': '444', + '555bf': '555', + fto: 'fto', + pll: 'pll', + zbll: 'zbll', + lse: 'lse', + ru: 'ru', +}; + +const knownEventIds = new Set(events.map(({ id }) => id)); + +async function generateScramble(eventId) { + if (!knownEventIds.has(eventId)) { + throw new Error(`Unsupported scramble event: ${eventId}`); + } + + if (cubingEventIds.has(eventId)) { + return generateCubingScramble(eventId); + } + + return generateScrambowScramble(scrambowTypesByEventId[eventId]); +} + +module.exports = { generateScramble }; diff --git a/packages/scrambles/index.test.js b/packages/scrambles/index.test.js new file mode 100644 index 0000000..8783466 --- /dev/null +++ b/packages/scrambles/index.test.js @@ -0,0 +1,101 @@ +/** @jest-environment node */ + +jest.mock('./providers/cubing', () => ({ + generateCubingScramble: jest.fn(), +})); +jest.mock('./providers/scrambow', () => ({ + generateScrambowScramble: jest.fn(), +})); + +const events = require('./events.json'); +const { generateScramble } = require('./index'); +const { generateCubingScramble } = require('./providers/cubing'); +const { generateScrambowScramble } = require('./providers/scrambow'); + +const cubingEventIds = [ + '222', + '333', + '333bf', + '333oh', + '333ft', + '444', + '444bf', + '555', + '555bf', + '666', + '777', + 'minx', + 'pyram', + 'clock', + 'skewb', + 'sq1', + 'fto', +]; + +describe('generateScramble', () => { + beforeEach(() => { + jest.clearAllMocks(); + generateCubingScramble.mockResolvedValue('cubing scramble'); + generateScrambowScramble.mockReturnValue('scrambow scramble'); + }); + + it.each(cubingEventIds)('uses cubing.js for %s', async (eventId) => { + await expect(generateScramble(eventId)).resolves.toBe('cubing scramble'); + + expect(generateCubingScramble).toHaveBeenCalledWith(eventId); + expect(generateScrambowScramble).not.toHaveBeenCalled(); + }); + + it('uses Scrambow for the custom practice events', async () => { + await expect(generateScramble('clock-optimal')).resolves.toBe('scrambow scramble'); + await expect(generateScramble('pll')).resolves.toBe('scrambow scramble'); + await expect(generateScramble('zbll')).resolves.toBe('scrambow scramble'); + await expect(generateScramble('lse')).resolves.toBe('scrambow scramble'); + await expect(generateScramble('ru')).resolves.toBe('scrambow scramble'); + + expect(generateScrambowScramble).toHaveBeenNthCalledWith(1, 'clock-optimal'); + expect(generateScrambowScramble).toHaveBeenNthCalledWith(2, 'pll'); + expect(generateScrambowScramble).toHaveBeenNthCalledWith(3, 'zbll'); + expect(generateScrambowScramble).toHaveBeenNthCalledWith(4, 'lse'); + expect(generateScrambowScramble).toHaveBeenNthCalledWith(5, 'ru'); + }); + + it('rejects events that are not in the shared catalog', async () => { + await expect(generateScramble('unknown')).rejects.toThrow('Unsupported scramble event: unknown'); + + expect(generateCubingScramble).not.toHaveBeenCalled(); + expect(generateScrambowScramble).not.toHaveBeenCalled(); + }); + + it('surfaces cubing.js failures without using Scrambow', async () => { + const error = new Error('cubing worker failed'); + generateCubingScramble.mockRejectedValue(error); + + await expect(generateScramble('333')).rejects.toBe(error); + expect(generateScrambowScramble).not.toHaveBeenCalled(); + }); +}); + +describe('events', () => { + it('contains every event supported by the generator', () => { + expect(events.map(({ id }) => id)).toEqual(expect.arrayContaining([ + ...cubingEventIds, + 'clock-optimal', + 'pll', + 'zbll', + 'lse', + 'ru', + ])); + }); + + it('does not expose provider implementation details', () => { + events.forEach((event) => { + expect(event).toEqual(expect.objectContaining({ + id: expect.any(String), + name: expect.any(String), + group: expect.any(String), + })); + expect(event.scrambler).toBeUndefined(); + }); + }); +}); diff --git a/packages/scrambles/package.json b/packages/scrambles/package.json new file mode 100644 index 0000000..e409093 --- /dev/null +++ b/packages/scrambles/package.json @@ -0,0 +1,30 @@ +{ + "name": "letscube-scrambles", + "version": "0.1.0", + "private": true, + "main": "index.js", + "exports": { + ".": "./index.js", + "./events": "./events.json" + }, + "files": [ + "index.js", + "events.json", + "providers" + ], + "dependencies": { + "cubing": "0.63.3", + "scrambow": "^1.8.1" + }, + "scripts": { + "lint": "eslint ./", + "test": "jest --passWithNoTests", + "test:ci": "yarn test" + }, + "devDependencies": { + "eslint": "6.8.0", + "eslint-config-airbnb-base": "14.1.0", + "eslint-plugin-import": "2.20.2", + "jest": "^26.6.0" + } +} diff --git a/packages/scrambles/providers/cubing.js b/packages/scrambles/providers/cubing.js new file mode 100644 index 0000000..b26b825 --- /dev/null +++ b/packages/scrambles/providers/cubing.js @@ -0,0 +1,29 @@ +let cubingModulesPromise; + +function loadCubingModules() { + if (!cubingModulesPromise) { + cubingModulesPromise = Promise.all([ + // cubing publishes ESM-only subpath exports, which eslint-plugin-import 2.20 cannot resolve. + // eslint-disable-next-line import/no-unresolved + import('cubing/scramble'), + // eslint-disable-next-line import/no-unresolved + import('cubing/search'), + ]).then(([scramble, search]) => { + search.setSearchDebug({ + logPerf: false, + scramblePrefetchLevel: 'none', + showWorkerInstantiationWarnings: false, + }); + return scramble; + }); + } + + return cubingModulesPromise; +} + +async function generateCubingScramble(eventId) { + const { randomScrambleForEvent } = await loadCubingModules(); + return (await randomScrambleForEvent(eventId)).toString(); +} + +module.exports = { generateCubingScramble }; diff --git a/packages/scrambles/providers/scrambow.js b/packages/scrambles/providers/scrambow.js new file mode 100644 index 0000000..f02b5c1 --- /dev/null +++ b/packages/scrambles/providers/scrambow.js @@ -0,0 +1,7 @@ +const { Scrambow } = require('scrambow'); + +function generateScrambowScramble(scramblerType) { + return new Scrambow().setType(scramblerType).get(1)[0].scramble_string; +} + +module.exports = { generateScrambowScramble }; diff --git a/server/models/room.js b/server/models/room.js index 366f6e8..37bdd72 100644 --- a/server/models/room.js +++ b/server/models/room.js @@ -1,8 +1,7 @@ const mongoose = require('mongoose'); -const { Scrambow } = require('scrambow'); const bcrypt = require('bcrypt'); const { v4: uuidv4 } = require('uuid'); -const Events = require('../../client/src/lib/events.json'); +const { generateScramble } = require('letscube-scrambles'); const { mirrorRoomChanges } = require('../postgres/dualWrite'); const PASSWORD_SALT_ROUNDS = 10; @@ -127,10 +126,6 @@ Room.index({ expireAfterSeconds: 0, }); -Room.virtual('scrambler').get(function () { - return new Scrambow().setType(Events.find((e) => e.id === this.event).scrambler); -}); - Room.virtual('usersInRoom').get(function () { return this.users.filter(({ id }) => this.inRoom.get(id.toString())); }); @@ -268,16 +263,16 @@ Room.methods.doneWithScramble = function () { return (this.waitingForCount === 0 || this.attempts.length === 0) && this.usersInRoom.length > 0; }; -Room.methods.genAttempt = function () { +Room.methods.genAttempt = async function () { return { id: this.attempts.length, - scrambles: this.scrambler.get(1).map((i) => i.scramble_string), + scrambles: [await generateScramble(this.event)], results: {}, }; }; -Room.methods.newAttempt = function () { - const attempt = this.genAttempt(); +Room.methods.newAttempt = async function () { + const attempt = await this.genAttempt(); this.attempts = this.attempts.concat([attempt]); diff --git a/server/models/room.test.js b/server/models/room.test.js index 131ad2b..49daee2 100644 --- a/server/models/room.test.js +++ b/server/models/room.test.js @@ -3,11 +3,44 @@ const bcrypt = require('bcrypt'); const mongoose = require('mongoose'); +const { generateScramble } = require('letscube-scrambles'); const { collectPostgresChanges, Room } = require('./room'); +jest.mock('letscube-scrambles', () => ({ + generateScramble: jest.fn(), +})); + const RoomModel = mongoose.model('RoomPostgresChangesTest', Room); describe('room security helpers', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('creates attempts with the shared async scramble generator', async () => { + generateScramble.mockResolvedValue('R U R\''); + const room = { + event: '333', + attempts: [], + usersInRoom: [{ id: 1234 }], + waitingFor: new Map(), + competing: new Map([['1234', true]]), + genAttempt: Room.methods.genAttempt, + save: jest.fn().mockResolvedValue(undefined), + }; + + await Room.methods.newAttempt.call(room); + + expect(generateScramble).toHaveBeenCalledWith('333'); + expect(room.attempts).toEqual([{ + id: 0, + scrambles: ['R U R\''], + results: {}, + }]); + expect(room.waitingFor.get('1234')).toBe(true); + expect(room.save).toHaveBeenCalledTimes(1); + }); + it('authenticates passwords asynchronously', async () => { const password = 'correct horse battery staple'; const room = { diff --git a/server/package.json b/server/package.json index e06ba46..fed3219 100644 --- a/server/package.json +++ b/server/package.json @@ -12,6 +12,7 @@ "express-socket.io-session": "^1.3.5", "getconfig": "^4.5.0", "ioredis": "^5.8.2", + "letscube-scrambles": "0.1.0", "lodash": "^4.17.21", "mongoose": "^6.13.9", "morgan": "^1.11.0", @@ -23,7 +24,6 @@ "pg": "8.22.0", "prisma": "7.8.0", "rest-api-errors": "^1.2.1", - "scrambow": "^1.8.1", "socket.io": "^4.8.3", "uuid": "^11.1.1", "winston": "3.2.1" diff --git a/yarn.lock b/yarn.lock index 2a44c39..a8b328d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2061,6 +2061,18 @@ dependencies: "@tybys/wasm-util" "^0.10.3" +"@optique/core@1.1.1", "@optique/core@>=0.9.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@optique/core/-/core-1.1.1.tgz#dfedf03cee4dbdc11b7a99d83908ccd4ba403c1f" + integrity sha512-tbZLdH5wEnh8IRG8KTmWPZCKHLmD4bXigoPdzoSUVi+V46qZTNEaTdqtR7EuEwhbPI0KQjLSYRZCxFTdr9OZkA== + +"@optique/run@>=0.9.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@optique/run/-/run-1.1.1.tgz#38565ef3c612fb36bc1eb205b0de6ac2a98932ac" + integrity sha512-jSZeuiljDEWYkQ67bVDucRknzFVwiSH1j4ho2jFWfcdMMa+WDRSfZA3GZoHA8zKj0Bf6UbJ+FMs/2IReQ0KYNQ== + dependencies: + "@optique/core" "1.1.1" + "@oxc-project/types@=0.139.0": version "0.139.0" resolved "https://registry.yarnpkg.com/@oxc-project/types/-/types-0.139.0.tgz#38d76b9dbf934c2a02be174fb32ceebf182fe742" @@ -2684,6 +2696,11 @@ resolved "https://registry.yarnpkg.com/@turbo/windows-arm64/-/windows-arm64-2.10.4.tgz#ab5f0c53266bbe18a52d1f5a0a15f718ba2378f3" integrity sha512-Iv02YgOpaEShc2OkG7mgCJ2pEw1RUKiKbs0h8W5wAf4jZ5vpmraTEjuGTgHRuOORQnC1GN3KHo5WB+hu1abRMA== +"@tweenjs/tween.js@~23.1.3": + version "23.1.3" + resolved "https://registry.yarnpkg.com/@tweenjs/tween.js/-/tween.js-23.1.3.tgz#eff0245735c04a928bb19c026b58c2a56460539d" + integrity sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA== + "@tybys/wasm-util@^0.10.3": version "0.10.3" resolved "https://registry.yarnpkg.com/@tybys/wasm-util/-/wasm-util-0.10.3.tgz#015cba9e9dd47ce14d03d2a8c5d547bfb169665d" @@ -2885,6 +2902,11 @@ resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.3.tgz#6209321eb2c1712a7e7466422b8cb1fc0d9dd5d8" integrity sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw== +"@types/stats.js@*": + version "0.17.4" + resolved "https://registry.yarnpkg.com/@types/stats.js/-/stats.js-0.17.4.tgz#1933e5ff153a23c7664487833198d685c22e791e" + integrity sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA== + "@types/testing-library__dom@*": version "7.5.0" resolved "https://registry.yarnpkg.com/@types/testing-library__dom/-/testing-library__dom-7.5.0.tgz#e0a00dd766983b1d6e9d10d33e708005ce6ad13e" @@ -2908,6 +2930,18 @@ "@types/testing-library__dom" "*" pretty-format "^25.1.0" +"@types/three@^0.169.0": + version "0.169.0" + resolved "https://registry.yarnpkg.com/@types/three/-/three-0.169.0.tgz#9cf4c33575721669d88846f22b265696d2c2e7a6" + integrity sha512-oan7qCgJBt03wIaK+4xPWclYRPG9wzcg7Z2f5T8xYTNEF95kh0t0lklxLLYBDo7gQiGLYzE6iF4ta7nXF2bcsw== + dependencies: + "@tweenjs/tween.js" "~23.1.3" + "@types/stats.js" "*" + "@types/webxr" "*" + "@webgpu/types" "*" + fflate "~0.8.2" + meshoptimizer "~0.18.1" + "@types/tmp@^0.2.3": version "0.2.6" resolved "https://registry.yarnpkg.com/@types/tmp/-/tmp-0.2.6.tgz#d785ee90c52d7cc020e249c948c36f7b32d1e217" @@ -2928,11 +2962,21 @@ resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.11.tgz#11af57b127e32487774841f7a4e54eab166d03c4" integrity sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA== +"@types/web-bluetooth@^0.0.20": + version "0.0.20" + resolved "https://registry.yarnpkg.com/@types/web-bluetooth/-/web-bluetooth-0.0.20.tgz#f066abfcd1cbe66267cdbbf0de010d8a41b41597" + integrity sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow== + "@types/webidl-conversions@*": version "7.0.3" resolved "https://registry.yarnpkg.com/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz#1306dbfa53768bcbcfc95a1c8cde367975581859" integrity sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA== +"@types/webxr@*": + version "0.5.24" + resolved "https://registry.yarnpkg.com/@types/webxr/-/webxr-0.5.24.tgz#734d5d90dadc5809a53e422726c60337fa2f4a44" + integrity sha512-h8fgEd/DpoS9CBrjEQXR+dIDraopAEfu4wYVNY2tEPwk60stPWhvZMf4Foo5FakuQ7HFZoa8WceaWFervK2Ovg== + "@types/whatwg-url@^8.2.1": version "8.2.2" resolved "https://registry.yarnpkg.com/@types/whatwg-url/-/whatwg-url-8.2.2.tgz#749d5b3873e845897ada99be4448041d4cc39e63" @@ -3024,6 +3068,11 @@ dependencies: "@rolldown/pluginutils" "^1.0.1" +"@webgpu/types@*": + version "0.1.71" + resolved "https://registry.yarnpkg.com/@webgpu/types/-/types-0.1.71.tgz#46ed33ba7816029d622d0b52e1e0004bf441db98" + integrity sha512-mMy8/ODcKhab808co15eW+yN+HgXoQxRQHTiBV9Mrvl1r0ufnid7YOcI+gi4eUWSWl9ezD6TW2KXccrL8HCh2A== + abab@^2.0.0, abab@^2.0.3, abab@^2.0.5: version "2.0.6" resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.6.tgz#41b80f2c871d19686216b82309231cfd3cb3d291" @@ -4598,6 +4647,19 @@ csstype@^3.0.2, csstype@^3.2.2: resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.2.3.tgz#ec48c0f3e993e50648c86da559e2610995cf989a" integrity sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ== +cubing@0.63.3: + version "0.63.3" + resolved "https://registry.yarnpkg.com/cubing/-/cubing-0.63.3.tgz#b1e0ec2c5385a39a4b20559f5a13ffda2b577981" + integrity sha512-151hSarw9sN06iohHClHQ3RaiI5MNpaIyyb1+I0cuRerHq3jfyhb2Eyif9YAEIbEJiiMc6AUsNFo00Rnti2JWw== + dependencies: + "@optique/core" ">=0.9.1" + "@optique/run" ">=0.9.1" + "@types/three" "^0.169.0" + "@types/web-bluetooth" "^0.0.20" + random-uint-below v3.3.0 + three "^0.170.0" + type-fest "^5.2.0" + cypress@^15.18.1: version "15.18.1" resolved "https://registry.yarnpkg.com/cypress/-/cypress-15.18.1.tgz#b4d6e0f124efd18e5694cc1bd6dfd15db9319604" @@ -5943,6 +6005,11 @@ fecha@^4.2.0: resolved "https://registry.yarnpkg.com/fecha/-/fecha-4.2.3.tgz#4d9ccdbc61e8629b259fdca67e65891448d569fd" integrity sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw== +fflate@~0.8.2: + version "0.8.3" + resolved "https://registry.yarnpkg.com/fflate/-/fflate-0.8.3.tgz#bc27d8eb30343d4d512abb03480202ce65d825fc" + integrity sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA== + figures@^3.0.0: version "3.2.0" resolved "https://registry.yarnpkg.com/figures/-/figures-3.2.0.tgz#625c18bd293c604dc4a8ddb2febf0c88341746af" @@ -8635,6 +8702,11 @@ merge-stream@^2.0.0: resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== +meshoptimizer@~0.18.1: + version "0.18.1" + resolved "https://registry.yarnpkg.com/meshoptimizer/-/meshoptimizer-0.18.1.tgz#cdb90907f30a7b5b1190facd3b7ee6b7087797d8" + integrity sha512-ZhoIoL7TNV4s5B6+rx5mC//fw8/POGyNxS/DZyCJeiZ12ScLfVwRE/GfsxwiTkMYYD5DmK2/JXnEVXqL4rF+Sw== + methods@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" @@ -9949,6 +10021,11 @@ random-bytes@~1.0.0: resolved "https://registry.yarnpkg.com/random-bytes/-/random-bytes-1.0.0.tgz#4f68a1dc0ae58bd3fb95848c30324db75d64360b" integrity sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ== +random-uint-below@v3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/random-uint-below/-/random-uint-below-3.3.0.tgz#4d911f497215eb9103f8c09bc5d1a3e21cbaaa20" + integrity sha512-5x3lft1bn5gev9qA9iVFcmd0+t10j3eDl/QKB56T/3v/bdrM7WsfO2D0rb1JYcQdkcV0/Bs5iydgQmkTE6rczA== + range-parser@~1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" @@ -11555,6 +11632,11 @@ table@^5.2.3: slice-ansi "^2.1.0" string-width "^3.0.0" +tagged-tag@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/tagged-tag/-/tagged-tag-1.0.0.tgz#a0b5917c2864cba54841495abfa3f6b13edcf4d6" + integrity sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng== + temp-dir@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/temp-dir/-/temp-dir-2.0.0.tgz#bde92b05bdfeb1516e804c9c00ad45177f31321e" @@ -11622,6 +11704,11 @@ text-table@^0.2.0: resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" integrity sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw== +three@^0.170.0: + version "0.170.0" + resolved "https://registry.yarnpkg.com/three/-/three-0.170.0.tgz#6087f97aab79e9e9312f9c89fcef6808642dfbb7" + integrity sha512-FQK+LEpYc0fBD+J8g6oSEyyNzjp+Q7Ks1C568WWaoMRLW+TkNNWmenWeGgJjV105Gd+p/2ql1ZcjYvNiPZBhuQ== + throat@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/throat/-/throat-5.0.0.tgz#c5199235803aad18754a667d659b5e72ce16764b" @@ -11875,6 +11962,13 @@ type-fest@^0.8.0, type-fest@^0.8.1: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d" integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA== +type-fest@^5.2.0: + version "5.8.0" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-5.8.0.tgz#6d517998257c33159db4d4da6f18efa33bd47df3" + integrity sha512-YGYEVz3Fm5iy/AybuA0oyNFq7H4CgQNfRp/qfe8nurE1kuCeNm3/vfm9X4Mtl+qLyaKJUh5xrFZwogr41SMjYA== + dependencies: + tagged-tag "^1.0.0" + type-is@~1.6.18: version "1.6.18" resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131"