diff --git a/package-lock.json b/package-lock.json index 139bbf6..c79af07 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,7 +15,8 @@ }, "devDependencies": { "@vitejs/plugin-vue": "^5.2.1", - "vite": "^6.2.0" + "vite": "^6.2.0", + "vitest": "^4.0.18" } }, "node_modules/@babel/helper-string-parser": { @@ -490,9 +491,9 @@ } }, "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", - "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", "license": "MIT" }, "node_modules/@rollup/rollup-android-arm-eabi": { @@ -761,6 +762,31 @@ "win32" ] }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", @@ -782,6 +808,127 @@ "vue": "^3.2.25" } }, + "node_modules/@vitest/expect": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.18.tgz", + "integrity": "sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.0.18", + "@vitest/utils": "4.0.18", + "chai": "^6.2.1", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.0.18.tgz", + "integrity": "sha512-HhVd0MDnzzsgevnOWCBj5Otnzobjy5wLBe4EdeeFGv8luMsGcYqDuFRMcttKWZA5vVO8RFjexVovXvAM4JoJDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.0.18", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/mocker/node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.18.tgz", + "integrity": "sha512-P24GK3GulZWC5tz87ux0m8OADrQIUVDPIjjj65vBXYG17ZeU3qD7r+MNZ1RNv4l8CGU2vtTRqixrOi9fYk/yKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.0.18.tgz", + "integrity": "sha512-rpk9y12PGa22Jg6g5M3UVVnTS7+zycIGk9ZNGN+m6tZHKQb7jrP7/77WfZy13Y/EUDd52NDsLRQhYKtv7XfPQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.0.18", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.0.18.tgz", + "integrity": "sha512-PCiV0rcl7jKQjbgYqjtakly6T1uwv/5BQ9SwBLekVg/EaYeQFPiXcgrC2Y7vDMA8dM1SUEAEV82kgSQIlXNMvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.0.18", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.0.18.tgz", + "integrity": "sha512-cbQt3PTSD7P2OARdVW3qWER5EGq7PHlvE+QfzSC0lbwO+xnt7+XH06ZzFjFRgzUX//JmpxrCu92VdwvEPlWSNw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.0.18.tgz", + "integrity": "sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.0.18", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, "node_modules/@vue/compiler-core": { "version": "3.5.13", "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.13.tgz", @@ -882,6 +1029,26 @@ "integrity": "sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ==", "license": "MIT" }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/csstype": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", @@ -909,6 +1076,13 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, "node_modules/esbuild": { "version": "0.25.1", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.1.tgz", @@ -956,6 +1130,34 @@ "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", "license": "MIT" }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -981,12 +1183,12 @@ } }, "node_modules/magic-string": { - "version": "0.30.17", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", - "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", "license": "MIT", "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0" + "@jridgewell/sourcemap-codec": "^1.5.5" } }, "node_modules/nanoid": { @@ -5352,12 +5554,43 @@ "node": ">=6" } }, + "node_modules/obug": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", + "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT" + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", "license": "ISC" }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/postcss": { "version": "8.5.3", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", @@ -5425,6 +5658,13 @@ "fsevents": "~2.3.2" } }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -5434,6 +5674,64 @@ "node": ">=0.10.0" } }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz", + "integrity": "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyrainbow": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.0.3.tgz", + "integrity": "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/toastify-js": { "version": "1.12.0", "resolved": "https://registry.npmjs.org/toastify-js/-/toastify-js-1.12.0.tgz", @@ -5512,6 +5810,84 @@ } } }, + "node_modules/vitest": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.0.18.tgz", + "integrity": "sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.0.18", + "@vitest/mocker": "4.0.18", + "@vitest/pretty-format": "4.0.18", + "@vitest/runner": "4.0.18", + "@vitest/snapshot": "4.0.18", + "@vitest/spy": "4.0.18", + "@vitest/utils": "4.0.18", + "es-module-lexer": "^1.7.0", + "expect-type": "^1.2.2", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^3.10.0", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.0.3", + "vite": "^6.0.0 || ^7.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.0.18", + "@vitest/browser-preview": "4.0.18", + "@vitest/browser-webdriverio": "4.0.18", + "@vitest/ui": "4.0.18", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, "node_modules/vue": { "version": "3.5.13", "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.13.tgz", @@ -5532,6 +5908,23 @@ "optional": true } } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } } } } diff --git a/package.json b/package.json index 429c2a8..e861bf1 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,8 @@ "scripts": { "dev": "vite", "build": "vite build", - "preview": "vite preview" + "preview": "vite preview", + "test": "vitest run" }, "dependencies": { "cubejs": "^1.3.2", @@ -16,6 +17,7 @@ }, "devDependencies": { "@vitejs/plugin-vue": "^5.2.1", - "vite": "^6.2.0" + "vite": "^6.2.0", + "vitest": "^4.0.18" } } diff --git a/src/components/renderers/MoveHistoryPanel.vue b/src/components/renderers/MoveHistoryPanel.vue index 0824624..5304720 100644 --- a/src/components/renderers/MoveHistoryPanel.vue +++ b/src/components/renderers/MoveHistoryPanel.vue @@ -171,6 +171,8 @@ onUnmounted(() => { align-items: center; justify-content: center; width: 16px; + min-width: 16px; + min-height: 24px; padding: 4px 8px; border-radius: 999px; border: 1px solid rgba(255, 255, 255, 0.2); diff --git a/src/components/renderers/SmartCube.vue b/src/components/renderers/SmartCube.vue index e2171ad..3bf2d8b 100644 --- a/src/components/renderers/SmartCube.vue +++ b/src/components/renderers/SmartCube.vue @@ -11,6 +11,7 @@ import { identityMatrix, rotateXMatrix, rotateYMatrix, rotateZMatrix, multiplyMa import { MOVE_MAP, INTERNAL_TO_UI, getAxisIndexForBase, getMathDirectionForBase, getDragMoveLabel, coerceStepsToSign, formatMoveLabel } from "../../utils/moveMapping.js"; import { easeInOutCubic, easeInOutCubicDerivative, cubicEaseWithInitialVelocity, cubicEaseWithInitialVelocityDerivative } from "../../utils/easing.js"; import { getFaceNormal as getFaceNormalRaw, getAllowedAxes as getAllowedAxesRaw, getAxisVector, cross, project as projectRaw } from "../../utils/cubeProjection.js"; +import { tokenReducer } from "../../utils/tokenReducer.js"; const { cubies, deepCubeState, initCube, rotateLayer, rotateSlice, turn, FACES, solve, solveResult, solveError, isSolverReady } = useCube(); const { isCubeTranslucent } = useSettings(); @@ -320,8 +321,22 @@ const finishMove = (steps, directionOverride = null) => { const movesHistory = ref([]); const displayMoves = computed(() => { - const list = movesHistory.value.slice(); + // Reduce completed moves (consolidate consecutive same-face) + const done = movesHistory.value.filter((m) => m.status === 'done'); + const inProgress = movesHistory.value.filter((m) => m.status === 'in_progress'); + const doneLabels = done.map((m) => m.label); + const reduced = tokenReducer(doneLabels); + const list = reduced.tokens.map((label, idx) => ({ + id: `r-${idx}`, + label, + status: 'done', + })); + + // Append in-progress moves as-is + inProgress.forEach((m) => list.push(m)); + + // Append pending queue moves moveQueue.value.forEach((q, idx) => { const stepsMod = ((q.steps % 4) + 4) % 4; if (stepsMod === 0) return; @@ -533,10 +548,8 @@ const stepProgrammaticAnimation = (time) => { (m) => m.id === currentMoveId.value, ); if (idx !== -1) { - movesHistory.value[idx] = { - ...movesHistory.value[idx], - status: "done", - }; + // 0 steps = full rotation, remove from history instead of marking done + movesHistory.value.splice(idx, 1); } currentMoveId.value = null; } @@ -674,8 +687,10 @@ const applyMove = (move) => { currentAnim.v0 = Math.max(-3, Math.min(3, v0)); - // Format the new label instantly - updateCurrentMoveLabel(displayBase, currentAnim.logicalSteps); + // Convert logicalSteps (math convention) to label steps (UI convention) + // For mathDir=-1 faces (R/U/F): display = logicalSteps (same sign) + // For mathDir=1 faces (D/L/B): display = -logicalSteps (inverted sign) + updateCurrentMoveLabel(displayBase, -mathDir * currentAnim.logicalSteps); return; } diff --git a/src/config/settings.js b/src/config/settings.js index 12a0079..6eddd02 100644 --- a/src/config/settings.js +++ b/src/config/settings.js @@ -1,3 +1,4 @@ + export const LAYER_ANIMATION_DURATION = 200; export const MIDDLE_SLICES_ENABLED = false; diff --git a/src/utils/moveMapping.js b/src/utils/moveMapping.js index af1ee7e..9927f3f 100644 --- a/src/utils/moveMapping.js +++ b/src/utils/moveMapping.js @@ -3,114 +3,114 @@ // UI button key → internal base + modifier export const MOVE_MAP = { - U: { base: "U", modifier: "" }, - "U-prime": { base: "U", modifier: "'" }, - U2: { base: "U", modifier: "2" }, + U: { base: "U", modifier: "" }, + "U-prime": { base: "U", modifier: "'" }, + U2: { base: "U", modifier: "2" }, - D: { base: "D", modifier: "" }, - "D-prime": { base: "D", modifier: "'" }, - D2: { base: "D", modifier: "2" }, + D: { base: "D", modifier: "" }, + "D-prime": { base: "D", modifier: "'" }, + D2: { base: "D", modifier: "2" }, - L: { base: "B", modifier: "" }, - "L-prime": { base: "B", modifier: "'" }, - L2: { base: "B", modifier: "2" }, + L: { base: "B", modifier: "" }, + "L-prime": { base: "B", modifier: "'" }, + L2: { base: "B", modifier: "2" }, - R: { base: "F", modifier: "" }, - "R-prime": { base: "F", modifier: "'" }, - R2: { base: "F", modifier: "2" }, + R: { base: "F", modifier: "" }, + "R-prime": { base: "F", modifier: "'" }, + R2: { base: "F", modifier: "2" }, - F: { base: "L", modifier: "" }, - "F-prime": { base: "L", modifier: "'" }, - F2: { base: "L", modifier: "2" }, + F: { base: "L", modifier: "" }, + "F-prime": { base: "L", modifier: "'" }, + F2: { base: "L", modifier: "2" }, - B: { base: "R", modifier: "" }, - "B-prime": { base: "R", modifier: "'" }, - B2: { base: "R", modifier: "2" }, + B: { base: "R", modifier: "" }, + "B-prime": { base: "R", modifier: "'" }, + B2: { base: "R", modifier: "2" }, }; // Internal face name → UI face name export const INTERNAL_TO_UI = { - 'F': 'R', 'B': 'L', 'R': 'B', 'L': 'F', - 'U': 'U', 'D': 'D', - 'M': 'M', 'E': 'E', 'S': 'S', + 'F': 'R', 'B': 'L', 'R': 'B', 'L': 'F', + 'U': 'U', 'D': 'D', + 'M': 'M', 'E': 'E', 'S': 'S', }; // Internal base → axis and layer index export const getAxisIndexForBase = (base) => { - if (base === "U") return { axis: "y", index: 1 }; - if (base === "D") return { axis: "y", index: -1 }; - if (base === "L") return { axis: "x", index: -1 }; - if (base === "R") return { axis: "x", index: 1 }; - if (base === "F") return { axis: "z", index: 1 }; - if (base === "B") return { axis: "z", index: -1 }; - return { axis: "y", index: 0 }; + if (base === "U") return { axis: "y", index: 1 }; + if (base === "D") return { axis: "y", index: -1 }; + if (base === "L") return { axis: "x", index: -1 }; + if (base === "R") return { axis: "x", index: 1 }; + if (base === "F") return { axis: "z", index: 1 }; + if (base === "B") return { axis: "z", index: -1 }; + return { axis: "y", index: 0 }; }; // Mathematical positive rotation direction (Right-Hand Rule) export const getMathDirectionForBase = (base) => { - if (['R', 'U', 'F', 'S'].includes(base)) return -1; - if (['L', 'D', 'B', 'M', 'E'].includes(base)) return 1; - return 1; + if (['R', 'U', 'F', 'S'].includes(base)) return -1; + if (['L', 'D', 'B', 'M', 'E'].includes(base)) return 1; + return 1; }; // Convert axis/index/direction to a standard Rubik's notation label (UI-facing) export const getDragMoveLabel = (axis, index, direction, count) => { - const OUTER_MAP = { - 'y_1': { base: 'U', dir: -1 }, - 'y_-1': { base: 'D', dir: 1 }, - 'x_1': { base: 'R', dir: -1 }, - 'x_-1': { base: 'L', dir: 1 }, - 'z_1': { base: 'F', dir: -1 }, - 'z_-1': { base: 'B', dir: 1 }, - }; - const SLICE_MAP = { - 'x_0': { base: 'M', dir: 1 }, - 'y_0': { base: 'E', dir: 1 }, - 'z_0': { base: 'S', dir: -1 }, - }; + const OUTER_MAP = { + 'y_1': { base: 'U', dir: -1 }, + 'y_-1': { base: 'D', dir: 1 }, + 'x_1': { base: 'R', dir: -1 }, + 'x_-1': { base: 'L', dir: 1 }, + 'z_1': { base: 'F', dir: -1 }, + 'z_-1': { base: 'B', dir: 1 }, + }; + const SLICE_MAP = { + 'x_0': { base: 'M', dir: 1 }, + 'y_0': { base: 'E', dir: 1 }, + 'z_0': { base: 'S', dir: -1 }, + }; - const key = `${axis}_${index}`; - const mapping = OUTER_MAP[key] || SLICE_MAP[key]; - if (!mapping) return null; + const key = `${axis}_${index}`; + const mapping = OUTER_MAP[key] || SLICE_MAP[key]; + if (!mapping) return null; - const effective = direction * mapping.dir; - const stepsMod = ((count % 4) + 4) % 4; - if (stepsMod === 0) return null; + const effective = direction * mapping.dir; + const stepsMod = ((count % 4) + 4) % 4; + if (stepsMod === 0) return null; - let modifier = ''; - if (stepsMod === 2) { - modifier = '2'; - } else if ((effective > 0 && stepsMod === 1) || (effective < 0 && stepsMod === 3)) { - modifier = ''; - } else { - modifier = "'"; - } + let modifier = ''; + if (stepsMod === 2) { + modifier = '2'; + } else if ((effective > 0 && stepsMod === 1) || (effective < 0 && stepsMod === 3)) { + modifier = ''; + } else { + modifier = "'"; + } - const uiBase = INTERNAL_TO_UI[mapping.base] || mapping.base; - return uiBase + modifier; + const uiBase = INTERNAL_TO_UI[mapping.base] || mapping.base; + return uiBase + modifier; }; // Coerce rotation step count to match a desired sign direction export const coerceStepsToSign = (steps, sign) => { - if (steps === 0) return 0; - const mod = ((steps % 4) + 4) % 4; - if (sign < 0) { - if (mod === 1) return -3; - if (mod === 2) return -2; - return -1; - } - if (mod === 1) return 1; - if (mod === 2) return 2; - return 3; + if (steps === 0) return 0; + const mod = ((steps % 4) + 4) % 4; + if (sign < 0) { + if (mod === 1) return -3; + if (mod === 2) return -2; + return -1; + } + if (mod === 1) return 1; + if (mod === 2) return 2; + return 3; }; // Format a move label from a display base and step count export const formatMoveLabel = (displayBase, steps) => { - const stepsMod = ((steps % 4) + 4) % 4; - if (stepsMod === 0) return displayBase; - let modifier = ""; - if (stepsMod === 1) modifier = "'"; - else if (stepsMod === 2) modifier = "2"; - else if (stepsMod === 3) modifier = ""; - return displayBase + (modifier === "'" ? "'" : modifier === "2" ? "2" : ""); + const stepsMod = ((steps % 4) + 4) % 4; + if (stepsMod === 0) return ''; + let modifier = ""; + if (stepsMod === 1) modifier = "'"; + else if (stepsMod === 2) modifier = "2"; + else if (stepsMod === 3) modifier = ""; + return displayBase + (modifier === "'" ? "'" : modifier === "2" ? "2" : ""); }; diff --git a/src/utils/tokenReducer.js b/src/utils/tokenReducer.js new file mode 100644 index 0000000..2f00f11 --- /dev/null +++ b/src/utils/tokenReducer.js @@ -0,0 +1,36 @@ +// Reduces consecutive same-face moves into their net rotation. +// Agnostic to move names — works with any single-letter move notation. + +const EMPTY = 'E'; +const MODS = [EMPTY, '', '2', "'"]; + +const reduceGroup = (group) => { + const sum = group.reduce((acc, curr) => acc + MODS.indexOf(curr.mod), 0); + const mod = MODS[sum % 4]; + return mod === EMPTY ? '' : `${group[0].name}${mod}`; +}; + +export const parseToken = (token) => { + const match = token.match(/^(\w)(.?)$/); + if (!match) return null; + return { token, name: match[1], mod: match[2] }; +}; + +export const tokenReducer = (tokens) => { + const parsed = tokens.map(parseToken).filter(Boolean); + const desc = []; + const res = []; + let lastPos = 0; + + for (let i = 0; i <= parsed.length; i++) { + if (i === parsed.length || (i > lastPos && parsed[i].name !== parsed[lastPos].name)) { + const group = parsed.slice(lastPos, i); + const reduced = reduceGroup(group); + desc.push({ reduced, group }); + if (reduced !== '') res.push(reduced); + lastPos = i; + } + } + + return { desc, tokens: res }; +}; diff --git a/test/cube_integrity.test.js b/test/cube_integrity.test.js deleted file mode 100644 index c998335..0000000 --- a/test/cube_integrity.test.js +++ /dev/null @@ -1,166 +0,0 @@ -import { Cube, FACES, COLORS } from "../src/utils/Cube.js"; -import assert from "assert"; - -console.log("Running Cube Integrity Tests..."); - -const cube = new Cube(); - -// Helper: Count colors on all faces -const countColors = () => { - const counts = { - [COLORS.WHITE]: 0, - [COLORS.YELLOW]: 0, - [COLORS.ORANGE]: 0, - [COLORS.RED]: 0, - [COLORS.GREEN]: 0, - [COLORS.BLUE]: 0, - [COLORS.BLACK]: 0, // Should be ignored or internal - }; - - cube.cubies.forEach((cubie) => { - Object.values(cubie.faces).forEach((color) => { - if (counts[color] !== undefined) { - counts[color]++; - } - }); - }); - - return counts; -}; - -// Helper: Verify solved state counts -const verifyCounts = (counts) => { - // Each face has 9 stickers. 6 faces. - // 9 * 6 = 54 colored stickers. - // 27 cubies * 6 faces = 162 total faces. - // 162 - 54 = 108 black faces (internal). - - assert.strictEqual(counts[COLORS.WHITE], 9, "White count should be 9"); - assert.strictEqual(counts[COLORS.YELLOW], 9, "Yellow count should be 9"); - assert.strictEqual(counts[COLORS.ORANGE], 9, "Orange count should be 9"); - assert.strictEqual(counts[COLORS.RED], 9, "Red count should be 9"); - assert.strictEqual(counts[COLORS.GREEN], 9, "Green count should be 9"); - assert.strictEqual(counts[COLORS.BLUE], 9, "Blue count should be 9"); -}; - -// Helper: Verify piece integrity -// Corners: 8 corners, each has 3 colors. -// Edges: 12 edges, each has 2 colors. -// Centers: 6 centers, each has 1 color. -// Core: 1 core, 0 colors. -const verifyPieceTypes = () => { - let corners = 0; - let edges = 0; - let centers = 0; - let cores = 0; - - cube.cubies.forEach((cubie) => { - const coloredFaces = Object.values(cubie.faces).filter( - (c) => c !== COLORS.BLACK, - ).length; - if (coloredFaces === 3) corners++; - else if (coloredFaces === 2) edges++; - else if (coloredFaces === 1) centers++; - else if (coloredFaces === 0) cores++; - else - assert.fail( - `Invalid cubie with ${coloredFaces} colors at (${cubie.x},${cubie.y},${cubie.z})`, - ); - }); - - assert.strictEqual(corners, 8, "Should have 8 corners"); - assert.strictEqual(edges, 12, "Should have 12 edges"); - assert.strictEqual(centers, 6, "Should have 6 centers"); - assert.strictEqual(cores, 1, "Should have 1 core"); -}; - -// Helper: Verify specific relative positions of centers (they never change relative to each other) -// Up (White) opposite Down (Yellow) -// Front (Green) opposite Back (Blue) -// Left (Orange) opposite Right (Red) -const verifyCenters = () => { - const centers = cube.cubies.filter( - (c) => - Object.values(c.faces).filter((f) => f !== COLORS.BLACK).length === 1, - ); - - // Find center by color - const findCenter = (color) => - centers.find((c) => Object.values(c.faces).includes(color)); - - const white = findCenter(COLORS.WHITE); - const yellow = findCenter(COLORS.YELLOW); - const green = findCenter(COLORS.GREEN); - const blue = findCenter(COLORS.BLUE); - const orange = findCenter(COLORS.ORANGE); - const red = findCenter(COLORS.RED); - - // Check opposites - // Distance between opposites should be 2 (e.g. y=1 and y=-1) - // And they should be on same axis - - // Note: After rotations, x/y/z coordinates change. - // But relative vectors should hold? - // Actually, centers DO rotate around the core. - // But White is always opposite Yellow. - // So vector(White) + vector(Yellow) == (0,0,0). - - const checkOpposite = (c1, c2, name) => { - assert.strictEqual(c1.x + c2.x, 0, `${name} X mismatch`); - assert.strictEqual(c1.y + c2.y, 0, `${name} Y mismatch`); - assert.strictEqual(c1.z + c2.z, 0, `${name} Z mismatch`); - }; - - checkOpposite(white, yellow, "White-Yellow"); - checkOpposite(green, blue, "Green-Blue"); - checkOpposite(orange, red, "Orange-Red"); -}; - -// --- Test Execution --- - -// 1. Initial State -console.log("Test 1: Initial State Integrity"); -verifyCounts(countColors()); -verifyPieceTypes(); -verifyCenters(); -console.log("PASS Initial State"); - -// 2. Single Rotation (R) -console.log("Test 2: Single Rotation (R)"); -cube.rotateLayer("x", 1, -1); // R -verifyCounts(countColors()); -verifyPieceTypes(); -verifyCenters(); -console.log("PASS Single Rotation"); - -// 3. Multiple Rotations (R U R' U') -console.log("Test 3: Sexy Move (R U R' U')"); -cube.reset(); -cube.move("R"); -cube.move("U"); -cube.move("R'"); -cube.move("U'"); -verifyCounts(countColors()); -verifyPieceTypes(); -verifyCenters(); -console.log("PASS Sexy Move"); - -// 4. Random Rotations (Fuzzing) -console.log("Test 4: 100 Random Moves"); -cube.reset(); -const axes = ["x", "y", "z"]; -const indices = [-1, 0, 1]; -const dirs = [1, -1]; - -for (let i = 0; i < 100; i++) { - const axis = axes[Math.floor(Math.random() * axes.length)]; - const index = indices[Math.floor(Math.random() * indices.length)]; - const dir = dirs[Math.floor(Math.random() * dirs.length)]; - cube.rotateLayer(axis, index, dir); -} -verifyCounts(countColors()); -verifyPieceTypes(); -verifyCenters(); -console.log("PASS 100 Random Moves"); - -console.log("ALL INTEGRITY TESTS PASSED"); diff --git a/test/cube_logic.test.js b/test/cube_logic.test.js deleted file mode 100644 index 66f5fb3..0000000 --- a/test/cube_logic.test.js +++ /dev/null @@ -1,117 +0,0 @@ -import { Cube, FACES, COLORS } from "../src/utils/Cube.js"; -import assert from "assert"; - -console.log("Running Cube Logic Tests..."); - -const cube = new Cube(); - -// Helper to check a specific face color at a position -const checkFace = (x, y, z, face, expectedColor, message) => { - const cubie = cube.cubies.find((c) => c.x === x && c.y === y && c.z === z); - if (!cubie) { - console.error(`Cubie not found at ${x}, ${y}, ${z}`); - return false; - } - const color = cubie.faces[face]; - if (color !== expectedColor) { - console.error( - `FAIL: ${message}. Expected ${expectedColor} at ${face} of (${x},${y},${z}), got ${color}`, - ); - return false; - } - return true; -}; - -// Test 1: Initial State -console.log("Test 1: Initial State"); -// Top-Front-Right corner (1, 1, 1) should have Up=White, Front=Green, Right=Red -checkFace(1, 1, 1, FACES.UP, COLORS.WHITE, "Initial Top-Right-Front UP"); -checkFace(1, 1, 1, FACES.FRONT, COLORS.GREEN, "Initial Top-Right-Front FRONT"); -checkFace(1, 1, 1, FACES.RIGHT, COLORS.RED, "Initial Top-Right-Front RIGHT"); - -// Test 2: Rotate Right Face (R) -> Axis X, index 1, direction -1 (based on previous mapping) -// Wait, let's test `rotateLayer` directly first with axis 'x'. -// Axis X Positive Rotation (direction 1). -// Up (y=1) -> Front (z=1). -// The cubie at (1, 1, 1) (Top-Front-Right) -// Should move to (1, 0, 1)? No. -// (x, y, z) -> (x, -z, y). -// (1, 1, 1) -> (1, -1, 1). (Bottom-Front-Right). -// Let's trace the color. -// The White color was on UP. -// The cubie moves to Bottom-Front. -// The UP face of the cubie now points FRONT. -// So the cubie at (1, -1, 1) should have FRONT = WHITE. - -console.log("Test 2: Rotate X Axis +90 (Right Layer)"); -cube.rotateLayer("x", 1, 1); - -// Cubie originally at (1, 1, 1) [White Up] moves to (1, -1, 1). -// Check (1, -1, 1). -// Its Front face should be White. -const result1 = checkFace( - 1, - -1, - 1, - FACES.FRONT, - COLORS.WHITE, - "After X+90: Old Up(White) should be on Front", -); - -// Cubie originally at (1, 1, -1) [Blue Back, White Up] (Top-Back-Right) -// (1, 1, -1) -> (1, 1, 1). (Top-Front-Right). -// Wait. ny = -z = -(-1) = 1. nz = y = 1. -// So Top-Back moves to Top-Front. -// Its UP face (White) moves to FRONT? -// No. The rotation is around X. -// Top-Back (y=1, z=-1). -// Rot +90 X: y->z, z->-y ? No. -// ny = -z = 1. nz = y = 1. -// New pos: (1, 1, 1). -// The cubie moves from Top-Back to Top-Front. -// Its Up face (White) stays Up? -// No, the cubie rotates. -// Up face rotates to Front? -// Rotation around X axis. -// Top (Y+) rotates to Front (Z+)? -// Yes. -// So the cubie at (1, 1, 1) (new position) should have FRONT = WHITE. -const result2 = checkFace( - 1, - 1, - 1, - FACES.FRONT, - COLORS.WHITE, - "After X+90: Old Top-Back Up(White) should be on Front", -); - -if (result1 && result2) { - console.log("PASS: X Axis Rotation Logic seems correct (if fixed)"); -} else { - console.log("FAIL: X Axis Rotation Logic is broken"); -} - -// Reset for Y test -cube.reset(); -console.log("Test 3: Rotate Y Axis +90 (Top Layer)"); -// Top Layer (y=1). -// Rotate Y+ (direction 1). -// Front (z=1) -> Right (x=1). -// Cubie at (0, 1, 1) (Front-Top-Center) [Green Front, White Up]. -// Moves to (1, 1, 0) (Right-Top-Center). -// Its Front Face (Green) should move to Right Face. -cube.rotateLayer("y", 1, 1); -const resultY = checkFace( - 1, - 1, - 0, - FACES.RIGHT, - COLORS.GREEN, - "After Y+90: Old Front(Green) should be on Right", -); - -if (resultY) { - console.log("PASS: Y Axis Rotation Logic seems correct"); -} else { - console.log("FAIL: Y Axis Rotation Logic is broken"); -} diff --git a/test/cube_matrix.test.js b/test/cube_matrix.test.js deleted file mode 100644 index 9bb9d38..0000000 --- a/test/cube_matrix.test.js +++ /dev/null @@ -1,107 +0,0 @@ -import { Cube, FACES, COLORS } from "../src/utils/Cube.js"; -import assert from "assert"; - -console.log("Running Cube Matrix Rotation Tests..."); - -const cube = new Cube(); - -// Helper to check position and face -const checkCubie = (origX, origY, origZ, newX, newY, newZ, faceCheck) => { - const cubie = cube.cubies.find( - (c) => c.x === newX && c.y === newY && c.z === newZ, - ); - if (!cubie) { - console.error(`FAIL: Cubie not found at ${newX}, ${newY}, ${newZ}`); - return false; - } - - // Verify it's the correct original cubie (tracking ID would be better, but position logic is enough if unique) - // Let's assume we track a specific cubie. - return true; -}; - -// Test 1: Z-Axis Rotation (Front Face) -// Front Face is z=1. -// Top-Left (x=-1, y=1) -> Top-Right (x=1, y=1)? -// Physical CW (Z-Axis): Up -> Right. -// Top-Middle (0, 1) -> Right-Middle (1, 0). -console.log("Test 1: Z-Axis CW (Front)"); -cube.reset(); -// Find Top-Middle of Front Face: (0, 1, 1). White Up, Green Front. -const topMid = cube.cubies.find((c) => c.x === 0 && c.y === 1 && c.z === 1); -assert.strictEqual(topMid.faces[FACES.UP], COLORS.WHITE); -assert.strictEqual(topMid.faces[FACES.FRONT], COLORS.GREEN); - -cube.rotateLayer("z", 1, -1); // CW (direction -1 in move(), but rotateLayer takes direction. Standard move F is direction -1?) -// move('F') calls rotateLayer('z', 1, -1). -// So let's test rotateLayer('z', 1, -1). - -// Expect: (0, 1, 1) -> (1, 0, 1). (Right-Middle of Front). -// Faces: Old Up (White) becomes Right? -// Z-Axis CW: Up -> Right. -// So new pos should have Right=White. -// Old Front (Green) stays Front. -const newPos = cube.cubies.find((c) => c.id === topMid.id); -console.log(`Moved to: (${newPos.x}, ${newPos.y}, ${newPos.z})`); -assert.strictEqual(newPos.x, 1); -assert.strictEqual(newPos.y, 0); -assert.strictEqual(newPos.z, 1); -assert.strictEqual(newPos.faces[FACES.RIGHT], COLORS.WHITE); -assert.strictEqual(newPos.faces[FACES.FRONT], COLORS.GREEN); -console.log("PASS Z-Axis CW"); - -// Test 2: X-Axis Rotation (Right Face) -// Right Face is x=1. -// Top-Front (1, 1, 1) -> Top-Back (1, 1, -1)? -// Physical CW (X-Axis): Up -> Front. -// Top-Middle (1, 1, 0) -> Front-Middle (1, 0, 1). -console.log("Test 2: X-Axis CW (Right)"); -cube.reset(); -// Find Top-Middle of Right Face: (1, 1, 0). White Up, Red Right. -const rightTop = cube.cubies.find((c) => c.x === 1 && c.y === 1 && c.z === 0); - -cube.rotateLayer("x", 1, -1); // CW (direction -1 for R in move()?) -// move('R') calls rotateLayer('x', 1, -1). -// So let's test -1. - -// Expect: (1, 1, 0) -> (1, 0, -1). -// Faces: Old Up (White) becomes Back? -// X-Axis CW (Right Face): Up -> Back. -// So new pos should have Back=White. -// Old Right (Red) stays Right. -const newRightPos = cube.cubies.find((c) => c.id === rightTop.id); -console.log(`Moved to: (${newRightPos.x}, ${newRightPos.y}, ${newRightPos.z})`); -assert.strictEqual(newRightPos.x, 1); -assert.strictEqual(newRightPos.y, 0); -assert.strictEqual(newRightPos.z, -1); -assert.strictEqual(newRightPos.faces[FACES.BACK], COLORS.WHITE); -assert.strictEqual(newRightPos.faces[FACES.RIGHT], COLORS.RED); -console.log("PASS X-Axis CW"); - -// Test 3: Y-Axis Rotation (Up Face) -// Up Face is y=1. -// Front-Middle (0, 1, 1) -> Left-Middle (-1, 1, 0). -// Physical CW (Y-Axis): Front -> Left. -// Wait. move('U') calls rotateLayer('y', 1, -1). -// Standard U is CW. Y-Axis direction? -// move('U'): dir = -1. -console.log("Test 3: Y-Axis CW (Up)"); -cube.reset(); -// Find Front-Middle of Up Face: (0, 1, 1). Green Front, White Up. -const upFront = cube.cubies.find((c) => c.x === 0 && c.y === 1 && c.z === 1); - -cube.rotateLayer("y", 1, -1); // CW (direction -1). - -// Expect: (0, 1, 1) -> (-1, 1, 0). (Left-Middle). -// Faces: Old Front (Green) becomes Left? -// Y-Axis CW (U): Front -> Left. -// So new pos should have Left=Green. -// Old Up (White) stays Up. -const newUpPos = cube.cubies.find((c) => c.id === upFront.id); -console.log(`Moved to: (${newUpPos.x}, ${newUpPos.y}, ${newUpPos.z})`); -assert.strictEqual(newUpPos.x, -1); -assert.strictEqual(newUpPos.y, 1); -assert.strictEqual(newUpPos.z, 0); -assert.strictEqual(newUpPos.faces[FACES.LEFT], COLORS.GREEN); -assert.strictEqual(newUpPos.faces[FACES.UP], COLORS.WHITE); -console.log("PASS Y-Axis CW"); diff --git a/test/tokenReducer.test.js b/test/tokenReducer.test.js new file mode 100644 index 0000000..1b6c780 --- /dev/null +++ b/test/tokenReducer.test.js @@ -0,0 +1,70 @@ +import { describe, it, expect } from 'vitest'; +import { tokenReducer, parseToken } from '../src/utils/tokenReducer.js'; + +describe('parseToken', () => { + it('parses simple move', () => { + expect(parseToken('D')).toEqual({ token: 'D', name: 'D', mod: '' }); + }); + + it('parses prime move', () => { + expect(parseToken("U'")).toEqual({ token: "U'", name: 'U', mod: "'" }); + }); + + it('parses double move', () => { + expect(parseToken('R2')).toEqual({ token: 'R2', name: 'R', mod: '2' }); + }); +}); + +describe('tokenReducer', () => { + it('user example: mixed faces', () => { + const result = tokenReducer(['D', 'U2', 'U2', 'B2', "B'", 'B2', "U'", 'U2']); + expect(result.tokens).toEqual(['D', "B'", 'U']); + }); + + it('cancellation: same move 4 times = identity', () => { + expect(tokenReducer(['R', 'R', 'R', 'R']).tokens).toEqual([]); + }); + + it('cancellation: move + inverse = identity', () => { + expect(tokenReducer(["F'", 'F']).tokens).toEqual([]); + }); + + it('cancellation: double move twice = identity', () => { + expect(tokenReducer(['D2', 'D2']).tokens).toEqual([]); + }); + + it('merge: move + move = double', () => { + expect(tokenReducer(['U', 'U']).tokens).toEqual(['U2']); + }); + + it('merge: double + move = prime', () => { + expect(tokenReducer(['R2', 'R']).tokens).toEqual(["R'"]); + }); + + it('D2 D2 D\' D cancels to empty', () => { + expect(tokenReducer(['D2', 'D2', "D'", 'D']).tokens).toEqual([]); + }); + + it('preserves non-adjacent different faces', () => { + expect(tokenReducer(['R', 'U', 'R']).tokens).toEqual(['R', 'U', 'R']); + }); + + it('reduces only consecutive same-face groups', () => { + expect(tokenReducer(['F', 'F', 'U', "U'"]).tokens).toEqual(['F2']); + }); + + it('handles single move unchanged', () => { + expect(tokenReducer(['B']).tokens).toEqual(['B']); + }); + + it('handles empty input', () => { + expect(tokenReducer([]).tokens).toEqual([]); + }); + + it('desc contains group info', () => { + const result = tokenReducer(['R', 'R']); + expect(result.desc).toHaveLength(1); + expect(result.desc[0].reduced).toBe('R2'); + expect(result.desc[0].group).toHaveLength(2); + }); +});