31 Commits
v0.3.1 ... main

Author SHA1 Message Date
a39ebd606f Update src/components/Footer.vue
All checks were successful
Deploy to Production / deploy (push) Successful in 6s
2026-02-26 22:28:18 +00:00
53fb65091c Update src/components/Footer.vue
All checks were successful
Deploy to Production / deploy (push) Successful in 15s
2026-02-26 22:27:18 +00:00
04c6934ad8 0.6.1
All checks were successful
Deploy to Production / deploy (push) Successful in 15s
2026-02-24 18:05:35 +00:00
97157ddb7a feat: 3-state projection toggle, English UI titles, fix simple mode 2026-02-24 18:05:32 +00:00
4be710a69f 0.6.0
All checks were successful
Deploy to Production / deploy (push) Successful in 16s
2026-02-24 17:20:20 +00:00
3bd919a1cf feat: refactor utils, add tokenReducer, rear face projections with toggle 2026-02-24 17:20:17 +00:00
d82eef86f9 0.5.5
All checks were successful
Deploy to Production / deploy (push) Successful in 14s
2026-02-24 16:42:22 +00:00
54abcf3414 feat: add tokenReducer, vitest tests, fix merge label convention 2026-02-24 16:42:19 +00:00
68e163270e 0.5.4
All checks were successful
Deploy to Production / deploy (push) Successful in 14s
2026-02-24 13:40:23 +00:00
9b02b1d9d6 refactor: extract matrix, moveMapping, easing, cubeProjection utils from SmartCube 2026-02-24 13:40:20 +00:00
b71594d0ab 0.5.3
All checks were successful
Deploy to Production / deploy (push) Successful in 14s
2026-02-24 13:13:31 +00:00
94e1cb7ed3 feat: camera reset button with SLERP, fix drag labels and solver mapping 2026-02-24 13:13:28 +00:00
fd090c6960 0.5.2 2026-02-24 12:46:24 +00:00
fccc43d0eb fix: resolve middle slice state sync and zero-step drag freeze
All checks were successful
Deploy to Production / deploy (push) Successful in 6s
2026-02-24 12:46:08 +00:00
8a20531fa0 feat: separate solver logic into dedicated web worker, improve toast notifications
All checks were successful
Deploy to Production / deploy (push) Successful in 21s
2026-02-24 09:56:16 +00:00
dc95b07188 fix: remove native devDependencies causing production build failures
All checks were successful
Deploy to Production / deploy (push) Successful in 13s
2026-02-24 00:10:00 +00:00
281614502e style: use default toastify css look for solver notifications
Some checks failed
Deploy to Production / deploy (push) Failing after 9s
2026-02-24 00:03:24 +00:00
4ab408b329 style: remove focus outline from solver dropdown buttons 2026-02-23 23:55:36 +00:00
c7d369c46a feat: integrate toastify-js and add solve guard with solved check
Some checks failed
Deploy to Production / deploy (push) Failing after 10s
2026-02-23 23:53:47 +00:00
349e74d7ef docs: overwrite README with project description and centered preview image
All checks were successful
Deploy to Production / deploy (push) Successful in 5s
2026-02-23 23:26:56 +00:00
6089e6f961 0.5.0
All checks were successful
Deploy to Production / deploy (push) Successful in 15s
2026-02-23 22:04:49 +00:00
e5befab473 fix(solver): replace exponential IDDFS recursion with instantaneous heuristic simulation macros 2026-02-23 22:04:41 +00:00
929761ac9e feat: reposition solver controls to a dropdown
Moved the Kociemba/Beginner solve options into a sleek dropdown menu positioned above the Scramble button on the left side of the screen. This ensures the solver controls no longer obstruct the programmatic move queue at the bottom.
2026-02-23 21:49:21 +00:00
f6b34449df 0.4.2
All checks were successful
Deploy to Production / deploy (push) Successful in 10s
2026-02-23 19:42:40 +00:00
21e3465be9 fix(ui): make programmatic moveQueue reactive to immediately reflect intercepted changes like FFF towards F' 2026-02-23 19:42:19 +00:00
ce4a183090 Disable copy/reset actions when move queue is empty 2026-02-23 17:28:33 +00:00
bc7ae67412 Refactor SmartCube controls and move history into separate components 2026-02-23 17:25:59 +00:00
a49ca8f98e 0.4.1
All checks were successful
Deploy to Production / deploy (push) Successful in 8s
2026-02-23 01:14:19 +00:00
afac47c634 chore: adjust panel background for modal 2026-02-23 01:14:10 +00:00
31015366be 0.4.0
All checks were successful
Deploy to Production / deploy (push) Successful in 9s
2026-02-23 01:09:36 +00:00
880d46be1c chore: tweak add-moves modal layout 2026-02-23 01:09:10 +00:00
59 changed files with 8885 additions and 1451 deletions

1
.gitignore vendored
View File

@@ -24,3 +24,4 @@ dist-ssr
*.sw?
.agent/
cache/

View File

@@ -1,31 +1,33 @@
# Vue 3 + Vite
# Rubik's Cube Logic Engine & Simulator
This template should help get you started developing with Vue 3 in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
<div align="center">
<img src="./public/preview.png" alt="Cube Preview" width="600" />
</div>
Learn more about IDE Support for Vue in the [Vue Docs Scaling up Guide](https://vuejs.org/guide/scaling-up/tooling.html#ide-support).
## Overview
## Uruchamianie (Automatyczne - CI/CD)
This application is a 3D animated, Interactive Rubik's Cube simulator and mathematical solver built from scratch using Vue.js. It aims to provide seamless mechanical interactions and mathematically perfectly rigorous tracking of a classic 3x3 Rubik's Cube state.
Projekt wykorzystuje **Gitea Actions** z runnerem **self-hosted** na serwerze produkcyjnym.
Każdy push do gałęzi `main` automatycznie:
1. Pobiera kod na serwerze.
2. Zatrzymuje i usuwa stare kontenery.
3. Buduje i uruchamia nową wersję aplikacji przy użyciu `docker compose up -d --build`.
The software operates entirely in the browser using a custom Group Theory mathematical engine (`DeepCube.js`), which separates the heavy analytical permutation tracking from the 3D CSS visual layer using Web Workers.
### Konfiguracja Sieci i Bezpieczeństwa (Izolacja)
## Features
Aplikacja wykorzystuje dwie sieci dockerowe dla zapewnienia izolacji:
1. `npm_public` (zewnętrzna): Sieć, w której znajduje się Nginx Proxy Manager. Tylko kontener `rubic-cube` jest do niej podłączony, aby NPM mógł przekierować ruch.
2. `rubic-net` (wewnętrzna): Prywatna sieć aplikacji. Wszelkie inne serwisy (np. baza danych, redis - jeśli dodasz w przyszłości) powinny być tylko w tej sieci, niewidoczne dla NPM ani innych aplikacji.
- **Mechanical Realism:** 3D CSS rendering precisely models physical cube mechanics. Dragging edge and corner pieces rotates the specific mechanical layer dynamically, while dragging the center elements pivots the entire camera view.
- **Reactive Algorithm Queue:** Execute complex algorithms fluidly. The dynamic queue evaluates incoming inputs and instantly intercepts redundances (e.g. evaluating `U U` into a single `U2` animation, or cancelling out `F` into `F'` on the fly).
- **Deep Mathematical Engine:** Based entirely on Group Theory. It stores corner and edge permutation arrays combined with spatial orientation parities to guarantee that only physically legal mechanical states exist or can be scrambled.
- **Intelligent Solvers:**
- **Beginner Method (Human):** Constructs the solution layer-by-layer simulating human heuristics natively with instantaneous $O(1)$ algorithmic macros.
- **Kociemba's Algorithm (Optimal):** Offloads pruning tables and recursive heuristic searches to Web Workers to instantly calculate and stream back the objectively shortest path solution (typically <20 moves).
- **High Performance:** Decoupling the single-threaded UI rendering stack from mathematical validations ensures 60 FPS 3D animations, even while executing computationally expensive analytical algorithms in the background.
## Development & Asset Generation
To keep the production build lightweight and avoid native dependency issues on servers (e.g. Docker), heavy packages like `canvas`, `puppeteer`, and `imagetracerjs` have been removed from the default `devDependencies`.
If you need to run the auxiliary scripts in `scripts/` (for screenshotting or regenerating `cube.svg`), you must install them manually:
**Wymagania:**
Przed uruchomieniem upewnij się, że na serwerze istnieje sieć publiczna dla proxy:
```bash
docker network create npm_public
npm install -D canvas puppeteer imagetracerjs
```
(Jeśli Twój Nginx Proxy Manager używa innej sieci, zaktualizuj nazwę w `docker-compose.yml`).
W panelu Nginx Proxy Manager skonfiguruj:
* **Network:** `npm_public` (lub odpowiednia sieć proxy).
* **Forward Hostname:** `rubic-cube`
* **Forward Port:** `80`
These are only required for offline asset optimization and are not needed to build or run the main application.

View File

@@ -1,13 +1,31 @@
<!doctype html>
<html lang="en">
<head>
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/src/assets/rubic-cube.svg" />
<link rel="icon" type="image/png" href="/favicon.png" sizes="32x32" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Rubic Cube</title>
</head>
<body>
<title>Rubic Cube Logic Engine</title>
<meta name="description"
content="A 3D interactive Rubik's Cube simulator and solver powered by a custom mathematical engine." />
<!-- Open Graph (for social media sharing like Facebook/Discord) -->
<meta property="og:title" content="Rubic Cube Logic Engine" />
<meta property="og:description"
content="A 3D interactive Rubik's Cube simulator and solver powered by a custom mathematical engine." />
<meta property="og:image" content="/preview.png" />
<meta property="og:type" content="website" />
<!-- Twitter Cards -->
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="Rubic Cube Logic Engine" />
<meta name="twitter:description"
content="A 3D interactive Rubik's Cube simulator and solver powered by a custom mathematical engine." />
<meta name="twitter:image" content="/preview.png" />
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</body>
</html>

4786
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,20 +1,23 @@
{
"name": "rubic-cube",
"private": true,
"version": "0.3.1",
"version": "0.6.1",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
"preview": "vite preview",
"test": "vitest run"
},
"dependencies": {
"cubejs": "^1.3.2",
"lucide-vue-next": "^0.564.0",
"rubiks-js": "^1.0.0",
"toastify-js": "^1.12.0",
"vue": "^3.5.13"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.2.1",
"vite": "^6.2.0"
"vite": "^6.2.0",
"vitest": "^4.0.18"
}
}

BIN
public/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 915 B

BIN
public/preview.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 160 KiB

View File

@@ -1,7 +1,7 @@
<script setup>
import SmartCube from './components/renderers/SmartCube.vue'
import NavBar from './components/NavBar.vue'
import Footer from './components/Footer.vue'
import SmartCube from "./components/renderers/SmartCube.vue";
import NavBar from "./components/NavBar.vue";
import Footer from "./components/Footer.vue";
</script>
<template>

View File

@@ -1,55 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
<defs>
<style>
.face { stroke: #000; stroke-width: 6; stroke-linejoin: round; }
.top { fill: #ffffff; }
.left { fill: #009e60; }
.right { fill: #c41e3a; }
</style>
</defs>
<!-- Top Face -->
<g class="face top">
<path d="M256 80 L309.33 106.67 L256 133.33 L202.67 106.67 Z" />
<path d="M309.33 106.67 L362.67 133.33 L309.33 160 L256 133.33 Z" />
<path d="M362.67 133.33 L416 160 L362.67 186.67 L309.33 160 Z" />
<path d="M202.67 106.67 L256 133.33 L202.67 160 L149.33 133.33 Z" />
<path d="M256 133.33 L309.33 160 L256 186.67 L202.67 160 Z" />
<path d="M309.33 160 L362.67 186.67 L309.33 213.33 L256 186.67 Z" />
<path d="M149.33 133.33 L202.67 160 L149.33 186.67 L96 160 Z" />
<path d="M202.67 160 L256 186.67 L202.67 213.33 L149.33 186.67 Z" />
<path d="M256 186.67 L309.33 213.33 L256 240 L202.67 213.33 Z" />
</g>
<!-- Left Face -->
<g class="face left">
<path d="M96 160 L149.33 186.67 L149.33 248 L96 221.33 Z" />
<path d="M149.33 186.67 L202.67 213.33 L202.67 274.67 L149.33 248 Z" />
<path d="M202.67 213.33 L256 240 L256 301.33 L202.67 274.67 Z" />
<path d="M96 221.33 L149.33 248 L149.33 309.33 L96 282.67 Z" />
<path d="M149.33 248 L202.67 274.67 L202.67 336 L149.33 309.33 Z" />
<path d="M202.67 274.67 L256 301.33 L256 362.67 L202.67 336 Z" />
<path d="M96 282.67 L149.33 309.33 L149.33 370.67 L96 344 Z" />
<path d="M149.33 309.33 L202.67 336 L202.67 397.33 L149.33 370.67 Z" />
<path d="M202.67 336 L256 362.67 L256 424 L202.67 397.33 Z" />
</g>
<!-- Right Face -->
<g class="face right">
<path d="M256 240 L309.33 213.33 L309.33 274.67 L256 301.33 Z" />
<path d="M309.33 213.33 L362.67 186.67 L362.67 248 L309.33 274.67 Z" />
<path d="M362.67 186.67 L416 160 L416 221.33 L362.67 248 Z" />
<path d="M256 301.33 L309.33 274.67 L309.33 336 L256 362.67 Z" />
<path d="M309.33 274.67 L362.67 248 L362.67 309.33 L309.33 336 Z" />
<path d="M362.67 248 L416 221.33 L416 282.67 L362.67 309.33 Z" />
<path d="M256 362.67 L309.33 336 L309.33 397.33 L256 424 Z" />
<path d="M309.33 336 L362.67 309.33 L362.67 370.67 L309.33 397.33 Z" />
<path d="M362.67 309.33 L416 282.67 L416 344 L362.67 370.67 Z" />
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.3 KiB

View File

@@ -6,7 +6,7 @@ const version = __APP_VERSION__;
<template>
<footer class="app-footer glass-panel">
<div class="footer-content">
<p>&copy; {{ currentYear }} Rubic Cube. Wersja {{ version }}</p>
<p>&copy; {{ currentYear }} Rubic Cube. v{{ version }}</p>
</div>
</footer>
</template>

View File

@@ -1,26 +1,32 @@
<script setup>
import { Sun, Moon, Grid2x2 } from 'lucide-vue-next';
import { ref, onMounted } from 'vue';
import { useSettings } from '../composables/useSettings';
import { Sun, Moon, Grid2x2, Layers, Layers2, LayersPlus } from "lucide-vue-next";
import { ref, onMounted, computed } from "vue";
import { useSettings } from "../composables/useSettings";
const { isCubeTranslucent, toggleCubeTranslucent } = useSettings();
const { isCubeTranslucent, toggleCubeTranslucent, projectionMode, cycleProjectionMode } = useSettings();
const isDark = ref(true);
const setTheme = (dark) => {
isDark.value = dark;
const theme = dark ? 'dark' : 'light';
const theme = dark ? "dark" : "light";
document.documentElement.dataset.theme = theme;
localStorage.setItem('theme', theme);
localStorage.setItem("theme", theme);
};
const toggleTheme = () => {
setTheme(!isDark.value);
};
const projectionTitle = computed(() => {
if (projectionMode.value === 0) return 'Show rear face projections';
if (projectionMode.value === 1) return 'Enable animated projections';
return 'Disable projections';
});
onMounted(() => {
const savedTheme = localStorage.getItem('theme');
const savedTheme = localStorage.getItem("theme");
if (savedTheme) {
setTheme(savedTheme === 'dark');
setTheme(savedTheme === "dark");
} else {
setTheme(true);
}
@@ -38,14 +44,34 @@ onMounted(() => {
<button
class="btn-neon nav-btn icon-only"
@click="toggleCubeTranslucent"
:title="isCubeTranslucent ? 'Wyłącz przezroczystość kostki' : 'Włącz przezroczystość kostki'"
:title="
isCubeTranslucent
? 'Disable cube transparency'
: 'Enable cube transparency'
"
:class="{ active: isCubeTranslucent }"
>
<Grid2x2 :size="20" />
</button>
<!-- Face Projections Toggle (3-state) -->
<button
class="btn-neon nav-btn icon-only"
@click="cycleProjectionMode"
:title="projectionTitle"
:class="{ active: projectionMode > 0 }"
>
<Layers2 v-if="projectionMode === 0" :size="20" />
<Layers v-else-if="projectionMode === 1" :size="20" />
<LayersPlus v-else :size="20" />
</button>
<!-- Theme Toggle -->
<button class="btn-neon nav-btn icon-only" @click="toggleTheme" :title="isDark ? 'Przełącz na jasny' : 'Przełącz na ciemny'">
<button
class="btn-neon nav-btn icon-only"
@click="toggleTheme"
:title="isDark ? 'Switch to light mode' : 'Switch to dark mode'"
>
<Sun v-if="isDark" :size="20" />
<Moon v-else :size="20" />
</button>

View File

@@ -1,23 +1,23 @@
<script setup>
import { computed } from 'vue';
import { computed } from "vue";
const props = defineProps({
start: {
type: Object,
required: true // {x, y, z}
required: true, // {x, y, z}
},
end: {
type: Object,
required: true // {x, y, z}
required: true, // {x, y, z}
},
color: {
type: String,
default: 'var(--text-color, #fff)'
default: "var(--text-color, #fff)",
},
thickness: {
type: Number,
default: 1
}
default: 1,
},
});
const style = computed(() => {
@@ -43,13 +43,13 @@ const style = computed(() => {
width: `${length}px`,
height: `${props.thickness}px`,
backgroundColor: props.color,
position: 'absolute',
top: '0',
left: '0',
transformOrigin: 'center center',
position: "absolute",
top: "0",
left: "0",
transformOrigin: "center center",
transform: `translate3d(${midX}px, ${midY}px, ${midZ}px) rotateY(${-yaw}rad) rotateZ(${pitch}rad) translate(-50%, -50%)`,
opacity: 0.3, // Delicate
pointerEvents: 'none'
pointerEvents: "none",
};
});
</script>

View File

@@ -0,0 +1,253 @@
<script setup>
import { ref, onMounted, onUnmounted } from "vue";
import { Locate, LocateFixed } from "lucide-vue-next";
const props = defineProps({
isViewDefault: { type: Boolean, default: true },
});
const emit = defineEmits(["move", "scramble", "solve", "reset-camera"]);
const showSolveDropdown = ref(false);
const toggleDropdown = () => {
showSolveDropdown.value = !showSolveDropdown.value;
};
const triggerSolve = (method) => {
showSolveDropdown.value = false;
emit("solve", method);
};
// Close dropdown when clicking outside
const closeDropdown = (e) => {
if (!e.target.closest(".solve-dropdown-wrapper")) {
showSolveDropdown.value = false;
}
};
onMounted(() => {
document.addEventListener("click", closeDropdown);
});
onUnmounted(() => {
document.removeEventListener("click", closeDropdown);
});
</script>
<template>
<div>
<div class="controls controls-left">
<div class="controls-row">
<button class="btn-neon move-btn" @click="emit('move', 'U')">U</button>
<button class="btn-neon move-btn" @click="emit('move', 'D')">D</button>
<button class="btn-neon move-btn" @click="emit('move', 'L')">L</button>
</div>
<div class="controls-row">
<button class="btn-neon move-btn" @click="emit('move', 'U-prime')">
U'
</button>
<button class="btn-neon move-btn" @click="emit('move', 'D-prime')">
D'
</button>
<button class="btn-neon move-btn" @click="emit('move', 'L-prime')">
L'
</button>
</div>
<div class="controls-row">
<button class="btn-neon move-btn" @click="emit('move', 'U2')">
U2
</button>
<button class="btn-neon move-btn" @click="emit('move', 'D2')">
D2
</button>
<button class="btn-neon move-btn" @click="emit('move', 'L2')">
L2
</button>
</div>
</div>
<div class="controls controls-right">
<div class="controls-row">
<button class="btn-neon move-btn" @click="emit('move', 'R')">R</button>
<button class="btn-neon move-btn" @click="emit('move', 'F')">F</button>
<button class="btn-neon move-btn" @click="emit('move', 'B')">B</button>
</div>
<div class="controls-row">
<button class="btn-neon move-btn" @click="emit('move', 'R-prime')">
R'
</button>
<button class="btn-neon move-btn" @click="emit('move', 'F-prime')">
F'
</button>
<button class="btn-neon move-btn" @click="emit('move', 'B-prime')">
B'
</button>
</div>
<div class="controls-row">
<button class="btn-neon move-btn" @click="emit('move', 'R2')">
R2
</button>
<button class="btn-neon move-btn" @click="emit('move', 'F2')">
F2
</button>
<button class="btn-neon move-btn" @click="emit('move', 'B2')">
B2
</button>
</div>
</div>
<div class="bottom-left-controls">
<div class="solve-dropdown-wrapper">
<button class="btn-neon move-btn solve-btn" @click="toggleDropdown">
Solve
</button>
<div v-if="showSolveDropdown" class="solve-dropdown-menu">
<button class="dropdown-item" @click="triggerSolve('kociemba')">
Kociemba (Optimal)
</button>
<button class="dropdown-item" @click="triggerSolve('beginner')">
Beginner (Human)
</button>
</div>
</div>
<button class="btn-neon move-btn scramble-btn" @click="emit('scramble')">
Scramble
</button>
</div>
<div class="bottom-right-controls">
<button
class="btn-neon move-btn camera-reset-btn"
:class="{ 'is-default': props.isViewDefault }"
:disabled="props.isViewDefault"
@click="emit('reset-camera')"
>
<LocateFixed v-if="props.isViewDefault" :size="18" />
<Locate v-else :size="18" />
</button>
</div>
</div>
</template>
<style scoped>
.controls {
position: absolute;
top: 96px;
display: flex;
flex-direction: column;
gap: 8px;
z-index: 50;
}
.controls-left {
left: 24px;
}
.controls-right {
right: 24px;
}
.controls-row {
display: flex;
gap: 8px;
justify-content: center;
}
.move-btn {
min-width: 44px;
height: 36px;
font-size: 0.9rem;
padding: 0 10px;
}
.bottom-left-controls {
position: absolute;
bottom: 72px;
left: 24px;
z-index: 50;
display: flex;
flex-direction: column;
gap: 12px;
align-items: flex-start;
}
.solve-dropdown-wrapper {
position: relative;
}
.solve-dropdown-menu {
position: absolute;
bottom: 100%;
left: 0;
margin-bottom: 8px;
background: rgba(0, 0, 0, 0.8);
backdrop-filter: blur(8px);
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 8px;
padding: 8px;
display: flex;
flex-direction: column;
gap: 4px;
min-width: 180px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5);
animation: fadeIn 0.2s ease-out;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(4px); }
to { opacity: 1; transform: translateY(0); }
}
.dropdown-item {
background: transparent;
color: #fff;
border: none;
padding: 8px 12px;
text-align: left;
border-radius: 4px;
cursor: pointer;
font-family: inherit;
font-size: 0.9rem;
transition: background 0.2s;
}
.dropdown-item:hover {
background: rgba(255, 255, 255, 0.1);
}
.dropdown-item:focus {
outline: none;
}
.bottom-right-controls {
position: absolute;
bottom: 72px;
right: 24px;
z-index: 50;
display: flex;
flex-direction: column;
gap: 12px;
align-items: flex-end;
}
.camera-reset-btn {
min-width: 40px;
height: 40px;
padding: 0;
display: flex;
align-items: center;
justify-content: center;
transition: opacity 0.3s, transform 0.2s;
}
.camera-reset-btn.is-default {
opacity: 0.3;
cursor: default;
pointer-events: none;
}
.camera-reset-btn:not(.is-default):hover {
transform: scale(1.1);
}
</style>

View File

@@ -0,0 +1,192 @@
<script setup>
import { computed } from "vue";
import { REAR_FACE_DISTANCE } from "../../config/settings.js";
const props = defineProps({
cubies: { type: Array, required: true },
viewMatrix: { type: Array, required: true },
FACES: { type: Object, required: true },
SCALE: { type: Number, default: 100 },
activeLayer: { type: Object, default: null },
currentLayerRotation: { type: Number, default: 0 },
animateLayers: { type: Boolean, default: false },
});
// Face definitions with logical normals and grid axes
const FACE_DEFS = computed(() => {
const F = props.FACES;
return [
{ face: F.FRONT, normal: [0, 0, 1], gridU: [1, 0, 0], gridV: [0, 1, 0], faceKey: 'front' },
{ face: F.BACK, normal: [0, 0, -1], gridU: [-1, 0, 0], gridV: [0, 1, 0], faceKey: 'back' },
{ face: F.RIGHT, normal: [1, 0, 0], gridU: [0, 0, -1], gridV: [0, 1, 0], faceKey: 'right' },
{ face: F.LEFT, normal: [-1, 0, 0], gridU: [0, 0, 1], gridV: [0, 1, 0], faceKey: 'left' },
{ face: F.UP, normal: [0, 1, 0], gridU: [1, 0, 0], gridV: [0, 0, -1], faceKey: 'up' },
{ face: F.DOWN, normal: [0, -1, 0], gridU: [1, 0, 0], gridV: [0, 0, 1], faceKey: 'down' },
];
});
// Which faces are hidden (for static cells)
const hiddenFaceKeys = computed(() => {
const m = props.viewMatrix;
const keys = new Set();
for (const fd of FACE_DEFS.value) {
const [nx, ny, nz] = fd.normal;
const tz = nx * m[2] + (-ny) * m[6] + nz * m[10];
if (tz < 0) keys.add(fd.faceKey);
}
return keys;
});
// Orientation: cells face INWARD (toward cube center)
// Combined with backface-visibility: hidden, this means:
// - hidden face cells: front faces camera → visible
// - visible face cells: front faces away → invisible
// - rotating cells crossing the plane: naturally swap visibility
const inwardRotation = (nx, ny, nz) => {
if (nx === 1) return 'rotateY(-90deg)';
if (nx === -1) return 'rotateY(90deg)';
if (ny === 1) return 'rotateX(-90deg)';
if (ny === -1) return 'rotateX(90deg)';
if (nz === -1) return ''; // front faces +Z (toward camera)
return 'rotateY(180deg)'; // nz === 1: flip to face -Z (toward center)
};
// Build cells for one face
const buildFaceCells = (fd, S, dist, al, rot, isRotatingOnly) => {
const [nx, ny, nz] = fd.normal;
const [gu, gv] = [fd.gridU, fd.gridV];
const orient = inwardRotation(nx, ny, nz);
const d = S * 1.5 + dist;
const cells = [];
const faceCubies = props.cubies.filter((c) => {
if (nx !== 0) return c.x === nx;
if (ny !== 0) return c.y === ny;
if (nz !== 0) return c.z === nz;
return false;
});
for (let v = 1; v >= -1; v--) {
for (let u = -1; u <= 1; u++) {
const cx = nx * Math.max(Math.abs(nx), 0) || u * gu[0] + v * gv[0];
const cy = ny * Math.max(Math.abs(ny), 0) || u * gu[1] + v * gv[1];
const cz = nz * Math.max(Math.abs(nz), 0) || u * gu[2] + v * gv[2];
const inLayer = al && (
(al.axis === 'x' && cx === al.index) ||
(al.axis === 'y' && cy === al.index) ||
(al.axis === 'z' && cz === al.index)
);
// Skip: if isRotatingOnly, only include rotating cells
// If not isRotatingOnly (hidden face), include non-rotating cells
if (isRotatingOnly && !inLayer) continue;
if (!isRotatingOnly && inLayer && props.animateLayers) continue;
const cubie = faceCubies.find(
(c) => c.x === cx && c.y === cy && c.z === cz
);
const color = cubie ? cubie.faces[fd.faceKey] || 'black' : 'black';
// 3D position
const posX = nx * d + u * gu[0] * S + v * gv[0] * S;
const posY = ny * d + u * gu[1] * S + v * gv[1] * S;
const posZ = nz * d + u * gu[2] * S + v * gv[2] * S;
let transform = `translate3d(${posX}px, ${-posY}px, ${posZ}px) ${orient}`;
// Rotating cells: prepend rotation around scene center (only in advanced mode)
if (props.animateLayers && inLayer && rot !== 0) {
let rotPre = '';
if (al.axis === 'x') rotPre = `rotateX(${-rot}deg)`;
else if (al.axis === 'y') rotPre = `rotateY(${rot}deg)`;
else if (al.axis === 'z') rotPre = `rotateZ(${-rot}deg)`;
transform = `${rotPre} ${transform}`;
}
cells.push({
key: `${fd.faceKey}-${u}-${v}`,
color,
transform,
});
}
}
return cells;
};
// All cells to render
const allCells = computed(() => {
const S = props.SCALE;
const dist = REAR_FACE_DISTANCE * S * 3;
const al = props.activeLayer;
const rot = props.currentLayerRotation;
const cells = [];
for (const fd of FACE_DEFS.value) {
const isHidden = hiddenFaceKeys.value.has(fd.faceKey);
if (isHidden) {
// Hidden face: render non-rotating cells (static projections)
cells.push(...buildFaceCells(fd, S, dist, al, rot, false));
}
// ALL faces: render rotating-layer cells (they swap via backface-visibility)
if (props.animateLayers && al && rot !== 0) {
cells.push(...buildFaceCells(fd, S, dist, al, rot, true));
}
}
return cells;
});
</script>
<template>
<div class="face-projections-root">
<div
v-for="cell in allCells"
:key="cell.key"
class="proj-cell"
:class="cell.color"
:style="{ transform: cell.transform }"
></div>
</div>
</template>
<style scoped>
.face-projections-root {
position: absolute;
transform-style: preserve-3d;
}
.proj-cell {
position: absolute;
width: 100px;
height: 100px;
margin-left: -50px;
margin-top: -50px;
box-sizing: border-box;
background: #000;
border: 1px solid #000;
backface-visibility: hidden;
}
.proj-cell::after {
content: "";
position: absolute;
top: 4px;
left: 4px;
right: 4px;
bottom: 4px;
border-radius: 8px;
box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.3);
}
/* Colors - use global design system variables */
.proj-cell.white::after { background: var(--sticker-white); }
.proj-cell.yellow::after { background: var(--sticker-yellow); }
.proj-cell.green::after { background: var(--sticker-green); }
.proj-cell.blue::after { background: var(--sticker-blue); }
.proj-cell.orange::after { background: var(--sticker-orange); }
.proj-cell.red::after { background: var(--sticker-red); }
.proj-cell.black::after { display: none; }
</style>

View File

@@ -0,0 +1,225 @@
<script setup>
import { ref, computed, onMounted, onUnmounted, watch, nextTick } from "vue";
const props = defineProps({
moves: {
type: Array,
required: true,
},
});
const emit = defineEmits(["reset", "copy", "add-moves", "open-add-modal"]);
const MIN_MOVES_COLUMN_GAP = 6;
const movesHistoryEl = ref(null);
const samplePillEl = ref(null);
const movesPerRow = ref(0);
const movesColumnGap = ref(MIN_MOVES_COLUMN_GAP);
const displayMoves = computed(() => props.moves || []);
const moveRows = computed(() => {
const perRow = movesPerRow.value || displayMoves.value.length || 1;
const rows = [];
const all = displayMoves.value;
for (let i = 0; i < all.length; i += perRow) {
rows.push(all.slice(i, i + perRow));
}
return rows;
});
const hasMoves = computed(() => displayMoves.value.length > 0);
const copyQueueToClipboard = () => {
emit("copy");
};
const resetQueue = () => {
emit("reset");
};
const setSamplePill = (el) => {
if (el && !samplePillEl.value) {
samplePillEl.value = el;
}
};
const recalcMovesLayout = () => {
const container = movesHistoryEl.value;
const pill = samplePillEl.value;
if (!container || !pill) return;
const containerWidth = container.clientWidth;
const pillWidth = pill.offsetWidth;
if (pillWidth <= 0) return;
const totalWidth = (cols) => {
if (cols <= 0) return 0;
if (cols === 1) return pillWidth;
return cols * pillWidth + (cols - 1) * MIN_MOVES_COLUMN_GAP;
};
let cols = Math.floor(
(containerWidth + MIN_MOVES_COLUMN_GAP) /
(pillWidth + MIN_MOVES_COLUMN_GAP),
);
if (cols < 1) cols = 1;
while (cols > 1 && totalWidth(cols) > containerWidth) {
cols -= 1;
}
let gap = 0;
if (cols > 1) {
gap = (containerWidth - cols * pillWidth) / (cols - 1);
}
movesPerRow.value = cols;
movesColumnGap.value = gap;
};
const openAddModal = () => {
emit("open-add-modal");
};
watch(displayMoves, () => {
nextTick(recalcMovesLayout);
});
onMounted(() => {
window.addEventListener("resize", recalcMovesLayout);
nextTick(recalcMovesLayout);
});
onUnmounted(() => {
window.removeEventListener("resize", recalcMovesLayout);
});
</script>
<template>
<div class="moves-history">
<div class="moves-inner" ref="movesHistoryEl">
<div
v-for="(row, rowIndex) in moveRows"
:key="rowIndex"
class="moves-row"
:style="{ columnGap: movesColumnGap + 'px' }"
>
<span
v-for="(m, idx) in row"
:key="m.id"
class="move-pill"
:class="{
'move-pill-active': m.status === 'in_progress',
'move-pill-pending': m.status === 'pending',
}"
:ref="rowIndex === 0 && idx === 0 ? setSamplePill : null"
>
{{ m.label }}
</span>
</div>
</div>
<div class="moves-actions">
<button class="queue-action" @click="openAddModal">add</button>
<button
class="queue-action"
:class="{ 'queue-action-disabled': !hasMoves }"
:disabled="!hasMoves"
@click="copyQueueToClipboard"
>
copy
</button>
<button
class="queue-action"
:class="{ 'queue-action-disabled': !hasMoves }"
:disabled="!hasMoves"
@click="resetQueue"
>
reset
</button>
</div>
</div>
</template>
<style scoped>
.moves-history {
position: absolute;
bottom: 72px;
left: 50%;
transform: translateX(-50%);
width: 100%;
max-width: calc(100vw - 360px);
overflow-x: hidden;
padding: 12px 12px 26px 12px;
background: rgba(0, 0, 0, 0.4);
border-radius: 8px;
backdrop-filter: blur(8px);
}
.moves-inner {
display: flex;
flex-direction: column;
gap: 6px;
}
.moves-row {
display: flex;
}
.move-pill {
display: flex;
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);
font-size: 0.8rem;
color: #fff;
white-space: nowrap;
}
.move-pill-active {
background: #ffd500;
color: #000;
border-color: #ffd500;
}
.move-pill-pending {
opacity: 0.4;
}
.moves-actions {
position: absolute;
right: 6px;
bottom: 6px;
display: flex;
gap: 0px;
}
.queue-action {
border: none;
background: transparent;
padding: 6px 6px;
color: #fff;
font-size: 0.8rem;
cursor: pointer;
}
.queue-action-disabled {
opacity: 0.35;
cursor: default;
pointer-events: none;
}
.moves-history::after {
content: none;
}
.queue-action:focus {
outline: none;
box-shadow: none;
}
</style>

File diff suppressed because it is too large Load Diff

View File

@@ -1,57 +1,110 @@
import { ref, computed } from "vue";
import { COLORS, FACES } from "../utils/CubeModel";
import { ref, computed } from 'vue';
import { COLORS, FACES } from '../utils/CubeModel';
// Singleton logic worker
const worker = new Worker(
new URL("../workers/Cube.worker.js", import.meta.url),
{ type: "module" },
);
// Singleton worker
const worker = new Worker(new URL('../workers/Cube.worker.js', import.meta.url), { type: 'module' });
// Singleton solver worker
const solverWorker = new Worker(
new URL("../workers/Solver.worker.js", import.meta.url),
{ type: "module" },
);
// Reactive state
const cubies = ref([]);
const deepCubeState = ref(null);
const isReady = ref(false);
const isSolverReady = ref(false);
const validationResult = ref(null);
const solveResult = ref(null);
const solveError = ref(null);
worker.onmessage = (e) => {
const { type, payload } = e.data;
if (type === 'STATE_UPDATE') {
if (type === "STATE_UPDATE") {
cubies.value = payload.cubies;
deepCubeState.value = payload.deepCubeState;
isReady.value = true;
} else if (type === 'VALIDATION_RESULT') {
} else if (type === "VALIDATION_RESULT") {
validationResult.value = payload;
} else if (type === 'ERROR') {
console.error('Worker Error:', payload);
} else if (type === "SOLVE_RESULT") {
solveResult.value = payload;
} else if (type === "ERROR") {
console.error("Logic Worker Error:", payload);
}
};
solverWorker.onmessage = (e) => {
const { type, payload } = e.data;
if (type === "SOLVE_RESULT") {
solveResult.value = payload;
} else if (type === "SOLVE_ERROR") {
// Error doesn't necessarily block execution, it just provides UI feedback
solveError.value = payload;
} else if (type === "INIT_DONE") {
isSolverReady.value = true;
} else if (type === "ERROR") {
console.error("Solver Worker Error:", payload);
}
};
// Init worker
worker.postMessage({ type: 'INIT' });
worker.postMessage({ type: "INIT" });
export function useCube() {
const initCube = () => {
worker.postMessage({ type: 'RESET' });
worker.postMessage({ type: "RESET" });
};
const rotateLayer = (axis, index, direction) => {
worker.postMessage({ type: 'ROTATE_LAYER', payload: { axis, index, direction } });
const rotateLayer = (axis, index, direction, steps = 1) => {
worker.postMessage({
type: "ROTATE_LAYER",
payload: { axis, index, direction, steps },
});
};
const rotateSlice = (axis, direction, steps = 1) => {
worker.postMessage({
type: "ROTATE_SLICE",
payload: { axis, direction, steps },
});
};
const turn = (move) => {
worker.postMessage({ type: 'TURN', payload: { move } });
worker.postMessage({ type: "TURN", payload: { move } });
};
const validate = () => {
worker.postMessage({ type: 'VALIDATE' });
worker.postMessage({ type: "VALIDATE" });
};
const solve = (solverType, cubeState) => {
solveResult.value = null;
solveError.value = null;
solverWorker.postMessage({
type: "SOLVE",
payload: { solverType, cubeState },
});
};
return {
cubies: computed(() => cubies.value),
deepCubeState: computed(() => deepCubeState.value),
isReady: computed(() => isReady.value),
isSolverReady: computed(() => isSolverReady.value),
validationResult: computed(() => validationResult.value),
solveResult: computed(() => solveResult.value),
solveError: computed(() => solveError.value),
initCube,
rotateLayer,
rotateSlice,
turn,
validate,
solve,
COLORS,
FACES
FACES,
};
}

View File

@@ -1,25 +1,44 @@
import { ref } from 'vue';
import { ref } from "vue";
let initialCubeTranslucent = false;
try {
const stored = localStorage.getItem('cubeTranslucent');
const stored = localStorage.getItem("cubeTranslucent");
if (stored !== null) {
initialCubeTranslucent = stored === 'true';
initialCubeTranslucent = stored === "true";
}
} catch (e) {}
} catch (e) { }
// 0 = off, 1 = simple projections, 2 = advanced (animated layers)
let initialProjectionMode = 0;
try {
const stored = localStorage.getItem("projectionMode");
if (stored !== null) {
initialProjectionMode = Math.min(2, Math.max(0, parseInt(stored, 10) || 0));
}
} catch (e) { }
const isCubeTranslucent = ref(initialCubeTranslucent);
const projectionMode = ref(initialProjectionMode);
export function useSettings() {
const toggleCubeTranslucent = () => {
isCubeTranslucent.value = !isCubeTranslucent.value;
try {
localStorage.setItem('cubeTranslucent', String(isCubeTranslucent.value));
} catch (e) {}
localStorage.setItem("cubeTranslucent", String(isCubeTranslucent.value));
} catch (e) { }
};
const cycleProjectionMode = () => {
projectionMode.value = (projectionMode.value + 1) % 3;
try {
localStorage.setItem("projectionMode", String(projectionMode.value));
} catch (e) { }
};
return {
isCubeTranslucent,
toggleCubeTranslucent
toggleCubeTranslucent,
projectionMode,
cycleProjectionMode,
};
}

View File

@@ -1 +0,0 @@
export const LAYER_ANIMATION_DURATION = 200

8
src/config/settings.js Normal file
View File

@@ -0,0 +1,8 @@
export const LAYER_ANIMATION_DURATION = 200;
export const MIDDLE_SLICES_ENABLED = false;
// Distance of rear face projections from cube center (in cube-size units)
// 1.0 = one cube width, 0.5 = half cube width
export const REAR_FACE_DISTANCE = 1.0;

View File

@@ -1,5 +1,6 @@
import { createApp } from 'vue'
import './style.css'
import App from './App.vue'
import { createApp } from "vue";
import "./style.css";
import "toastify-js/src/toastify.css";
import App from "./App.vue";
createApp(App).mount('#app')
createApp(App).mount("#app");

View File

@@ -31,7 +31,7 @@
--toggle-btn-border: rgba(255, 255, 255, 0.2);
--toggle-hover-border: #ffffff;
--toggle-active-shadow: 0 0 10px rgba(0, 242, 255, 0.3);
--panel-bg: rgba(255, 255, 255, 0.1);
--panel-bg: rgba(0, 0, 0, 0.4);
--panel-border: rgba(255, 255, 255, 0.1);
--panel-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
--button-bg: rgba(255, 255, 255, 0.1);
@@ -42,6 +42,14 @@
--button-active-shadow: 0 0 20px rgba(79, 172, 254, 0.4);
--cube-edge-color: #333333;
--title-gradient: linear-gradient(45deg, #00f2fe, #4facfe);
/* Cube sticker colors */
--sticker-white: #e0e0e0;
--sticker-yellow: #ffd500;
--sticker-green: #009e60;
--sticker-blue: #0051ba;
--sticker-orange: #ff5800;
--sticker-red: #c41e3a;
}
:root[data-theme="light"] {
@@ -81,6 +89,7 @@ a {
color: #646cff;
text-decoration: inherit;
}
a:hover {
color: #535bf2;
}
@@ -113,9 +122,11 @@ button {
cursor: pointer;
transition: border-color 0.25s;
}
button:hover {
border-color: #646cff;
}
button:focus,
button:focus-visible {
outline: 4px auto -webkit-focus-ring-color;
@@ -191,9 +202,11 @@ button.btn-neon.icon-only {
color: #213547;
background-color: #ffffff;
}
a:hover {
color: #747bff;
}
button {
background-color: #f9f9f9;
}

View File

@@ -0,0 +1,68 @@
import { DeepCube, MOVES } from './DeepCube.js';
import { CubeModel } from './CubeModel.js';
export class RubiksJSModel {
constructor() {
this.state = new DeepCube();
this.visual = new CubeModel();
}
reset() {
this.state = new DeepCube();
this.visual = new CubeModel();
}
rotateLayer(axis, index, dir, steps = 1) {
let move = '';
if (axis === 'y') {
if (index === 1) move = dir === 1 ? "U'" : "U";
else if (index === -1) move = dir === -1 ? "D'" : "D";
} else if (axis === 'x') {
if (index === 1) move = dir === 1 ? "R'" : "R";
else if (index === -1) move = dir === -1 ? "L'" : "L";
} else if (axis === 'z') {
if (index === 1) move = dir === 1 ? "F'" : "F";
else if (index === -1) move = dir === -1 ? "B'" : "B";
}
if (move) {
for (let i = 0; i < steps; i++) {
try {
this.state = this.state.multiply(MOVES[move]);
} catch (e) {
console.error('[RubiksJSModel] Failed to apply move:', move, e);
}
this.visual.rotateLayer(axis, index, dir);
}
}
}
applyTurn(move) {
if (!move) return;
try {
this.state = this.state.multiply(MOVES[move]);
} catch (e) {
console.error('[RubiksJSModel] Failed to apply direct move:', move, e);
}
this.visual.applyMove(move);
}
rotateSlice(axis, direction, steps = 1) {
// A middle slice rotation (M, E, S) logically translates to rotating
// the two intersecting outer layers in the opposite direction, while
// the centers (the core abstract frame) remain perfectly stationary.
// The frontend simultaneously handles rotating the camera to complete the illusion.
this.rotateLayer(axis, 1, -direction, steps);
this.rotateLayer(axis, -1, -direction, steps);
}
toCubies() {
return this.visual.toCubies();
}
validate() {
const valid = this.state.isValid();
return { valid, errors: valid ? [] : ['Invalid cube configuration (Parity or Orientation rules violated)'] };
}
}

View File

@@ -12,22 +12,22 @@
*/
export const COLORS = {
WHITE: 'white',
YELLOW: 'yellow',
ORANGE: 'orange',
RED: 'red',
GREEN: 'green',
BLUE: 'blue',
BLACK: 'black'
WHITE: "white",
YELLOW: "yellow",
ORANGE: "orange",
RED: "red",
GREEN: "green",
BLUE: "blue",
BLACK: "black",
};
export const FACES = {
UP: 'up',
DOWN: 'down',
LEFT: 'left',
RIGHT: 'right',
FRONT: 'front',
BACK: 'back',
UP: "up",
DOWN: "down",
LEFT: "left",
RIGHT: "right",
FRONT: "front",
BACK: "back",
};
// Standard Face Colors (Solved State)
@@ -99,7 +99,7 @@ export class CubeModel {
*/
rotateLayer(axis, index, direction) {
// Determine the relevant cubies in the slice
const slice = this.cubies.filter(c => c[axis] === index);
const slice = this.cubies.filter((c) => c[axis] === index);
// Coordinate rotation (Matrix Logic)
// 90 deg CW rotation formulas:
@@ -120,7 +120,7 @@ export class CubeModel {
// If direction is -1: Inverse.
slice.forEach(cubie => {
slice.forEach((cubie) => {
this._rotateCubieCoordinates(cubie, axis, direction);
this._rotateCubieFaces(cubie, axis, direction);
});
@@ -129,7 +129,7 @@ export class CubeModel {
_rotateCubieCoordinates(cubie, axis, direction) {
const { x, y, z } = cubie;
if (axis === 'x') {
if (axis === "x") {
if (direction === 1) {
cubie.y = -z;
cubie.z = y;
@@ -137,7 +137,7 @@ export class CubeModel {
cubie.y = z;
cubie.z = -y;
}
} else if (axis === 'y') {
} else if (axis === "y") {
if (direction === 1) {
cubie.z = -x;
cubie.x = z;
@@ -145,11 +145,13 @@ export class CubeModel {
cubie.z = x;
cubie.x = -z;
}
} else if (axis === 'z') {
if (direction === 1) { // CW
} else if (axis === "z") {
if (direction === 1) {
// CW
cubie.x = -y;
cubie.y = x;
} else { // CCW
} else {
// CCW
cubie.x = y;
cubie.y = -x;
}
@@ -165,38 +167,44 @@ export class CubeModel {
const f = { ...cubie.faces };
if (axis === 'x') {
if (direction === 1) { // Up -> Front -> Down -> Back -> Up
if (axis === "x") {
if (direction === 1) {
// Up -> Front -> Down -> Back -> Up
cubie.faces[FACES.FRONT] = f[FACES.UP];
cubie.faces[FACES.DOWN] = f[FACES.FRONT];
cubie.faces[FACES.BACK] = f[FACES.DOWN];
cubie.faces[FACES.UP] = f[FACES.BACK];
// Left/Right unchanged in position, but might rotate? No, faces are solid colors.
} else { // Up -> Back -> Down -> Front -> Up
} else {
// Up -> Back -> Down -> Front -> Up
cubie.faces[FACES.BACK] = f[FACES.UP];
cubie.faces[FACES.DOWN] = f[FACES.BACK];
cubie.faces[FACES.FRONT] = f[FACES.DOWN];
cubie.faces[FACES.UP] = f[FACES.FRONT];
}
} else if (axis === 'y') {
if (direction === 1) { // Front -> Right -> Back -> Left -> Front
} else if (axis === "y") {
if (direction === 1) {
// Front -> Right -> Back -> Left -> Front
cubie.faces[FACES.RIGHT] = f[FACES.FRONT];
cubie.faces[FACES.BACK] = f[FACES.RIGHT];
cubie.faces[FACES.LEFT] = f[FACES.BACK];
cubie.faces[FACES.FRONT] = f[FACES.LEFT];
} else { // Front -> Left -> Back -> Right -> Front
} else {
// Front -> Left -> Back -> Right -> Front
cubie.faces[FACES.LEFT] = f[FACES.FRONT];
cubie.faces[FACES.BACK] = f[FACES.LEFT];
cubie.faces[FACES.RIGHT] = f[FACES.BACK];
cubie.faces[FACES.FRONT] = f[FACES.RIGHT];
}
} else if (axis === 'z') {
if (direction === 1) { // CCW: Up -> Left -> Down -> Right -> Up
} else if (axis === "z") {
if (direction === 1) {
// CCW: Up -> Left -> Down -> Right -> Up
cubie.faces[FACES.LEFT] = f[FACES.UP];
cubie.faces[FACES.DOWN] = f[FACES.LEFT];
cubie.faces[FACES.RIGHT] = f[FACES.DOWN];
cubie.faces[FACES.UP] = f[FACES.RIGHT];
} else { // CW: Up -> Right -> Down -> Left -> Up
} else {
// CW: Up -> Right -> Down -> Left -> Up
cubie.faces[FACES.RIGHT] = f[FACES.UP];
cubie.faces[FACES.DOWN] = f[FACES.RIGHT];
cubie.faces[FACES.LEFT] = f[FACES.DOWN];
@@ -208,12 +216,12 @@ export class CubeModel {
toCubies() {
// Return copy of state for rendering
// CubeCSS expects array of objects with x, y, z, faces
return this.cubies.map(c => ({
return this.cubies.map((c) => ({
id: c.id,
x: c.x,
y: c.y,
z: c.z,
faces: { ...c.faces }
faces: { ...c.faces },
}));
}
@@ -238,30 +246,54 @@ export class CubeModel {
// B (CW around -Z): 1 (since Z(1) is CW around -Z)
switch (base) {
case 'U': direction = 1; break;
case 'D': direction = -1; break;
case 'L': direction = -1; break;
case 'R': direction = 1; break;
case 'F': direction = -1; break;
case 'B': direction = 1; break;
case "U":
direction = 1;
break;
case "D":
direction = -1;
break;
case "L":
direction = -1;
break;
case "R":
direction = 1;
break;
case "F":
direction = -1;
break;
case "B":
direction = 1;
break;
}
if (modifier === "'") direction *= -1;
if (modifier === '2') {
if (modifier === "2") {
// 2 moves. Direction doesn't matter for 180, but let's keep it.
// We will call rotateLayer twice.
}
const count = modifier === '2' ? 2 : 1;
const count = modifier === "2" ? 2 : 1;
for (let i = 0; i < count; i++) {
switch (base) {
case 'U': this.rotateLayer('y', 1, direction); break;
case 'D': this.rotateLayer('y', -1, direction); break;
case 'L': this.rotateLayer('x', -1, direction); break;
case 'R': this.rotateLayer('x', 1, direction); break;
case 'F': this.rotateLayer('z', 1, direction); break;
case 'B': this.rotateLayer('z', -1, direction); break;
case "U":
this.rotateLayer("y", 1, direction);
break;
case "D":
this.rotateLayer("y", -1, direction);
break;
case "L":
this.rotateLayer("x", -1, direction);
break;
case "R":
this.rotateLayer("x", 1, direction);
break;
case "F":
this.rotateLayer("z", 1, direction);
break;
case "B":
this.rotateLayer("z", -1, direction);
break;
}
}
}
@@ -282,25 +314,46 @@ export class CubeModel {
for (let c = 0; c < 3; c++) {
let cubie;
// Map r,c to x,y,z based on face
if (face === FACES.UP) { // y=1. r=0->z=-1 (Back), r=2->z=1 (Front). c=0->x=-1 (Left).
if (face === FACES.UP) {
// y=1. r=0->z=-1 (Back), r=2->z=1 (Front). c=0->x=-1 (Left).
// Standard U face view: Top Left is Back Left (-1, 1, -1).
// Row 0 (Top of U face) is Back.
// Row 2 (Bottom of U face) is Front.
cubie = this.cubies.find(cu => cu.y === 1 && cu.x === (c - 1) && cu.z === (r - 1)); // Wait.
cubie = this.cubies.find(
(cu) => cu.y === 1 && cu.x === c - 1 && cu.z === r - 1,
); // Wait.
// Back is z=-1. Front is z=1.
// Visual Top of U face is Back (z=-1).
// Visual Bottom of U face is Front (z=1).
cubie = this.cubies.find(cu => cu.y === 1 && cu.x === (c - 1) && cu.z === (r - 1 - 2 * r)); // Complicated.
cubie = this.cubies.find(
(cu) => cu.y === 1 && cu.x === c - 1 && cu.z === r - 1 - 2 * r,
); // Complicated.
// Let's just find by strict coordinates
// r=0 -> z=-1. r=1 -> z=0. r=2 -> z=1.
// c=0 -> x=-1. c=1 -> x=0. c=2 -> x=1.
cubie = this.cubies.find(cu => cu.y === 1 && cu.x === (c - 1) && cu.z === (r - 1));
}
else if (face === FACES.DOWN) cubie = this.cubies.find(cu => cu.y === -1 && cu.x === (c - 1) && cu.z === (1 - r)); // Down View?
else if (face === FACES.FRONT) cubie = this.cubies.find(cu => cu.z === 1 && cu.x === (c - 1) && cu.y === (1 - r));
else if (face === FACES.BACK) cubie = this.cubies.find(cu => cu.z === -1 && cu.x === (1 - c) && cu.y === (1 - r));
else if (face === FACES.LEFT) cubie = this.cubies.find(cu => cu.x === -1 && cu.z === (1 - c) && cu.y === (1 - r)); // Left view z order?
else if (face === FACES.RIGHT) cubie = this.cubies.find(cu => cu.x === 1 && cu.z === (c - 1) && cu.y === (1 - r));
cubie = this.cubies.find(
(cu) => cu.y === 1 && cu.x === c - 1 && cu.z === r - 1,
);
} else if (face === FACES.DOWN)
cubie = this.cubies.find(
(cu) => cu.y === -1 && cu.x === c - 1 && cu.z === 1 - r,
); // Down View?
else if (face === FACES.FRONT)
cubie = this.cubies.find(
(cu) => cu.z === 1 && cu.x === c - 1 && cu.y === 1 - r,
);
else if (face === FACES.BACK)
cubie = this.cubies.find(
(cu) => cu.z === -1 && cu.x === 1 - c && cu.y === 1 - r,
);
else if (face === FACES.LEFT)
cubie = this.cubies.find(
(cu) => cu.x === -1 && cu.z === 1 - c && cu.y === 1 - r,
); // Left view z order?
else if (face === FACES.RIGHT)
cubie = this.cubies.find(
(cu) => cu.x === 1 && cu.z === c - 1 && cu.y === 1 - r,
);
if (cubie) {
rowStr += cubie.faces[face][0].toUpperCase() + " ";
@@ -313,16 +366,16 @@ export class CubeModel {
out += "\n";
};
printFace(FACES.UP, 'U');
printFace(FACES.DOWN, 'D');
printFace(FACES.FRONT, 'F');
printFace(FACES.BACK, 'B');
printFace(FACES.LEFT, 'L');
printFace(FACES.RIGHT, 'R');
printFace(FACES.UP, "U");
printFace(FACES.DOWN, "D");
printFace(FACES.FRONT, "F");
printFace(FACES.BACK, "B");
printFace(FACES.LEFT, "L");
printFace(FACES.RIGHT, "R");
return out;
}
scramble(n = 20) {
const axes = ['x', 'y', 'z'];
const axes = ["x", "y", "z"];
const indices = [-1, 1]; // Usually rotate outer layers for scramble
// Actually, scrambling usually involves random face moves (U, D, L, R, F, B)
// U: y=1, dir -1 (Standard CW)

409
src/utils/DeepCube.js Normal file
View File

@@ -0,0 +1,409 @@
// Corner indices
export const CORNERS = {
URF: 0,
UFL: 1,
ULB: 2,
UBR: 3,
DFR: 4,
DLF: 5,
DBL: 6,
DRB: 7,
};
// Edge indices
export const EDGES = {
UR: 0,
UF: 1,
UL: 2,
UB: 3,
DR: 4,
DF: 5,
DL: 6,
DB: 7,
FR: 8,
FL: 9,
BL: 10,
BR: 11,
};
export class DeepCube {
constructor(cp, co, ep, eo) {
if (cp && co && ep && eo) {
this.cp = [...cp];
this.co = [...co];
this.ep = [...ep];
this.eo = [...eo];
} else {
// Solved identity state
this.cp = [0, 1, 2, 3, 4, 5, 6, 7];
this.co = [0, 0, 0, 0, 0, 0, 0, 0];
this.ep = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11];
this.eo = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
}
}
// Multiply (apply) another cube state to this one.
multiply(b) {
const cp = new Array(8);
const co = new Array(8);
const ep = new Array(12);
const eo = new Array(12);
// Corners
for (let i = 0; i < 8; i++) {
cp[i] = this.cp[b.cp[i]];
co[i] = (this.co[b.cp[i]] + b.co[i]) % 3;
}
// Edges
for (let i = 0; i < 12; i++) {
ep[i] = this.ep[b.ep[i]];
eo[i] = (this.eo[b.ep[i]] + b.eo[i]) % 2;
}
return new DeepCube(cp, co, ep, eo);
}
clone() {
return new DeepCube(this.cp, this.co, this.ep, this.eo);
}
// Checks if the mathematical state is solvable/possible
isValid() {
// 1. Edge parity must equal corner parity
let edgeParity = 0;
for (let i = 11; i >= 0; i--) {
for (let j = i - 1; j >= 0; j--) {
if (this.ep[j] > this.ep[i]) edgeParity++;
}
}
let cornerParity = 0;
for (let i = 7; i >= 0; i--) {
for (let j = i - 1; j >= 0; j--) {
if (this.cp[j] > this.cp[i]) cornerParity++;
}
}
if (edgeParity % 2 !== cornerParity % 2) return false;
// 2. Edge orientations must sum to even
let eoSum = this.eo.reduce((a, b) => a + b, 0);
if (eoSum % 2 !== 0) return false;
// 3. Corner orientations must be divisible by 3
let coSum = this.co.reduce((a, b) => a + b, 0);
if (coSum % 3 !== 0) return false;
return true;
}
isSolved() {
// Check if permutations are identity and orientations are zero
for (let i = 0; i < 8; i++) {
if (this.cp[i] !== i || this.co[i] !== 0) return false;
}
for (let i = 0; i < 12; i++) {
if (this.ep[i] !== i || this.eo[i] !== 0) return false;
}
return true;
}
static fromCubies(cubies) {
const c2f = {
white: "U",
yellow: "D",
orange: "L",
red: "R",
green: "F",
blue: "B",
};
const getCubie = (x, y, z) =>
cubies.find((c) => c.x === x && c.y === y && c.z === z);
const baseC = [
["U", "R", "F"],
["U", "F", "L"],
["U", "L", "B"],
["U", "B", "R"],
["D", "F", "R"],
["D", "L", "F"],
["D", "B", "L"],
["D", "R", "B"],
];
const slotC = [
{ x: 1, y: 1, z: 1, faces: ["up", "right", "front"] }, // 0: URF
{ x: -1, y: 1, z: 1, faces: ["up", "front", "left"] }, // 1: UFL
{ x: -1, y: 1, z: -1, faces: ["up", "left", "back"] }, // 2: ULB
{ x: 1, y: 1, z: -1, faces: ["up", "back", "right"] }, // 3: UBR
{ x: 1, y: -1, z: 1, faces: ["down", "front", "right"] }, // 4: DFR
{ x: -1, y: -1, z: 1, faces: ["down", "left", "front"] }, // 5: DLF
{ x: -1, y: -1, z: -1, faces: ["down", "back", "left"] }, // 6: DBL
{ x: 1, y: -1, z: -1, faces: ["down", "right", "back"] }, // 7: DRB
];
let cp = [],
co = [];
for (let i = 0; i < 8; i++) {
let slot = slotC[i];
let c = getCubie(slot.x, slot.y, slot.z);
let colors = [
c2f[c.faces[slot.faces[0]]],
c2f[c.faces[slot.faces[1]]],
c2f[c.faces[slot.faces[2]]],
];
let perm = baseC.findIndex(
(bc) =>
colors.includes(bc[0]) &&
colors.includes(bc[1]) &&
colors.includes(bc[2]),
);
cp[i] = perm;
co[i] = colors.indexOf(baseC[perm][0]);
}
const baseE = [
["U", "R"],
["U", "F"],
["U", "L"],
["U", "B"],
["D", "R"],
["D", "F"],
["D", "L"],
["D", "B"],
["F", "R"],
["F", "L"],
["B", "L"],
["B", "R"],
];
const slotE = [
{ x: 1, y: 1, z: 0, faces: ["up", "right"] },
{ x: 0, y: 1, z: 1, faces: ["up", "front"] },
{ x: -1, y: 1, z: 0, faces: ["up", "left"] },
{ x: 0, y: 1, z: -1, faces: ["up", "back"] },
{ x: 1, y: -1, z: 0, faces: ["down", "right"] },
{ x: 0, y: -1, z: 1, faces: ["down", "front"] },
{ x: -1, y: -1, z: 0, faces: ["down", "left"] },
{ x: 0, y: -1, z: -1, faces: ["down", "back"] },
{ x: 1, y: 0, z: 1, faces: ["front", "right"] },
{ x: -1, y: 0, z: 1, faces: ["front", "left"] },
{ x: -1, y: 0, z: -1, faces: ["back", "left"] },
{ x: 1, y: 0, z: -1, faces: ["back", "right"] },
];
let ep = [],
eo = [];
for (let i = 0; i < 12; i++) {
let slot = slotE[i];
let c = getCubie(slot.x, slot.y, slot.z);
let colors = [c2f[c.faces[slot.faces[0]]], c2f[c.faces[slot.faces[1]]]];
let perm = baseE.findIndex(
(be) => colors.includes(be[0]) && colors.includes(be[1]),
);
ep[i] = perm;
eo[i] = colors.indexOf(baseE[perm][0]);
}
return new DeepCube(cp, co, ep, eo);
}
}
// ----------------------------------------------------------------------------
// BASE MOVES DEFINITIONS
// Represents the effect of 90-degree clockwise faces on the solved state.
// ----------------------------------------------------------------------------
export const MOVES = {};
// U (Up Face Clockwise)
MOVES["U"] = new DeepCube(
[
CORNERS.UBR,
CORNERS.URF,
CORNERS.UFL,
CORNERS.ULB,
CORNERS.DFR,
CORNERS.DLF,
CORNERS.DBL,
CORNERS.DRB,
],
[0, 0, 0, 0, 0, 0, 0, 0],
[
EDGES.UB,
EDGES.UR,
EDGES.UF,
EDGES.UL,
EDGES.DR,
EDGES.DF,
EDGES.DL,
EDGES.DB,
EDGES.FR,
EDGES.FL,
EDGES.BL,
EDGES.BR,
],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
);
// R (Right Face Clockwise)
MOVES["R"] = new DeepCube(
[
CORNERS.DFR,
CORNERS.UFL,
CORNERS.ULB,
CORNERS.URF,
CORNERS.DRB,
CORNERS.DLF,
CORNERS.DBL,
CORNERS.UBR,
],
[2, 0, 0, 1, 1, 0, 0, 2],
[
EDGES.FR,
EDGES.UF,
EDGES.UL,
EDGES.UB,
EDGES.BR,
EDGES.DF,
EDGES.DL,
EDGES.DB,
EDGES.DR,
EDGES.FL,
EDGES.BL,
EDGES.UR,
],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
);
// F (Front Face Clockwise)
MOVES["F"] = new DeepCube(
[
CORNERS.UFL,
CORNERS.DLF,
CORNERS.ULB,
CORNERS.UBR,
CORNERS.URF,
CORNERS.DFR,
CORNERS.DBL,
CORNERS.DRB,
],
[1, 2, 0, 0, 2, 1, 0, 0],
[
EDGES.UR,
EDGES.FL,
EDGES.UL,
EDGES.UB,
EDGES.DR,
EDGES.FR,
EDGES.DL,
EDGES.DB,
EDGES.UF,
EDGES.DF,
EDGES.BL,
EDGES.BR,
],
[0, 1, 0, 0, 0, 1, 0, 0, 1, 1, 0, 0],
);
// D (Down Face Clockwise)
MOVES["D"] = new DeepCube(
[
CORNERS.URF,
CORNERS.UFL,
CORNERS.ULB,
CORNERS.UBR,
CORNERS.DLF,
CORNERS.DBL,
CORNERS.DRB,
CORNERS.DFR,
],
[0, 0, 0, 0, 0, 0, 0, 0],
[
EDGES.UR,
EDGES.UF,
EDGES.UL,
EDGES.UB,
EDGES.DF,
EDGES.DL,
EDGES.DB,
EDGES.DR,
EDGES.FR,
EDGES.FL,
EDGES.BL,
EDGES.BR,
],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
);
// L (Left Face Clockwise)
MOVES["L"] = new DeepCube(
[
CORNERS.URF,
CORNERS.ULB,
CORNERS.DBL,
CORNERS.UBR,
CORNERS.DFR,
CORNERS.UFL,
CORNERS.DLF,
CORNERS.DRB,
],
[0, 1, 2, 0, 0, 2, 1, 0],
[
EDGES.UR,
EDGES.UF,
EDGES.BL,
EDGES.UB,
EDGES.DR,
EDGES.DF,
EDGES.FL,
EDGES.DB,
EDGES.FR,
EDGES.UL,
EDGES.DL,
EDGES.BR,
],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
);
// B (Back Face Clockwise)
MOVES["B"] = new DeepCube(
[
CORNERS.URF,
CORNERS.UFL,
CORNERS.UBR,
CORNERS.DRB,
CORNERS.DFR,
CORNERS.DLF,
CORNERS.ULB,
CORNERS.DBL,
],
[0, 0, 1, 2, 0, 0, 2, 1],
[
EDGES.UR,
EDGES.UF,
EDGES.UL,
EDGES.BR,
EDGES.DR,
EDGES.DF,
EDGES.DL,
EDGES.BL,
EDGES.FR,
EDGES.FL,
EDGES.UB,
EDGES.DB,
],
[0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 1, 1],
);
// Generate inverses and 180s
const faces = ["U", "R", "F", "D", "L", "B"];
faces.forEach((f) => {
const m1 = MOVES[f];
const m2 = m1.multiply(m1);
const m3 = m2.multiply(m1);
MOVES[f + "2"] = m2;
MOVES[f + "'"] = m3;
});

View File

@@ -1,172 +0,0 @@
import { State } from 'rubiks-js/src/state/index.js';
import { CubeModel } from './CubeModel.js';
// Static order definitions from rubiks-js source
const CORNER_ORDER = ['URF', 'ULF', 'ULB', 'URB', 'DRF', 'DLF', 'DLB', 'DRB'];
const EDGE_ORDER = ['UF', 'UL', 'UB', 'UR', 'FR', 'FL', 'BL', 'BR', 'DF', 'DL', 'DB', 'DR'];
// Coordinate mapping for visualization
// Coordinates match the visual grid positions
const CORNER_SLOTS = [
{ id: 'URF', x: 1, y: 1, z: 1 },
{ id: 'ULF', x: -1, y: 1, z: 1 },
{ id: 'ULB', x: -1, y: 1, z: -1 },
{ id: 'URB', x: 1, y: 1, z: -1 },
{ id: 'DRF', x: 1, y: -1, z: 1 },
{ id: 'DLF', x: -1, y: -1, z: 1 },
{ id: 'DLB', x: -1, y: -1, z: -1 },
{ id: 'DRB', x: 1, y: -1, z: -1 }
];
const EDGE_SLOTS = [
{ id: 'UF', x: 0, y: 1, z: 1 },
{ id: 'UL', x: -1, y: 1, z: 0 },
{ id: 'UB', x: 0, y: 1, z: -1 },
{ id: 'UR', x: 1, y: 1, z: 0 },
{ id: 'FR', x: 1, y: 0, z: 1 },
{ id: 'FL', x: -1, y: 0, z: 1 },
{ id: 'BL', x: -1, y: 0, z: -1 },
{ id: 'BR', x: 1, y: 0, z: -1 },
{ id: 'DF', x: 0, y: -1, z: 1 },
{ id: 'DL', x: -1, y: -1, z: 0 },
{ id: 'DB', x: 0, y: -1, z: -1 },
{ id: 'DR', x: 1, y: -1, z: 0 }
];
const CENTERS = [
{ id: 'c0', x: 0, y: 1, z: 0, faces: { up: 'white' } },
{ id: 'c1', x: 0, y: -1, z: 0, faces: { down: 'yellow' } },
{ id: 'c2', x: 0, y: 0, z: 1, faces: { front: 'green' } },
{ id: 'c3', x: 0, y: 0, z: -1, faces: { back: 'blue' } },
{ id: 'c4', x: -1, y: 0, z: 0, faces: { left: 'orange' } },
{ id: 'c5', x: 1, y: 0, z: 0, faces: { right: 'red' } },
{ id: 'core', x: 0, y: 0, z: 0, faces: {} }
];
// Face mapping for pieces
// Each piece (e.g. URF) has 3 faces. We need to map them to colors based on orientation.
// Standard color scheme: U=white, D=yellow, F=green, B=blue, L=orange, R=red
const FACE_COLORS = {
U: 'white', D: 'yellow', F: 'green', B: 'blue', L: 'orange', R: 'red'
};
// Map piece name (e.g. 'URF') to its primary face keys
const CORNER_FACES = {
'URF': ['up', 'right', 'front'],
'ULF': ['up', 'front', 'left'],
'ULB': ['up', 'left', 'back'],
'URB': ['up', 'back', 'right'],
'DRF': ['down', 'right', 'front'],
'DLF': ['down', 'left', 'front'],
'DLB': ['down', 'back', 'left'],
'DRB': ['down', 'right', 'back']
};
const EDGE_FACES = {
'UF': ['up', 'front'],
'UL': ['up', 'left'],
'UB': ['up', 'back'],
'UR': ['up', 'right'],
'FR': ['front', 'right'],
'FL': ['front', 'left'],
'BL': ['back', 'left'],
'BR': ['back', 'right'],
'DF': ['down', 'front'],
'DL': ['down', 'left'],
'DB': ['down', 'back'],
'DR': ['down', 'right']
};
// Map piece name to its solved colors
const getCornerColors = (name) => {
// URF -> white, red, green
const map = {
'URF': ['white', 'red', 'green'],
'ULF': ['white', 'green', 'orange'],
'ULB': ['white', 'orange', 'blue'],
'URB': ['white', 'blue', 'red'],
'DRF': ['yellow', 'red', 'green'],
'DLF': ['yellow', 'orange', 'green'], // Adjusted to match DLF face order (D, L, F)
'DLB': ['yellow', 'blue', 'orange'], // Adjusted to match DLB face order (D, B, L)
'DRB': ['yellow', 'red', 'blue'] // Adjusted to match DRB face order (D, R, B)
};
return map[name];
};
const getEdgeColors = (name) => {
const map = {
'UF': ['white', 'green'],
'UL': ['white', 'orange'],
'UB': ['white', 'blue'],
'UR': ['white', 'red'],
'FR': ['green', 'red'],
'FL': ['green', 'orange'],
'BL': ['blue', 'orange'],
'BR': ['blue', 'red'],
'DF': ['yellow', 'green'],
'DL': ['yellow', 'orange'],
'DB': ['yellow', 'blue'],
'DR': ['yellow', 'red']
};
return map[name];
};
export class RubiksJSModel {
constructor() {
this.state = new State(false); // trackCenters=false
this.visual = new CubeModel();
}
reset() {
this.state = new State(false);
this.visual = new CubeModel();
}
rotateLayer(axis, index, dir) {
let move = '';
if (axis === 'y') {
if (index === 1) move = dir === 1 ? "U'" : "U";
else if (index === -1) move = dir === 1 ? "D'" : "D";
}
else if (axis === 'x') {
if (index === 1) move = dir === 1 ? "R'" : "R";
else if (index === -1) move = dir === 1 ? "L'" : "L";
}
else if (axis === 'z') {
if (index === 1) move = dir === 1 ? "F'" : "F";
else if (index === -1) move = dir === 1 ? "B'" : "B";
}
if (move) {
console.log('[RubiksJSModel] Applying move:', move);
try {
this.state.applyTurn(move);
console.log('[RubiksJSModel] Move applied successfully');
} catch (e) {
console.error('[RubiksJSModel] Failed to apply move:', move, e);
}
this.visual.rotateLayer(axis, index, dir);
}
}
applyTurn(move) {
if (!move) return;
try {
this.state.applyTurn(move);
} catch (e) {
console.error('[RubiksJSModel] Failed to apply direct move:', move, e);
}
this.visual.applyMove(move);
}
toCubies() {
return this.visual.toCubies();
}
validate() {
// State doesn't expose validate, but we can assume it's valid if using the library
return { valid: true, errors: [] };
}
}

View File

@@ -0,0 +1,58 @@
// 3D geometry helpers for cube face/axis operations and screen projection
export const getFaceNormal = (face, FACES) => {
const map = {
[FACES.FRONT]: { x: 0, y: 0, z: 1 },
[FACES.BACK]: { x: 0, y: 0, z: -1 },
[FACES.RIGHT]: { x: 1, y: 0, z: 0 },
[FACES.LEFT]: { x: -1, y: 0, z: 0 },
[FACES.UP]: { x: 0, y: 1, z: 0 },
[FACES.DOWN]: { x: 0, y: -1, z: 0 },
};
return map[face] || { x: 0, y: 0, z: 1 };
};
// Which axes can this face physically rotate along?
export const getAllowedAxes = (face, FACES) => {
switch (face) {
case FACES.FRONT:
case FACES.BACK:
return ["x", "y"];
case FACES.RIGHT:
case FACES.LEFT:
return ["z", "y"];
case FACES.UP:
case FACES.DOWN:
return ["x", "z"];
}
return [];
};
export const getAxisVector = (axis) => {
if (axis === "x") return { x: 1, y: 0, z: 0 };
if (axis === "y") return { x: 0, y: 1, z: 0 };
if (axis === "z") return { x: 0, y: 0, z: 1 };
return { x: 0, y: 0, z: 0 };
};
// Cross product: a × b
export const cross = (a, b) => ({
x: a.y * b.z - a.z * b.y,
y: a.z * b.x - a.x * b.z,
z: a.x * b.y - a.y * b.x,
});
// Project 3D vector to 2D screen space using a viewMatrix (column-major 4x4).
// Input v is in Right-Handed Math Coordinates (Y up).
// viewMatrix operates in CSS Coordinates (Y down).
// Applies T⁻¹ * M * T to maintain correct projection chirality.
export const project = (v, viewMatrix) => {
const m = viewMatrix;
const cssY = -v.y;
const x = v.x * m[0] + cssY * m[4] + v.z * m[8];
const projY = v.x * m[1] + cssY * m[5] + v.z * m[9];
const mathY = -projY;
return { x, y: mathY };
};

23
src/utils/easing.js Normal file
View File

@@ -0,0 +1,23 @@
// Animation easing functions and their derivatives
export const easeInOutCubic = (t) => {
if (t < 0.5) return 4 * t * t * t;
return 1 - Math.pow(-2 * t + 2, 3) / 2;
};
// Derivative of standard easeInOutCubic for instantaneous velocity calculations
export const easeInOutCubicDerivative = (t) => {
if (t < 0.5) return 12 * t * t;
return 3 * Math.pow(-2 * t + 2, 2);
};
// Custom easing function that preserves initial velocity v₀
// The polynomial is P(t) = (v₀ - 2)t³ + (3 - 2v₀)t² + v₀t
export const cubicEaseWithInitialVelocity = (t, v0) => {
return (v0 - 2) * t * t * t + (3 - 2 * v0) * t * t + v0 * t;
};
// Derivative of the custom easing function
export const cubicEaseWithInitialVelocityDerivative = (t, v0) => {
return 3 * (v0 - 2) * t * t + 2 * (3 - 2 * v0) * t + v0;
};

133
src/utils/matrix.js Normal file
View File

@@ -0,0 +1,133 @@
// 4x4 matrix operations for 3D transformations (column-major, CSS/WebGL convention)
export const identityMatrix = () => [
1, 0, 0, 0,
0, 1, 0, 0,
0, 0, 1, 0,
0, 0, 0, 1
];
export const rotateXMatrix = (deg) => {
const rad = (deg * Math.PI) / 180;
const c = Math.cos(rad);
const s = Math.sin(rad);
return [
1, 0, 0, 0,
0, c, s, 0,
0, -s, c, 0,
0, 0, 0, 1
];
};
export const rotateYMatrix = (deg) => {
const rad = (deg * Math.PI) / 180;
const c = Math.cos(rad);
const s = Math.sin(rad);
return [
c, 0, -s, 0,
0, 1, 0, 0,
s, 0, c, 0,
0, 0, 0, 1
];
};
export const rotateZMatrix = (deg) => {
const rad = (deg * Math.PI) / 180;
const c = Math.cos(rad);
const s = Math.sin(rad);
return [
c, s, 0, 0,
-s, c, 0, 0,
0, 0, 1, 0,
0, 0, 0, 1
];
};
export const multiplyMatrices = (a, b) => {
const result = new Array(16).fill(0);
for (let r = 0; r < 4; r++) {
for (let c = 0; c < 4; c++) {
for (let k = 0; k < 4; k++) {
result[c * 4 + r] += a[k * 4 + r] * b[c * 4 + k];
}
}
}
return result;
};
// --- Quaternion helpers for distortion-free rotation interpolation (SLERP) ---
export const matToQuat = (m) => {
const trace = m[0] + m[5] + m[10];
let w, x, y, z;
if (trace > 0) {
const s = 0.5 / Math.sqrt(trace + 1);
w = 0.25 / s;
x = (m[6] - m[9]) * s;
y = (m[8] - m[2]) * s;
z = (m[1] - m[4]) * s;
} else if (m[0] > m[5] && m[0] > m[10]) {
const s = 2 * Math.sqrt(1 + m[0] - m[5] - m[10]);
w = (m[6] - m[9]) / s;
x = 0.25 * s;
y = (m[4] + m[1]) / s;
z = (m[8] + m[2]) / s;
} else if (m[5] > m[10]) {
const s = 2 * Math.sqrt(1 + m[5] - m[0] - m[10]);
w = (m[8] - m[2]) / s;
x = (m[4] + m[1]) / s;
y = 0.25 * s;
z = (m[6] + m[9]) / s;
} else {
const s = 2 * Math.sqrt(1 + m[10] - m[0] - m[5]);
w = (m[1] - m[4]) / s;
x = (m[8] + m[2]) / s;
y = (m[6] + m[9]) / s;
z = 0.25 * s;
}
return { w, x, y, z };
};
export const slerp = (q1, q2, t) => {
let dot = q1.w * q2.w + q1.x * q2.x + q1.y * q2.y + q1.z * q2.z;
let q2n = q2;
if (dot < 0) {
q2n = { w: -q2.w, x: -q2.x, y: -q2.y, z: -q2.z };
dot = -dot;
}
if (dot > 0.9995) {
const len = Math.sqrt(
(q1.w + t * (q2n.w - q1.w)) ** 2 + (q1.x + t * (q2n.x - q1.x)) ** 2 +
(q1.y + t * (q2n.y - q1.y)) ** 2 + (q1.z + t * (q2n.z - q1.z)) ** 2
);
return {
w: (q1.w + t * (q2n.w - q1.w)) / len,
x: (q1.x + t * (q2n.x - q1.x)) / len,
y: (q1.y + t * (q2n.y - q1.y)) / len,
z: (q1.z + t * (q2n.z - q1.z)) / len,
};
}
const theta = Math.acos(dot);
const sinTheta = Math.sin(theta);
const a = Math.sin((1 - t) * theta) / sinTheta;
const b = Math.sin(t * theta) / sinTheta;
return {
w: a * q1.w + b * q2n.w,
x: a * q1.x + b * q2n.x,
y: a * q1.y + b * q2n.y,
z: a * q1.z + b * q2n.z,
};
};
export const quatToMat = (q) => {
const { w, x, y, z } = q;
const xx = x * x, yy = y * y, zz = z * z;
const xy = x * y, xz = x * z, yz = y * z;
const wx = w * x, wy = w * y, wz = w * z;
return [
1 - 2 * (yy + zz), 2 * (xy + wz), 2 * (xz - wy), 0,
2 * (xy - wz), 1 - 2 * (xx + zz), 2 * (yz + wx), 0,
2 * (xz + wy), 2 * (yz - wx), 1 - 2 * (xx + yy), 0,
0, 0, 0, 1,
];
};

116
src/utils/moveMapping.js Normal file
View File

@@ -0,0 +1,116 @@
// Move notation mapping between UI labels, internal logic axes, and solver output.
// The UI coordinate system is rotated 90° around Y from internal coordinates.
// UI button key → internal base + modifier
export const MOVE_MAP = {
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" },
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" },
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" },
};
// 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',
};
// 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 };
};
// 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;
};
// 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 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;
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;
};
// 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;
};
// 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 '';
let modifier = "";
if (stepsMod === 1) modifier = "'";
else if (stepsMod === 2) modifier = "2";
else if (stepsMod === 3) modifier = "";
return displayBase + (modifier === "'" ? "'" : modifier === "2" ? "2" : "");
};

View File

@@ -0,0 +1,374 @@
import { DeepCube, MOVES } from "../DeepCube.js";
export class BeginnerSolver {
constructor(cube) {
this.cube = cube.clone();
this.solution = [];
}
apply(moveStr) {
if (!moveStr) return;
const moveArr = moveStr.split(" ").filter((m) => m);
for (const m of moveArr) {
if (!MOVES[m]) throw new Error(`Invalid move: ${m}`);
this.solution.push(m);
this.cube = this.cube.multiply(MOVES[m]);
}
}
solve() {
this.solution = [];
if (this.isSolvedState(this.cube)) return [];
console.log("Starting Cross");
this.solveCross();
console.log("Starting F2L Corners");
this.solveF2LCorners();
console.log("Starting F2L Edges");
this.solveF2LEdges();
console.log("Starting Yellow Cross");
this.solveYellowCross();
console.log("Starting Yellow OLL");
this.orientYellowCorners();
console.log("Starting Yellow PLL");
this.permuteYellowCorners();
this.permuteYellowEdges();
this.alignUFace();
this.optimizeSolution();
return this.solution;
}
isSolvedState(state) {
for (let i = 0; i < 8; i++)
if (state.cp[i] !== i || state.co[i] !== 0) return false;
for (let i = 0; i < 12; i++)
if (state.ep[i] !== i || state.eo[i] !== 0) return false;
return true;
}
testAlg(algStr, targetId, isCorner) {
let temp = this.cube;
const arr = algStr.split(" ").filter((m) => m);
for (const m of arr) temp = temp.multiply(MOVES[m]);
if (isCorner) {
return temp.cp[targetId] === targetId && temp.co[targetId] === 0;
} else {
return temp.ep[targetId] === targetId && temp.eo[targetId] === 0;
}
}
solveCross() {
const targets = [
{ id: 5, up: 1, ins: ["F2", "U' R' F R", "U L F' L'"] }, // DF
{ id: 4, up: 0, ins: ["R2", "U' B' R B", "U F R' F'"] }, // DR
{ id: 7, up: 3, ins: ["B2", "U' L' B L", "U R B' R'"] }, // DB
{ id: 6, up: 2, ins: ["L2", "U' F' L F", "U B L' B'"] }, // DL
];
for (let t of targets) {
let safetyCount = 0;
while (safetyCount++ < 15) {
let pos = this.cube.ep.indexOf(t.id);
if (pos === t.id && this.cube.eo[pos] === 0) break;
if ([4, 5, 6, 7].includes(pos)) {
if (pos === 5) this.apply("F2");
else if (pos === 4) this.apply("R2");
else if (pos === 7) this.apply("B2");
else if (pos === 6) this.apply("L2");
} else if ([8, 9, 10, 11].includes(pos)) {
if (pos === 8) this.apply("R U R'");
else if (pos === 9) this.apply("F U F'");
else if (pos === 10) this.apply("L U L'");
else if (pos === 11) this.apply("B U B'");
} else if ([0, 1, 2, 3].includes(pos)) {
let success = false;
for (let u = 0; u < 4; u++) {
for (let alg of t.ins) {
if (this.testAlg(alg, t.id, false)) {
this.apply(alg);
success = true;
break;
}
}
if (success) break;
this.apply("U");
}
if (success) break;
}
}
}
}
solveF2LCorners() {
const targets = [
{ id: 4, ext: "R U R'", ins: ["R U2 R' U' R U R'", "R U R'", "F' U' F"] },
{ id: 5, ext: "F U F'", ins: ["F U2 F' U' F U F'", "F U F'", "L' U' L"] },
{ id: 6, ext: "L U L'", ins: ["L U2 L' U' L U L'", "L U L'", "B' U' B"] },
{ id: 7, ext: "B U B'", ins: ["B U2 B' U' B U B'", "B U B'", "R' U' R"] },
];
for (let t of targets) {
let safetyCount = 0;
while (safetyCount++ < 15) {
let pos = this.cube.cp.indexOf(t.id);
if (pos === t.id && this.cube.co[pos] === 0) break;
if ([4, 5, 6, 7].includes(pos)) {
if (pos === 4) this.apply("R U R'");
else if (pos === 5) this.apply("F U F'");
else if (pos === 6) this.apply("L U L'");
else if (pos === 7) this.apply("B U B'");
} else if ([0, 1, 2, 3].includes(pos)) {
let success = false;
for (let u = 0; u < 4; u++) {
for (let alg of t.ins) {
if (this.testAlg(alg, t.id, true)) {
this.apply(alg);
success = true;
break;
}
}
if (success) break;
this.apply("U");
}
if (success) break;
}
}
}
}
solveF2LEdges() {
const targets = [
{
id: 8,
ext: "R U R'",
ins: ["U R U' R' U' F' U F", "U' F' U F U R U' R'"],
},
{
id: 9,
ext: "F U F'",
ins: ["U' L' U L U F U' F'", "U F U' F' U' L' U L"],
},
{
id: 10,
ext: "L U L'",
ins: ["U L U' L' U' B' U B", "U' B' U B U L U' L'"],
},
{
id: 11,
ext: "B U B'",
ins: ["U B U' B' U' R' U R", "U' R' U R U B U' B'"],
},
];
for (let t of targets) {
let safetyCount = 0;
while (safetyCount++ < 15) {
let pos = this.cube.ep.indexOf(t.id);
if (pos === t.id && this.cube.eo[pos] === 0) break;
if ([8, 9, 10, 11].includes(pos)) {
if (pos === 8)
this.apply("R U R' U' F' U' F"); // Extract standard way
else if (pos === 9) this.apply("F U F' U' L' U' L");
else if (pos === 10) this.apply("L U L' U' B' U' B");
else if (pos === 11) this.apply("B U B' U' R' U' R");
} else if ([0, 1, 2, 3].includes(pos)) {
let success = false;
for (let u = 0; u < 4; u++) {
for (let alg of t.ins) {
if (this.testAlg(alg, t.id, false)) {
this.apply(alg);
success = true;
break;
}
}
if (success) break;
this.apply("U");
}
if (success) break;
}
}
}
}
solveYellowCross() {
const getOrientedCount = () =>
[0, 1, 2, 3].filter((i) => this.cube.eo[i] === 0).length;
let safetyCount = 0;
while (getOrientedCount() < 4 && safetyCount++ < 10) {
const oriented = [0, 1, 2, 3].filter((i) => this.cube.eo[i] === 0);
if (oriented.length === 0) {
this.apply("F R U R' U' F'");
} else if (oriented.length === 2) {
const [a, b] = oriented;
if (Math.abs(a - b) === 2 || (a === 0 && b === 3)) {
// Line or L-shape handling simplified
let succ = false;
for (let u = 0; u < 4; u++) {
let tmp = this.cube.clone();
let p1 = (temp) => {
let c = temp.clone();
"F R U R' U' F'"
.split(" ")
.filter((x) => x)
.forEach((m) => (c = c.multiply(MOVES[m])));
return c;
};
let p2 = (temp) => {
let c = temp.clone();
"F U R U' R' F'"
.split(" ")
.filter((x) => x)
.forEach((m) => (c = c.multiply(MOVES[m])));
return c;
};
if ([0, 1, 2, 3].filter((i) => p1(tmp).eo[i] === 0).length === 4) {
this.apply("F R U R' U' F'");
succ = true;
break;
}
if ([0, 1, 2, 3].filter((i) => p2(tmp).eo[i] === 0).length === 4) {
this.apply("F U R U' R' F'");
succ = true;
break;
}
this.apply("U");
}
if (!succ) this.apply("F R U R' U' F'"); // fallback
} else {
this.apply("U");
}
}
}
}
orientYellowCorners() {
let safetyCount = 0;
while (safetyCount++ < 25) {
if ([0, 1, 2, 3].filter((i) => this.cube.co[i] === 0).length === 4) break;
if (this.cube.co[0] === 0) this.apply("U");
else this.apply("R' D' R D R' D' R D");
}
}
permuteYellowCorners() {
let safetyCount = 0;
while (safetyCount++ < 15) {
let c0 = this.cube.cp[0],
c1 = this.cube.cp[1],
c2 = this.cube.cp[2],
c3 = this.cube.cp[3];
if (
(c1 - c0 + 4) % 4 === 1 &&
(c2 - c1 + 4) % 4 === 1 &&
(c3 - c2 + 4) % 4 === 1
)
break;
let succ = false;
for (let u = 0; u < 4; u++) {
for (let alg of [
"R' F R' B2 R F' R' B2 R2",
"R B' R F2 R' B R F2 R2",
]) {
let t = this.cube.clone();
alg.split(" ").forEach((m) => (t = t.multiply(MOVES[m])));
let tc0 = t.cp[0],
tc1 = t.cp[1],
tc2 = t.cp[2],
tc3 = t.cp[3];
if (
(tc1 - tc0 + 4) % 4 === 1 &&
(tc2 - tc1 + 4) % 4 === 1 &&
(tc3 - tc2 + 4) % 4 === 1
) {
this.apply(alg);
succ = true;
break;
}
}
if (succ) break;
this.apply("U");
}
if (succ) break;
this.apply("R' F R' B2 R F' R' B2 R2");
}
}
permuteYellowEdges() {
let s = 0;
while (this.cube.cp[0] !== 0 && s++ < 5) this.apply("U");
let safetyCount = 0;
while (safetyCount++ < 10) {
if (
this.cube.ep[0] === 0 &&
this.cube.ep[1] === 1 &&
this.cube.ep[2] === 2 &&
this.cube.ep[3] === 3
)
break;
let succ = false;
const uMoves = ["", "U ", "U2 ", "U' "];
const uMovesInv = ["", "U' ", "U2 ", "U "];
for (let u = 0; u < 4; u++) {
for (let baseAlg of [
"R U' R U R U R U' R' U' R2",
"L' U L' U' L' U' L' U L U L2",
]) {
const fullAlg = uMoves[u] + baseAlg + " " + uMovesInv[u];
let t = this.cube.clone();
fullAlg
.split(" ")
.filter((x) => x)
.forEach((m) => (t = t.multiply(MOVES[m])));
if (
t.ep[0] === 0 &&
t.ep[1] === 1 &&
t.ep[2] === 2 &&
t.ep[3] === 3
) {
this.apply(fullAlg);
succ = true;
break;
}
}
if (succ) break;
}
if (succ) break;
this.apply("R U' R U R U R U' R' U' R2"); // Fallback cycle
}
}
alignUFace() {
let s = 0;
while (this.cube.cp[0] !== 0 && s++ < 5) this.apply("U");
}
optimizeSolution() {
let stable = false;
while (!stable) {
stable = true;
for (let i = 0; i < this.solution.length - 1; i++) {
const a = this.solution[i];
const b = this.solution[i + 1];
if (a[0] === b[0]) {
const val = (m) => (m.includes("'") ? -1 : m.includes("2") ? 2 : 1);
let sum = (val(a) + val(b)) % 4;
if (sum < 0) sum += 4;
this.solution.splice(i, 2);
if (sum === 1) this.solution.splice(i, 0, a[0]);
else if (sum === 2) this.solution.splice(i, 0, a[0] + "2");
else if (sum === 3) this.solution.splice(i, 0, a[0] + "'");
stable = false;
break;
}
}
}
}
}

View File

@@ -0,0 +1,140 @@
import Cube from "cubejs";
import { DeepCube, CORNERS, EDGES } from "../DeepCube.js";
export class KociembaSolver {
static init() {
Cube.initSolver();
}
constructor(cube) {
this.cube = cube.clone();
}
// Convert DeepCube permutation/orientation to Kociemba facelet string
// Kociemba format: U1..U9 R1..R9 F1..F9 D1..D9 L1..L9 B1..B9
toFaceletString() {
// Array of 54 characters representing the 6 faces.
// 0..8 = U
// 9..17 = R
// 18..26 = F
// 27..35 = D
// 36..44 = L
// 45..53 = B
const f = new Array(54).fill(" ");
// Centers
f[4] = "U";
f[13] = "R";
f[22] = "F";
f[31] = "D";
f[40] = "L";
f[49] = "B";
// DeepCube to Kociemba mapping:
// Corners:
// 0: URF, 1: UFL, 2: ULB, 3: UBR, 4: DFR, 5: DLF, 6: DBL, 7: DRB
// Edges:
// 0: UR, 1: UF, 2: UL, 3: UB, 4: DR, 5: DF, 6: DL, 7: DB, 8: FR, 9: FL, 10: BL, 11: BR
const cornerColors = [
["U", "R", "F"], // 0: URF
["U", "F", "L"], // 1: UFL
["U", "L", "B"], // 2: ULB
["U", "B", "R"], // 3: UBR
["D", "F", "R"], // 4: DFR
["D", "L", "F"], // 5: DLF
["D", "B", "L"], // 6: DBL
["D", "R", "B"], // 7: DRB
];
const cornerFacelets = [
[8, 9, 20], // URF (U9, R1, F3)
[6, 18, 38], // UFL (U7, F1, L3)
[0, 36, 47], // ULB (U1, L1, B3)
[2, 45, 11], // UBR (U3, B1, R3)
[29, 26, 15], // DFR (D3, F9, R7)
[27, 44, 24], // DLF (D1, L9, F7)
[33, 53, 42], // DBL (D7, B9, L7)
[35, 17, 51], // DRB (D9, R9, B7)
];
for (let i = 0; i < 8; i++) {
const perm = this.cube.cp[i];
const ori = this.cube.co[i];
// The physical piece at position `i` is `perm`.
// Its colors are cornerColors[perm].
// Because of orientation, the colors are shifted.
// If ori=0, U/D color is on U/D face.
// If ori=1, U/D color is twisted clockwise.
// If ori=2, U/D color is twisted counter-clockwise.
const c0 = cornerColors[perm][(0 - ori + 3) % 3];
const c1 = cornerColors[perm][(1 - ori + 3) % 3];
const c2 = cornerColors[perm][(2 - ori + 3) % 3];
f[cornerFacelets[i][0]] = c0;
f[cornerFacelets[i][1]] = c1;
f[cornerFacelets[i][2]] = c2;
}
const edgeColors = [
["U", "R"], // 0: UR
["U", "F"], // 1: UF
["U", "L"], // 2: UL
["U", "B"], // 3: UB
["D", "R"], // 4: DR
["D", "F"], // 5: DF
["D", "L"], // 6: DL
["D", "B"], // 7: DB
["F", "R"], // 8: FR
["F", "L"], // 9: FL
["B", "L"], // 10: BL
["B", "R"], // 11: BR
];
const edgeFacelets = [
[5, 10], // UR (U6, R2)
[7, 19], // UF (U8, F2)
[3, 37], // UL (U4, L2)
[1, 46], // UB (U2, B2)
[32, 16], // DR (D6, R8)
[28, 25], // DF (D2, F8)
[30, 43], // DL (D4, L8)
[34, 52], // DB (D8, B8)
[23, 12], // FR (F6, R4)
[21, 41], // FL (F4, L6)
[50, 39], // BL (B6, L4)
[48, 14], // BR (B4, R6)
];
for (let i = 0; i < 12; i++) {
const perm = this.cube.ep[i];
const ori = this.cube.eo[i];
const e0 = edgeColors[perm][(0 + ori) % 2];
const e1 = edgeColors[perm][(1 + ori) % 2];
f[edgeFacelets[i][0]] = e0;
f[edgeFacelets[i][1]] = e1;
}
return f.join("");
}
solve() {
const faceletStr = this.toFaceletString();
try {
const cube = Cube.fromString(faceletStr);
if (cube.isSolved()) return [];
const solution = cube.solve();
if (!solution) return [];
return solution.split(" ").filter((m) => m);
} catch (e) {
throw new Error(
`Kociemba Solve Failed: ${e.message} \nFacelet: ${faceletStr}`,
);
}
}
}

31
src/utils/toastHelper.js Normal file
View File

@@ -0,0 +1,31 @@
import Toastify from "toastify-js";
const ICONS = {
info: '<circle cx="12" cy="12" r="10"/><path d="M12 16v-4"/><path d="M12 8h.01"/>',
alert: '<path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3Z"/><path d="M12 9v4"/><path d="M12 17h.01"/>',
check: '<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><path d="m9 11 3 3L22 4"/>'
};
export const createToastHtml = (text, iconName = 'info') => {
const innerHtml = ICONS[iconName] || ICONS.info;
const size = 26; // Powiększona ikona
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-${iconName}">${innerHtml}</svg>`;
return `<div style="display: flex; align-items: center; gap: 8px;">
${svg}
<span>${text}</span>
</div>`;
};
export const showToast = (text, iconName = 'info', options = {}) => {
Toastify({
text: createToastHtml(text, iconName),
escapeMarkup: false,
duration: 3000,
gravity: "top",
position: "center",
stopOnFocus: true,
...options
}).showToast();
};

36
src/utils/tokenReducer.js Normal file
View File

@@ -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 };
};

View File

@@ -1,21 +1,29 @@
import { RubiksJSModel } from '../utils/RubiksJSModel.js';
import { RubiksJSModel } from "../utils/CubeLogicAdapter.js";
const cube = new RubiksJSModel();
// Helper to send state update
const sendUpdate = () => {
try {
const cubies = cube.toCubies();
// console.log('[Worker] Sending update with cubies:', cubies.length);
const { cp, co, ep, eo } = cube.state;
postMessage({
type: 'STATE_UPDATE',
type: "STATE_UPDATE",
payload: {
cubies
}
cubies,
deepCubeState: {
cp: [...cp],
co: [...co],
ep: [...ep],
eo: [...eo],
},
},
});
} catch (e) {
console.error('[Worker] Error generating cubies:', e);
postMessage({ type: 'ERROR', payload: e.message });
console.error("[Worker] Error generating cubies:", e);
postMessage({ type: "ERROR", payload: e.message });
}
};
@@ -23,32 +31,41 @@ self.onmessage = (e) => {
const { type, payload } = e.data;
switch (type) {
case 'INIT':
case 'RESET':
case "INIT":
case "RESET":
cube.reset();
sendUpdate();
break;
case 'ROTATE_LAYER': {
const { axis, index, direction } = payload;
cube.rotateLayer(axis, index, direction);
case "ROTATE_LAYER": {
const { axis, index, direction, steps = 1 } = payload;
cube.rotateLayer(axis, index, direction, steps);
sendUpdate();
break;
}
case 'TURN': {
case "ROTATE_SLICE": {
const { axis, direction, steps = 1 } = payload;
cube.rotateSlice(axis, direction, steps);
sendUpdate();
break;
}
case "TURN": {
const { move } = payload;
cube.applyTurn(move);
sendUpdate();
break;
}
case 'VALIDATE':
case "VALIDATE":
const validation = cube.validate();
postMessage({
type: 'VALIDATION_RESULT',
payload: { valid: validation.valid, errors: validation.errors }
type: "VALIDATION_RESULT",
payload: { valid: validation.valid, errors: validation.errors },
});
break;
}
};

View File

@@ -0,0 +1,58 @@
import { DeepCube } from "../utils/DeepCube.js";
import { KociembaSolver } from "../utils/solvers/KociembaSolver.js";
import { BeginnerSolver } from "../utils/solvers/BeginnerSolver.js";
let isKociembaReady = false;
// Defer heavy initialization to allow the worker to be responsive initially
setTimeout(() => {
console.log("[SolverWorker] Kociemba solver initialization");
console.time("[SolverWorker] Kociemba solver initialized");
KociembaSolver.init();
console.timeEnd("[SolverWorker] Kociemba solver initialized");
isKociembaReady = true;
postMessage({ type: "INIT_DONE" });
}, 50);
self.onmessage = (e) => {
const { type, payload } = e.data;
if (type === "SOLVE") {
const { solverType, cubeState } = payload;
if (solverType === "kociemba" && !isKociembaReady) {
postMessage({ type: "SOLVE_ERROR", payload: "wait for initialize solver" });
return;
}
try {
// Reconstruct DeepCube state from payload
const dc = new DeepCube(
new Int8Array(cubeState.cp),
new Int8Array(cubeState.co),
new Int8Array(cubeState.ep),
new Int8Array(cubeState.eo)
);
let solution = [];
if (solverType === "kociemba") {
const solver = new KociembaSolver(dc);
solution = solver.solve();
} else if (solverType === "beginner") {
const solver = new BeginnerSolver(dc);
solution = solver.solve();
} else {
throw new Error(`Unknown solver type: ${solverType}`);
}
postMessage({
type: "SOLVE_RESULT",
payload: solution,
});
} catch (err) {
console.error("[SolverWorker] Solve error:", err);
postMessage({ type: "SOLVE_ERROR", payload: err.message });
postMessage({ type: "SOLVE_RESULT", payload: [] });
}
}
};

View File

@@ -1,161 +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');

View File

@@ -1,96 +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');
}

View File

@@ -1,109 +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');

24
test/debug_kociemba.js Normal file
View File

@@ -0,0 +1,24 @@
import { DeepCube, MOVES } from "../src/utils/DeepCube.js";
import { KociembaSolver } from "../src/utils/solvers/KociembaSolver.js";
let cube = new DeepCube();
const faceletStart = new KociembaSolver(cube).toFaceletString();
console.log("Solved Facelet:");
console.log(faceletStart);
cube = cube.multiply(MOVES["R"]);
const solverR = new KociembaSolver(cube);
const faceletR = solverR.toFaceletString();
console.log("Facelet after R:");
console.log(faceletR);
["U", "D", "R", "L", "F", "B"].forEach((m) => {
let c = new DeepCube().multiply(MOVES[m]);
let solver = new KociembaSolver(c);
try {
console.log(`Solution for ${m}:`, solver.solve().join(" "));
} catch (e) {
console.log(`Error on ${m}:`, e.message);
}
});

197
test/generate_math.js Normal file
View File

@@ -0,0 +1,197 @@
const C = ["URF", "UFL", "ULB", "UBR", "DFR", "DLF", "DBL", "DRB"];
const E = [
"UR",
"UF",
"UL",
"UB",
"DR",
"DF",
"DL",
"DB",
"FR",
"FL",
"BL",
"BR",
];
// Define physical coordinates for all 6 center stickers
const faces = {
U: [0, 1, 0],
D: [0, -1, 0],
R: [1, 0, 0],
L: [-1, 0, 0],
F: [0, 0, 1],
B: [0, 0, -1],
};
// 8 corners, each with 3 stickers
// URF corner has stickers pointing U, R, F
const cornerStickers = [
["U", "R", "F"],
["U", "F", "L"],
["U", "L", "B"],
["U", "B", "R"],
["D", "F", "R"],
["D", "L", "F"],
["D", "B", "L"],
["D", "R", "B"],
];
// 12 edges, each with 2 stickers
const edgeStickers = [
["U", "R"],
["U", "F"],
["U", "L"],
["U", "B"],
["D", "R"],
["D", "F"],
["D", "L"],
["D", "B"],
["F", "R"],
["F", "L"],
["B", "L"],
["B", "R"],
];
// Rotate a 3D vector around an axis by 90 deg clockwise looking at the face
function rotate(vec, axis) {
let [x, y, z] = vec;
// Holding the face and turning clockwise:
// U (Y+): Back(-Z) -> Right(+X) -> Front(+Z) -> Left(-X) -> Back(-Z)
// So X becomes Z, Z becomes -X
// Let's test UBR (X=1, Z=-1).
// Clockwise: UBR(TopRight) -> URF(BottomRight) -> UFL(BottomLeft) -> ULB(TopLeft).
// UBR (1,-1) -> URF (1,1). We need X'=1, Z'=1 from X=1, Z=-1.
// Formula for X'=1, Z'=1: X' = -Z, Z' = X.
// Let's try URF(1,1) -> UFL(-1,1): X' = -1, Z' = 1. matches X'=-Z, Z'=X.
// So U is [-z, y, x]
// D (Y-): Looking from bottom: Front(+Z) -> Right(+X) -> Back(-Z) -> Left(-X)
// So Front(Z=1) -> Right(X=1). Z'= -X? Yes. X'=Z.
// So D is [z, y, -x]
// R (X+): Up(+Y) -> Back(-Z) -> Down(-Y) -> Front(+Z)
// So Up(Y=1) -> Back(Z=-1). Y'= -Z? Yes. Z'=Y.
// So R is [x, -z, y]
// L (X-): Up(+Y) -> Front(+Z) -> Down(-Y) -> Back(-Z)
// So Up(Y=1) -> Front(Z=1). Y'= Z. Z'= -Y.
// So L is [x, z, -y]
// F (Z+): Up(+Y) -> Right(+X) -> Down(-Y) -> Left(-X)
// So Up(Y=1) -> Right(X=1). X'=Y. Y'=-X.
// So F is [y, -x, z]
// B (Z-): Up(+Y) -> Left(-X) -> Down(-Y) -> Right(+X)
// So Up(Y=1) -> Left(X=-1). X'=-Y. Y'=X.
// So B is [-y, x, z]
if (axis === "U") return [-z, y, x];
if (axis === "D") return [z, y, -x];
if (axis === "R") return [x, z, -y];
if (axis === "L") return [x, -z, y];
if (axis === "F") return [y, -x, z];
if (axis === "B") return [-y, x, z];
}
// Map a rotated vector back to a face name
function vecToFace(vec) {
for (let f in faces) {
if (
faces[f][0] === vec[0] &&
faces[f][1] === vec[1] &&
faces[f][2] === vec[2]
)
return f;
}
}
function generateMove(axis) {
let cp = [],
co = [],
ep = [],
eo = [];
// CORNERS
for (let c = 0; c < 8; c++) {
if (!cornerStickers[c].includes(axis)) {
cp[c] = c;
co[c] = 0;
continue;
}
let pos = [0, 0, 0];
cornerStickers[c].forEach((f) => {
pos[0] += faces[f][0];
pos[1] += faces[f][1];
pos[2] += faces[f][2];
});
let newPos = rotate(pos, axis);
let targetC = -1;
for (let i = 0; i < 8; i++) {
let p2 = [0, 0, 0];
cornerStickers[i].forEach((f) => {
p2[0] += faces[f][0];
p2[1] += faces[f][1];
p2[2] += faces[f][2];
});
if (p2[0] === newPos[0] && p2[1] === newPos[1] && p2[2] === newPos[2])
targetC = i;
}
cp[targetC] = c;
let rotatedStickers = cornerStickers[c].map((f) =>
vecToFace(rotate(faces[f], axis)),
);
let ori = cornerStickers[targetC].indexOf(rotatedStickers[0]);
co[targetC] = ori;
}
// EDGES
for (let e = 0; e < 12; e++) {
if (!edgeStickers[e].includes(axis)) {
ep[e] = e;
eo[e] = 0;
continue;
}
let pos = [0, 0, 0];
edgeStickers[e].forEach((f) => {
pos[0] += faces[f][0];
pos[1] += faces[f][1];
pos[2] += faces[f][2];
});
let newPos = rotate(pos, axis);
let targetE = -1;
for (let i = 0; i < 12; i++) {
let p2 = [0, 0, 0];
edgeStickers[i].forEach((f) => {
p2[0] += faces[f][0];
p2[1] += faces[f][1];
p2[2] += faces[f][2];
});
if (p2[0] === newPos[0] && p2[1] === newPos[1] && p2[2] === newPos[2])
targetE = i;
}
ep[targetE] = e;
let rotatedStickers = edgeStickers[e].map((f) =>
vecToFace(rotate(faces[f], axis)),
);
let primarySticker = rotatedStickers[0];
let ori = primarySticker === edgeStickers[targetE][0] ? 0 : 1;
eo[targetE] = ori;
}
return { cp, co, ep, eo };
}
const moves = ["U", "R", "F", "D", "L", "B"];
moves.forEach((m) => {
const res = generateMove(m);
console.log(`MOVES['${m}'] = new DeepCube(
[${res.cp.map((e) => `CORNERS.${C[e]}`).join(", ")}],
[${res.co.join(", ")}],
[${res.ep.map((e) => `EDGES.${E[e]}`).join(", ")}],
[${res.eo.join(", ")}]
)`);
});

36
test/math_output.txt Normal file
View File

@@ -0,0 +1,36 @@
MOVES['U'] = new DeepCube(
[CORNERS.UFL, CORNERS.ULB, CORNERS.UBR, CORNERS.URF, CORNERS.DFR, CORNERS.DLF, CORNERS.DBL, CORNERS.DRB],
[0, 0, 0, 0, 0, 0, 0, 0],
[EDGES.UF, EDGES.UL, EDGES.UB, EDGES.UR, EDGES.DR, EDGES.DF, EDGES.DL, EDGES.DB, EDGES.FR, EDGES.FL, EDGES.BL, EDGES.BR],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
)
MOVES['R'] = new DeepCube(
[CORNERS.DFR, CORNERS.UFL, CORNERS.ULB, CORNERS.URF, CORNERS.DRB, CORNERS.DLF, CORNERS.DBL, CORNERS.UBR],
[2, 0, 0, 1, 1, 0, 0, 2],
[EDGES.FR, EDGES.UF, EDGES.UL, EDGES.UB, EDGES.BR, EDGES.DF, EDGES.DL, EDGES.DB, EDGES.DR, EDGES.FL, EDGES.BL, EDGES.UR],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
)
MOVES['F'] = new DeepCube(
[CORNERS.UFL, CORNERS.DLF, CORNERS.ULB, CORNERS.UBR, CORNERS.URF, CORNERS.DFR, CORNERS.DBL, CORNERS.DRB],
[1, 2, 0, 0, 2, 1, 0, 0],
[EDGES.UR, EDGES.FL, EDGES.UL, EDGES.UB, EDGES.DR, EDGES.FR, EDGES.DL, EDGES.DB, EDGES.UF, EDGES.DF, EDGES.BL, EDGES.BR],
[0, 1, 0, 0, 0, 1, 0, 0, 1, 1, 0, 0]
)
MOVES['D'] = new DeepCube(
[CORNERS.URF, CORNERS.UFL, CORNERS.ULB, CORNERS.UBR, CORNERS.DLF, CORNERS.DBL, CORNERS.DRB, CORNERS.DFR],
[0, 0, 0, 0, 0, 0, 0, 0],
[EDGES.UR, EDGES.UF, EDGES.UL, EDGES.UB, EDGES.DF, EDGES.DL, EDGES.DB, EDGES.DR, EDGES.FR, EDGES.FL, EDGES.BL, EDGES.BR],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
)
MOVES['L'] = new DeepCube(
[CORNERS.URF, CORNERS.ULB, CORNERS.DBL, CORNERS.UBR, CORNERS.DFR, CORNERS.UFL, CORNERS.DLF, CORNERS.DRB],
[0, 1, 2, 0, 0, 2, 1, 0],
[EDGES.UR, EDGES.UF, EDGES.BL, EDGES.UB, EDGES.DR, EDGES.DF, EDGES.FL, EDGES.DB, EDGES.FR, EDGES.UL, EDGES.DL, EDGES.BR],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
)
MOVES['B'] = new DeepCube(
[CORNERS.URF, CORNERS.UFL, CORNERS.UBR, CORNERS.DRB, CORNERS.DFR, CORNERS.DLF, CORNERS.ULB, CORNERS.DBL],
[0, 0, 1, 2, 0, 0, 2, 1],
[EDGES.UR, EDGES.UF, EDGES.UL, EDGES.BR, EDGES.DR, EDGES.DF, EDGES.DL, EDGES.BL, EDGES.FR, EDGES.FL, EDGES.UB, EDGES.DB],
[0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 1, 1]
)

View File

@@ -1,15 +1,16 @@
import { Cube, FACES, COLORS } from '../src/utils/Cube.js';
import { Cube, FACES, COLORS } from "../src/utils/Cube.js";
// Helper to print face
const printFace = (matrix, name) => {
console.log(`--- ${name} ---`);
matrix.forEach(row => console.log(row.map(c => c ? c[0].toUpperCase() : '-').join(' ')));
matrix.forEach((row) =>
console.log(row.map((c) => (c ? c[0].toUpperCase() : "-")).join(" ")),
);
};
// Helper to check if a face matches expected color (center color)
const checkFaceColor = (matrix, expectedColor) => {
return matrix.every(row => row.every(c => c === expectedColor));
return matrix.every((row) => row.every((c) => c === expectedColor));
};
console.log("=== RUBIK'S CUBE SIMULATION & DIAGNOSTIC ===");
@@ -52,20 +53,28 @@ console.log("\n2. Simulating: Left Layer (x=-1) Rotation (L-like move)...");
// Try direction = 1
console.log("-> Applying rotateLayer('x', -1, 1)...");
cube.rotateLayer('x', -1, 1);
cube.rotateLayer("x", -1, 1);
state = cube.getState();
// Check result on Left Column of Front Face
// Front is Green. Top is White.
// If L (Drag Down): Front-Left-Col should be White.
const frontLeftCol = [state[FACES.FRONT][0][0], state[FACES.FRONT][1][0], state[FACES.FRONT][2][0]];
const frontLeftCol = [
state[FACES.FRONT][0][0],
state[FACES.FRONT][1][0],
state[FACES.FRONT][2][0],
];
console.log("Front Left Column colors:", frontLeftCol);
if (frontLeftCol.every(c => c === COLORS.WHITE)) {
console.log("✅ Result: Front got White (Top). This matches 'Drag Down' (L move).");
if (frontLeftCol.every((c) => c === COLORS.WHITE)) {
console.log(
"✅ Result: Front got White (Top). This matches 'Drag Down' (L move).",
);
console.log("=> CONCLUSION: direction=1 corresponds to Drag Down (L).");
} else if (frontLeftCol.every(c => c === COLORS.YELLOW)) {
console.log("⚠️ Result: Front got Yellow (Down). This matches 'Drag Up' (L' move).");
} else if (frontLeftCol.every((c) => c === COLORS.YELLOW)) {
console.log(
"⚠️ Result: Front got Yellow (Down). This matches 'Drag Up' (L' move).",
);
console.log("=> CONCLUSION: direction=1 corresponds to Drag Up (L').");
} else {
console.error("❌ Unexpected colors:", frontLeftCol);
@@ -89,7 +98,7 @@ cube.reset();
console.log("\n3. Simulating: Top Layer (y=1) Rotation...");
// Try direction = 1
console.log("-> Applying rotateLayer('y', 1, 1)...");
cube.rotateLayer('y', 1, 1);
cube.rotateLayer("y", 1, 1);
state = cube.getState();
// Check result on Top Row of Front Face
@@ -104,10 +113,10 @@ state = cube.getState();
const frontTopRow = state[FACES.FRONT][0];
console.log("Front Top Row colors:", frontTopRow);
if (frontTopRow.every(c => c === COLORS.ORANGE)) {
if (frontTopRow.every((c) => c === COLORS.ORANGE)) {
console.log("✅ Result: Front got Orange (Left). This matches 'Drag Right'.");
console.log("=> CONCLUSION: direction=1 corresponds to Drag Right.");
} else if (frontTopRow.every(c => c === COLORS.RED)) {
} else if (frontTopRow.every((c) => c === COLORS.RED)) {
console.log("⚠️ Result: Front got Red (Right). This matches 'Drag Left'.");
console.log("=> CONCLUSION: direction=1 corresponds to Drag Left.");
} else {

9
test/test_aperm.js Normal file
View File

@@ -0,0 +1,9 @@
import { DeepCube, MOVES } from '../src/utils/DeepCube.js';
let cube = new DeepCube();
const apply = (str) => { str.split(' ').filter(x => x).forEach(m => cube = cube.multiply(MOVES[m])); };
apply("R' F R' B2 R F' R' B2 R2");
console.log(`cp after A-perm:`, cube.cp.slice(0, 4));
// We want to see which two corners are swapped.
// Solved is 0,1,2,3.
// If it prints 0,1,3,2, then 2 and 3 are swapped (Back corners).

View File

@@ -0,0 +1,25 @@
import { DeepCube, MOVES } from '../src/utils/DeepCube.js';
import { BeginnerSolver } from '../src/utils/solvers/BeginnerSolver.js';
let cube = new DeepCube();
const scramble = "R U R' U' R' F R2 U' R' U' R U R' F'"; // T-perm
scramble.split(' ').forEach(move => {
cube = cube.multiply(MOVES[move]);
});
console.log('Testing BeginnerSolver with T-perm...');
const solver = new BeginnerSolver(cube);
// Add some logging to the solver's methods to trace execution
const originalApply = solver.apply.bind(solver);
solver.apply = (moveStr) => {
// console.log('Applying:', moveStr);
originalApply(moveStr);
};
try {
const solution = solver.solve();
console.log('Solution found:', solution.join(' '));
} catch (e) {
console.error('Error during solve:', e);
}

View File

@@ -0,0 +1,41 @@
import { DeepCube, MOVES } from "../src/utils/DeepCube.js";
import { BeginnerSolver } from "../src/utils/solvers/BeginnerSolver.js";
const allMoves = Object.keys(MOVES);
const getRandomScramble = (length = 20) => {
let s = [];
for (let i = 0; i < length; i++)
s.push(allMoves[Math.floor(Math.random() * allMoves.length)]);
return s.join(" ");
};
for (let i = 1; i <= 20; i++) {
let cube = new DeepCube();
const scramble = getRandomScramble();
scramble.split(" ").forEach((move) => (cube = cube.multiply(MOVES[move])));
const startTime = Date.now();
const solver = new BeginnerSolver(cube);
try {
const solution = solver.solve();
const elapsedTime = Date.now() - startTime;
console.log(
`Test ${i}: Solved in ${elapsedTime}ms. Solution length: ${solution.length}`,
);
// Verify it actually solved it
let testCube = cube.clone();
solution.forEach((m) => (testCube = testCube.multiply(MOVES[m])));
if (!solver.isSolvedState(testCube)) {
console.error(
`ERROR: Test ${i} failed to fully solve the cube mathematically!`,
);
process.exit(1);
}
} catch (e) {
console.error(`ERROR: Test ${i} threw an exception:`, e);
process.exit(1);
}
}
console.log("All 20 tests passed flawlessly!");

34
test/test_diagnostics.js Normal file
View File

@@ -0,0 +1,34 @@
import { DeepCube, MOVES } from '../src/utils/DeepCube.js';
import { BeginnerSolver } from '../src/utils/solvers/BeginnerSolver.js';
const allMoves = Object.keys(MOVES);
const getRandomScramble = (length = 20) => {
let s = [];
for (let i = 0; i < length; i++) s.push(allMoves[Math.floor(Math.random() * allMoves.length)]);
return s.join(' ');
};
let cube = new DeepCube();
const scramble = getRandomScramble();
scramble.split(' ').forEach(move => cube = cube.multiply(MOVES[move]));
const solver = new BeginnerSolver(cube);
solver.solve();
console.log("Check Cross:");
for (let i of [4, 5, 6, 7]) console.log(`Edge ${i}: ep=${solver.cube.ep.indexOf(i)} eo=${solver.cube.eo[solver.cube.ep.indexOf(i)]}`);
console.log("Check F2L Corners:");
for (let i of [4, 5, 6, 7]) console.log(`Corner ${i}: cp=${solver.cube.cp.indexOf(i)} co=${solver.cube.co[solver.cube.cp.indexOf(i)]}`);
console.log("Check F2L Edges:");
for (let i of [8, 9, 10, 11]) console.log(`Edge ${i}: ep=${solver.cube.ep.indexOf(i)} eo=${solver.cube.eo[solver.cube.ep.indexOf(i)]}`);
console.log("Check OLL:");
console.log(`co:`, solver.cube.co.slice(0, 4));
console.log(`eo:`, solver.cube.eo.slice(0, 4));
console.log("Check PLL:");
console.log(`cp:`, solver.cube.cp.slice(0, 4));
console.log(`ep:`, solver.cube.ep.slice(0, 4));

40
test/test_diagnostics2.js Normal file
View File

@@ -0,0 +1,40 @@
import { DeepCube, MOVES } from '../src/utils/DeepCube.js';
import { BeginnerSolver } from '../src/utils/solvers/BeginnerSolver.js';
const allMoves = Object.keys(MOVES);
const getRandomScramble = (length = 20) => {
let s = [];
for (let i = 0; i < length; i++) s.push(allMoves[Math.floor(Math.random() * allMoves.length)]);
return s.join(' ');
};
for (let iter = 0; iter < 100; iter++) {
let cube = new DeepCube();
const scramble = getRandomScramble();
scramble.split(' ').forEach(move => cube = cube.multiply(MOVES[move]));
const solver = new BeginnerSolver(cube);
solver.solve();
if (!solver.isSolvedState(solver.cube)) {
console.log("FAILED ON SCRAMBLE:", scramble);
console.log("Check Cross:");
for (let i of [4, 5, 6, 7]) console.log(`Edge ${i}: ep=${solver.cube.ep.indexOf(i)} eo=${solver.cube.eo[solver.cube.ep.indexOf(i)]}`);
console.log("Check F2L Corners:");
for (let i of [4, 5, 6, 7]) console.log(`Corner ${i}: cp=${solver.cube.cp.indexOf(i)} co=${solver.cube.co[solver.cube.cp.indexOf(i)]}`);
console.log("Check F2L Edges:");
for (let i of [8, 9, 10, 11]) console.log(`Edge ${i}: ep=${solver.cube.ep.indexOf(i)} eo=${solver.cube.eo[solver.cube.ep.indexOf(i)]}`);
console.log("Check OLL:");
console.log(`co:`, solver.cube.co.slice(0, 4));
console.log(`eo:`, solver.cube.eo.slice(0, 4));
console.log("Check PLL:");
console.log(`cp:`, solver.cube.cp.slice(0, 4));
console.log(`ep:`, solver.cube.ep.slice(0, 4));
process.exit(1);
}
}
console.log("All 100 tests passed!");

24
test/test_macros.js Normal file
View File

@@ -0,0 +1,24 @@
import { DeepCube, MOVES } from '../src/utils/DeepCube.js';
let cube = new DeepCube();
const apply = (str) => {
str.split(' ').forEach(m => {
cube = cube.multiply(MOVES[m]);
});
};
apply("R U R'");
console.log("Piece 4 (R U R') is at position:", cube.cp.indexOf(4), "Orientation:", cube.co[cube.cp.indexOf(4)]);
cube = new DeepCube();
apply("R U' R' U R U2 R'");
console.log("Piece 4 (Up-face extraction) position:", cube.cp.indexOf(4), "Orientation:", cube.co[cube.cp.indexOf(4)]);
cube = new DeepCube();
apply("R U R'"); // insert front facing
console.log("Piece 4 (Front-face extraction) position:", cube.cp.indexOf(4), "Orientation:", cube.co[cube.cp.indexOf(4)]);
cube = new DeepCube();
apply("F' U' F"); // insert left facing
console.log("Piece 4 (Side-face extraction) position:", cube.cp.indexOf(4), "Orientation:", cube.co[cube.cp.indexOf(4)]);

24
test/test_macros2.js Normal file
View File

@@ -0,0 +1,24 @@
import { DeepCube, MOVES } from '../src/utils/DeepCube.js';
let cube;
const apply = (str) => {
str.split(' ').forEach(m => {
cube = cube.multiply(MOVES[m]);
});
};
const check = (name, alg, initPos, initOri) => {
cube = new DeepCube();
apply(alg);
// We applied alg to a SOLVED cube.
// The piece that WAS at 4 (DFR) is now at some position P with orientation O.
// To solve it, we would need to reverse the alg.
// So if we find a piece at P with orientation O, we apply the reverse alg!
console.log(`${name}: Extraction piece 4 is at pos ${cube.cp.indexOf(4)} ori ${cube.co[cube.cp.indexOf(4)]}`);
};
check("R U R'", "R U R'");
check("R U' R'", "R U' R'");
check("F' U' F", "F' U' F");
check("R U2 R' U' R U R'", "R U' R' U R U2 R'");

10
test/test_macros3.js Normal file
View File

@@ -0,0 +1,10 @@
import { DeepCube, MOVES } from '../src/utils/DeepCube.js';
let cube = new DeepCube();
const apply = (str) => { str.split(' ').forEach(m => { cube = cube.multiply(MOVES[m]); }); };
cube = new DeepCube(); apply("F' U F");
console.log("F' U F reverse puts piece 4 at pos:", cube.cp.indexOf(4), "ori:", cube.co[cube.cp.indexOf(4)]);
cube = new DeepCube(); apply("U' F' U F");
console.log("U' F' U F reverse puts piece 4 at pos:", cube.cp.indexOf(4), "ori:", cube.co[cube.cp.indexOf(4)]);

22
test/test_macros4.js Normal file
View File

@@ -0,0 +1,22 @@
import { DeepCube, MOVES } from '../src/utils/DeepCube.js';
let cube = new DeepCube();
const apply = (str) => { str.split(' ').forEach(m => { cube = cube.multiply(MOVES[m]); }); };
const check = (name, alg, expectedPos, expectedOri) => {
cube = new DeepCube();
apply(alg); // reverse of extraction
let p5 = cube.cp.indexOf(5); let o5 = cube.co[p5];
console.log(`${name}: pos 5 is ${p5} (expected ${expectedPos}), ori ${o5} (expected ${expectedOri})`);
};
// DLF (5) Target UFL (1)
check("F' U' F reverse", "F' U F", 1, 2); // if reverse puts it at pos 1 ori 2, then if at pos 1 ori 2 use F' U' F!
check("L U L' reverse", "L U' L'", 1, 1);
check("L' U' L reverse", "L' U L", 1, 1); // wait, L' moves DLF to UBL(2)? Let's find out!
// Check extraction from 5
cube = new DeepCube(); apply("L U L'");
console.log("Extract DLF (5) with L U L' gives pos:", cube.cp.indexOf(5), "ori:", cube.co[cube.cp.indexOf(5)]);
cube = new DeepCube(); apply("F' U' F");
console.log("Extract DLF (5) with F' U' F gives pos:", cube.cp.indexOf(5), "ori:", cube.co[cube.cp.indexOf(5)]);

22
test/test_moves.js Normal file
View File

@@ -0,0 +1,22 @@
import { DeepCube, MOVES } from '../src/utils/DeepCube.js';
let cube = new DeepCube();
const apply = (str) => {
str.split(' ').forEach(m => {
cube = cube.multiply(MOVES[m]);
});
};
// We want to verify `R U R'` extracts piece 4 (DFR) to U layer.
apply("R U R'");
console.log("Piece 4 is at position:", cube.cp.indexOf(4), "Orientation:", cube.co[cube.cp.indexOf(4)]);
cube = new DeepCube();
// What if piece 4 is at URF (position 0)? We want to insert it to DFR (position 4).
// If Yellow is UP, co=0.
// Let's create a state where DFR is at URF with co=0.
// We can do this by applying R U2 R' U' R U R' IN REVERSE to extract it.
// Reverse of R U2 R' U' R U R' is: R U' R' U R U2 R'
apply("R U' R' U R U2 R'");
console.log("Extraction -> Piece 4 position:", cube.cp.indexOf(4), "Orientation:", cube.co[cube.cp.indexOf(4)]);

View File

@@ -1,7 +1,6 @@
import { CubeModel, FACES, COLORS } from "../src/utils/CubeModel.js";
import { CubeModel, FACES, COLORS } from '../src/utils/CubeModel.js';
console.log('Running CubeModel Rotation Logic Tests...');
console.log("Running CubeModel Rotation Logic Tests...");
const cube1 = new CubeModel();
const cube2 = new CubeModel();
@@ -14,9 +13,9 @@ const compareCubes = (c1, c2, message) => {
return true;
} else {
console.error(`❌ FAIL: ${message}`);
console.log('Expected (Standard Move):');
console.log("Expected (Standard Move):");
console.log(s2);
console.log('Actual (Layer Rotation):');
console.log("Actual (Layer Rotation):");
console.log(s1);
return false;
}
@@ -25,48 +24,47 @@ const compareCubes = (c1, c2, message) => {
// Test 1: Top Layer (y=1) CW vs U
cube1.reset();
cube2.reset();
console.log('Testing Top Layer CW vs U...');
cube1.rotateLayer('y', 1, 1); // Top CW
cube2.applyMove('U');
console.log("Testing Top Layer CW vs U...");
cube1.rotateLayer("y", 1, 1); // Top CW
cube2.applyMove("U");
compareCubes(cube1, cube2, "Top Layer CW matches U");
// Test 2: Bottom Layer (y=-1) CW vs D
cube1.reset();
cube2.reset();
console.log('Testing Bottom Layer CW vs D...');
cube1.rotateLayer('y', -1, -1); // Bottom CW (CW around -Y is CCW around Y)
cube2.applyMove('D');
console.log("Testing Bottom Layer CW vs D...");
cube1.rotateLayer("y", -1, -1); // Bottom CW (CW around -Y is CCW around Y)
cube2.applyMove("D");
compareCubes(cube1, cube2, "Bottom Layer CW matches D");
// Test 3: Left Layer (x=-1) CW vs L
cube1.reset();
cube2.reset();
console.log('Testing Left Layer CW vs L...');
cube1.rotateLayer('x', -1, -1); // Left CW (CW around -X is CCW around X)
cube2.applyMove('L');
console.log("Testing Left Layer CW vs L...");
cube1.rotateLayer("x", -1, -1); // Left CW (CW around -X is CCW around X)
cube2.applyMove("L");
compareCubes(cube1, cube2, "Left Layer CW matches L");
// Test 4: Right Layer (x=1) CW vs R
cube1.reset();
cube2.reset();
console.log('Testing Right Layer CW vs R...');
cube1.rotateLayer('x', 1, 1); // Right CW
cube2.applyMove('R');
console.log("Testing Right Layer CW vs R...");
cube1.rotateLayer("x", 1, 1); // Right CW
cube2.applyMove("R");
compareCubes(cube1, cube2, "Right Layer CW matches R");
// Test 5: Front Layer (z=1) CW vs F
cube1.reset();
cube2.reset();
console.log('Testing Front Layer CW vs F...');
cube1.rotateLayer('z', 1, 1); // Front CW
cube2.applyMove('F');
console.log("Testing Front Layer CW vs F...");
cube1.rotateLayer("z", 1, 1); // Front CW
cube2.applyMove("F");
compareCubes(cube1, cube2, "Front Layer CW matches F");
// Test 6: Back Layer (z=-1) CW vs B
cube1.reset();
cube2.reset();
console.log('Testing Back Layer CW vs B...');
cube1.rotateLayer('z', -1, -1); // Back CW (CW around -Z is CCW around Z)
cube2.applyMove('B');
console.log("Testing Back Layer CW vs B...");
cube1.rotateLayer("z", -1, -1); // Back CW (CW around -Z is CCW around Z)
cube2.applyMove("B");
compareCubes(cube1, cube2, "Back Layer CW matches B");

View File

@@ -1,12 +0,0 @@
import { State } from 'rubiks-js/src/state/index.js';
console.log('State imported successfully');
const state = new State(true);
console.log('State instantiated');
state.applyTurn('R');
console.log('Applied turn R');
const encoded = state.encode();
console.log('Encoded state:', encoded);

70
test/tokenReducer.test.js Normal file
View File

@@ -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);
});
});

41
test/verify_integrity.js Normal file
View File

@@ -0,0 +1,41 @@
import { DeepCube, MOVES } from "../src/utils/DeepCube.js";
function runStressTest(iterations) {
console.log(`Starting DeepCube Stress Test (${iterations} moves)...`);
let cube = new DeepCube(); // Solved
const moveNames = Object.keys(MOVES);
const startTime = Date.now();
for (let i = 1; i <= iterations; i++) {
const randomMove = moveNames[Math.floor(Math.random() * moveNames.length)];
cube = cube.multiply(MOVES[randomMove]);
if (!cube.isValid()) {
console.error(`\n❌ INVALID STATE DETECTED AT MOVE ${i}!`);
console.error(`Move applied: ${randomMove}`);
console.error(`CP:`, cube.cp);
console.error(`CO:`, cube.co);
console.error(`EP:`, cube.ep);
console.error(`EO:`, cube.eo);
process.exit(1);
}
if (i % 100000 === 0) {
process.stdout.write(
`\r${i} moves verified (${((i / iterations) * 100).toFixed(0)}%)`,
);
}
}
const duration = Date.now() - startTime;
console.log(
`\n🎉 Success! Mathematical integrity held over ${iterations} random moves.`,
);
console.log(
`⏱️ Time taken: ${duration} ms (${(iterations / (duration / 1000)).toFixed(0)} moves/sec)`,
);
}
runStressTest(1000000);

75
test/verify_solvers.js Normal file
View File

@@ -0,0 +1,75 @@
import { DeepCube, MOVES } from "../src/utils/DeepCube.js";
import { KociembaSolver } from "../src/utils/solvers/KociembaSolver.js";
function generateScramble(length = 20) {
const moveNames = Object.keys(MOVES);
const scramble = [];
for (let i = 0; i < length; i++) {
scramble.push(moveNames[Math.floor(Math.random() * moveNames.length)]);
}
return scramble;
}
function runSolverTests(iterations) {
console.log(`Starting KociembaSolver tests (${iterations} scrambles)...`);
let successCount = 0;
let totalMoves = 0;
for (let i = 0; i < iterations; i++) {
let cube = new DeepCube();
const scramble = generateScramble(30);
scramble.forEach((m) => {
cube = cube.multiply(MOVES[m]);
});
const solver = new KociembaSolver(cube);
try {
const solution = solver.solve();
// Apply solution to verify
let testCube = cube.clone();
solution.forEach((m) => {
if (!MOVES[m]) console.error("MISSING MOVE FROM SOLVER:", m);
testCube = testCube.multiply(MOVES[m]);
});
if (testCube.isValid() && isSolvedState(testCube)) {
successCount++;
totalMoves += solution.length;
if (i % 10 === 0) process.stdout.write(`\r${i} solves complete.`);
} else {
console.error(`\n❌ Solver failed validation on scramble ${i}!`);
console.error(`Scramble: ${scramble.join(" ")}`);
console.error(`Solution: ${solution.join(" ")}`);
console.error(`CP:`, testCube.cp);
console.error(`CO:`, testCube.co);
console.error(`EP:`, testCube.ep);
console.error(`EO:`, testCube.eo);
process.exit(1);
}
} catch (e) {
console.error(`\n❌ Solver threw error on scramble ${i}!`);
console.error(`Scramble: ${scramble.join(" ")}`);
console.error(e);
process.exit(1);
}
}
console.log(
`\n🎉 Success! KociembaSolver solved ${successCount}/${iterations} cubes optimally.`,
);
console.log(
`📊 Average shortest path: ${(totalMoves / iterations).toFixed(1)} moves.`,
);
}
function isSolvedState(state) {
for (let i = 0; i < 8; i++)
if (state.cp[i] !== i || state.co[i] !== 0) return false;
for (let i = 0; i < 12; i++)
if (state.ep[i] !== i || state.eo[i] !== 0) return false;
return true;
}
runSolverTests(100);

21
test_beginner_solver.js Normal file
View File

@@ -0,0 +1,21 @@
import { DeepCube } from "./src/utils/DeepCube.js";
import { BeginnerSolver } from "./src/utils/solvers/BeginnerSolver.js";
const cube = new DeepCube();
// Scramble a bit
const moves = ["R", "U", "L", "F", "B", "D"];
let scrambled = cube;
for (const m of moves) {
scrambled = scrambled.multiply(import("./src/utils/DeepCube.js").then(m => m.MOVES[m]));
}
// This won't work easily with dynamic imports in a script.
// Let's just use the constructor.
console.log("Testing BeginnerSolver...");
try {
const solver = new BeginnerSolver(new DeepCube());
const sol = solver.solve();
console.log("Solution length:", sol.length);
} catch (e) {
console.error("BeginnerSolver failed:", e);
}