34 Commits

Author SHA1 Message Date
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
8d5521e326 0.3.1
All checks were successful
Deploy to Production / deploy (push) Successful in 9s
2026-02-23 00:51:21 +00:00
b5e407f738 chore: refine moves queue layout gap 2026-02-23 00:51:06 +00:00
de736e1daf 0.3.0
All checks were successful
Deploy to Production / deploy (push) Successful in 9s
2026-02-23 00:24:09 +00:00
482da718f3 chore: cube queue UI and copy/reset actions 2026-02-23 00:23:54 +00:00
9cd3de40e8 chore: tweak light theme gradient 2026-02-22 22:10:25 +00:00
ef6a7fed4f chore: bump version to 0.2.0
All checks were successful
Deploy to Production / deploy (push) Successful in 8s
2026-02-22 21:58:02 +00:00
c60a37d0cc Adjust cube control button mappings 2026-02-22 21:52:18 +00:00
3261aea81d chore: remove projections toggle and bump to 0.1.0
All checks were successful
Deploy to Production / deploy (push) Successful in 10s
2026-02-22 21:11:42 +00:00
86c4a18851 fix: sync layer animation with worker updates 2026-02-22 21:07:08 +00:00
eb9e2f993d chore: adjust cube opacity
All checks were successful
Deploy to Production / deploy (push) Successful in 5s
2026-02-22 20:51:14 +00:00
4aea776207 feat: cube transparency toggle
All checks were successful
Deploy to Production / deploy (push) Successful in 6s
2026-02-22 20:48:29 +00:00
a75c148a5b chore: bump version to 0.0.29
All checks were successful
Deploy to Production / deploy (push) Successful in 9s
2026-02-22 20:31:54 +00:00
a9881bb5fa Bump version to 0.0.28
All checks were successful
Deploy to Production / deploy (push) Successful in 8s
2026-02-22 19:53:15 +00:00
0141cd404b Update config
Some checks failed
Deploy to Production / deploy (push) Failing after 4s
2026-02-22 19:49:32 +00:00
5c4eeeb3a9 Align docker compose with piggy-bank
All checks were successful
Deploy to Production / deploy (push) Successful in 5s
2026-02-22 19:40:06 +00:00
a7e97b0d9f Adjust docker compose networking
All checks were successful
Deploy to Production / deploy (push) Successful in 5s
2026-02-22 19:31:16 +00:00
c4b78ad7b7 Update CI and cube projections
Some checks failed
Deploy to Production / deploy (push) Failing after 5s
2026-02-22 19:23:25 +00:00
d536290e5d Adjust projections and CI config
Some checks failed
Deploy to Production / deploy (push) Failing after 4s
2026-02-22 19:19:03 +00:00
6eb60e1cb3 Align rubic-cube docker setup with nonograms
All checks were successful
Deploy to Production / deploy (push) Successful in 8s
2026-02-22 15:57:59 +00:00
a1574a149e Refactor: Remove unused components (Main, CubeCSS, etc.), utils (DeepCube, Matrix4), dependencies (matrix-js, utils), and untrack dist folder
All checks were successful
Deploy to Production / deploy (push) Successful in 9s
2026-02-22 04:46:32 +00:00
3857b926ff Fix: Remove node_modules from git tracking and update .gitignore
All checks were successful
Deploy to Production / deploy (push) Successful in 5s
2026-02-22 04:39:07 +00:00
15447c106a 0.0.27
All checks were successful
Deploy to Production / deploy (push) Successful in 11s
2026-02-22 04:36:04 +00:00
b5ddc21662 Refactor: Implement SmartCube renderer, improve UI styling, and fix gaps 2026-02-22 04:35:59 +00:00
57 changed files with 8732 additions and 1919 deletions

View File

@@ -1,5 +1,4 @@
name: Deploy to Production
run-name: Deploy to Production by @${{ github.actor }}
on:
push:
@@ -15,9 +14,6 @@ jobs:
- name: Build and deploy with Docker Compose
run: |
# Próba zatrzymania i usunięcia starego kontenera (ignoruje błąd jeśli nie istnieje)
# Sprzątaj TYLKO swój projekt
docker compose down --remove-orphans || true
docker rm -f rubic-cube || true
# Start nowej wersji
docker compose up -d --build

3
.gitignore vendored
View File

@@ -22,3 +22,6 @@ dist-ssr
*.njsproj
*.sln
*.sw?
.agent/
cache/

View File

@@ -1,5 +1,5 @@
# Stage 1: Build the application
FROM node:18-alpine as build-stage
FROM node:lts-alpine as build-stage
# Set working directory
WORKDIR /app

View File

@@ -1,4 +1,4 @@
version: '3.8'
name: rubic-cube
services:
rubic-cube:
@@ -6,17 +6,16 @@ services:
build:
context: .
dockerfile: Dockerfile
# ports:
# - "8083:80"
ports:
- "8083:80"
expose:
- "80"
restart: always
restart: unless-stopped
# volumes:
# - ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
networks:
- npm_public
- rubic-net
networks:
npm_public:
external: true
rubic-net:
driver: bridge

View File

@@ -1,6 +1,7 @@
server {
listen 80;
server_name localhost;
listen [::]:80;
server_name _;
root /usr/share/nginx/html;
index index.html;

4372
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
{
"name": "rubic-cube",
"private": true,
"version": "0.0.6",
"version": "0.5.0",
"type": "module",
"scripts": {
"dev": "vite",
@@ -9,9 +9,8 @@
"preview": "vite preview"
},
"dependencies": {
"@gkucmierz/utils": "^1.28.3",
"cubejs": "^1.3.2",
"lucide-vue-next": "^0.564.0",
"matrix-js": "^1.8.0",
"vue": "^3.5.13"
},
"devDependencies": {

View File

@@ -1,17 +1,13 @@
<script setup>
import CubeCSS from './components/renderers/CubeCSS.vue'
import DebugPanel from './components/DebugPanel.vue'
import InteractionReplay from './components/InteractionReplay.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>
<NavBar />
<div class="app-content">
<DebugPanel />
<InteractionReplay />
<CubeCSS />
<SmartCube />
</div>
<Footer />
</template>
@@ -23,7 +19,7 @@ import Footer from './components/Footer.vue'
justify-content: center;
align-items: center;
width: 100%;
padding: 2rem 0;
padding: 0;
position: relative;
z-index: 1;
user-select: none;

View File

@@ -1,137 +0,0 @@
<script setup>
import { useDebug } from '../composables/useDebug'
import { ref } from 'vue'
const { settings } = useDebug()
const isOpen = ref(true)
const toggle = () => isOpen.value = !isOpen.value
</script>
<template>
<div class="debug-panel" :class="{ open: isOpen }">
<div class="header" @click="toggle">
<span>🛠 Debug Config</span>
<span>{{ isOpen ? '▼' : '▲' }}</span>
</div>
<div v-if="isOpen" class="content">
<div class="section">
<h3>View Rotation</h3>
<label>
<input type="checkbox" v-model="settings.viewRotation.invertX"> Invert X (Up/Down)
</label>
<label>
<input type="checkbox" v-model="settings.viewRotation.invertY"> Invert Y (Left/Right)
</label>
<label>
Speed: <input type="number" step="0.1" v-model="settings.viewRotation.speed">
</label>
</div>
<div class="section">
<h3>Drag Mappings (Sign)</h3>
<p class="hint">Adjust signs (-1 or 1) to correct drag direction</p>
<div class="face-group" v-for="(val, face) in settings.dragMapping" :key="face">
<strong>{{ face.toUpperCase() }}</strong>
<div class="controls">
<label>X: <input type="number" :step="2" :min="-1" :max="1" v-model="settings.dragMapping[face].x"></label>
<label>Y: <input type="number" :step="2" :min="-1" :max="1" v-model="settings.dragMapping[face].y"></label>
</div>
</div>
</div>
<div class="section">
<h3>Physics</h3>
<label>
<input type="checkbox" v-model="settings.physics.enabled"> Inertia & Snap
</label>
</div>
</div>
</div>
</template>
<style scoped>
.debug-panel {
position: fixed;
top: 70px;
right: 10px;
width: 250px;
background: rgba(0, 0, 0, 0.85);
color: #fff;
border-radius: 8px;
font-family: monospace;
font-size: 12px;
z-index: 9999;
box-shadow: 0 4px 12px rgba(0,0,0,0.5);
max-height: 90vh;
display: flex;
flex-direction: column;
}
.header {
padding: 10px;
background: #333;
border-radius: 8px 8px 0 0;
cursor: pointer;
display: flex;
justify-content: space-between;
font-weight: bold;
user-select: none;
}
.content {
padding: 10px;
overflow-y: auto;
}
.section {
margin-bottom: 15px;
border-bottom: 1px solid #444;
padding-bottom: 10px;
}
h3 {
margin: 0 0 8px 0;
color: #aaa;
font-size: 11px;
text-transform: uppercase;
}
label {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 4px;
cursor: pointer;
}
input[type="number"] {
width: 40px;
background: #222;
border: 1px solid #444;
color: #fff;
padding: 2px;
}
.face-group {
margin-bottom: 8px;
background: #222;
padding: 5px;
border-radius: 4px;
}
.controls {
display: flex;
gap: 10px;
margin-top: 4px;
}
.hint {
font-size: 10px;
color: #888;
margin-bottom: 5px;
}
</style>

View File

@@ -7,9 +7,6 @@ const version = __APP_VERSION__;
<footer class="app-footer glass-panel">
<div class="footer-content">
<p>&copy; {{ currentYear }} Rubic Cube. Wersja {{ version }}</p>
<div class="social-links">
<!-- Placeholder for social links if needed -->
</div>
</div>
</footer>
</template>
@@ -22,11 +19,15 @@ const version = __APP_VERSION__;
display: flex;
align-items: center;
justify-content: center;
margin-top: auto;
background: var(--panel-bg);
backdrop-filter: blur(10px);
border-top: 1px solid var(--panel-border);
box-shadow: 0 -4px 15px rgba(0, 0, 0, 0.1);
position: absolute;
bottom: 0;
left: 0;
z-index: 10;
/* Glass panel styles handle background/border/shadow */
border-radius: 0; /* Full width bar usually square corners or specific radius */
border-left: none;
border-right: none;
border-bottom: none;
color: var(--text-muted);
box-sizing: border-box;
}

View File

@@ -1,188 +0,0 @@
<script setup>
import { ref } from 'vue'
import { useInteractionLogger } from '../composables/useInteractionLogger'
const { logs, isRecording, clearLogs, getRecentLogsForAnalysis } = useInteractionLogger()
const isOpen = ref(false)
const copied = ref(false)
const toggle = () => isOpen.value = !isOpen.value
const copyReport = async () => {
const report = getRecentLogsForAnalysis(50)
const context = `
### User Interaction Report
Please analyze the following interaction logs to identify the issue.
Focus on: Drag direction, Active Layer, Rotation Mapping, and State changes.
\`\`\`json
${report}
\`\`\`
`
try {
await navigator.clipboard.writeText(context)
copied.value = true
setTimeout(() => copied.value = false, 2000)
} catch (err) {
console.error('Failed to copy logs', err)
alert('Failed to copy to clipboard. Check console.')
}
}
</script>
<template>
<div class="interaction-replay">
<div class="header" @click="toggle" :class="{ recording: isRecording }">
<span class="indicator"></span>
<span>Logger ({{ logs.length }})</span>
</div>
<div v-if="isOpen" class="panel">
<div class="actions">
<button @click="copyReport" :class="{ success: copied }">
{{ copied ? 'Copied!' : '📋 Copy Report for AI' }}
</button>
<button @click="clearLogs" class="secondary">Clear</button>
<label>
<input type="checkbox" v-model="isRecording"> Rec
</label>
</div>
<div class="log-list">
<div v-for="log in logs.slice().reverse()" :key="log.id" class="log-item">
<span class="time">{{ new Date(log.timestamp).toISOString().substr(14, 9) }}</span>
<span class="type" :class="log.type">{{ log.type }}</span>
<pre class="data">{{ log.data }}</pre>
</div>
</div>
</div>
</div>
</template>
<style scoped>
.interaction-replay {
position: fixed;
bottom: 50px;
right: 10px;
z-index: 10000;
font-family: monospace;
font-size: 12px;
}
.header {
background: #222;
color: #fff;
padding: 8px 12px;
border-radius: 20px;
cursor: pointer;
display: flex;
align-items: center;
gap: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.3);
border: 1px solid #444;
}
.header.recording .indicator {
color: #ff4444;
animation: pulse 1.5s infinite;
}
.indicator {
color: #666;
}
@keyframes pulse {
0% { opacity: 1; }
50% { opacity: 0.5; }
100% { opacity: 1; }
}
.panel {
position: absolute;
bottom: 40px;
right: 0;
width: 350px;
height: 400px;
background: rgba(0, 0, 0, 0.9);
border-radius: 8px;
border: 1px solid #444;
display: flex;
flex-direction: column;
overflow: hidden;
}
.actions {
padding: 10px;
border-bottom: 1px solid #444;
display: flex;
gap: 8px;
background: #1a1a1a;
align-items: center;
}
button {
flex: 1;
padding: 6px;
background: #007acc;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
button.success {
background: #28a745;
}
button.secondary {
background: #444;
flex: 0 0 60px;
}
label {
color: #fff;
display: flex;
align-items: center;
gap: 4px;
cursor: pointer;
}
.log-list {
flex: 1;
overflow-y: auto;
padding: 10px;
}
.log-item {
margin-bottom: 8px;
border-bottom: 1px solid #333;
padding-bottom: 4px;
}
.time {
color: #666;
margin-right: 8px;
}
.type {
font-weight: bold;
padding: 2px 4px;
border-radius: 2px;
margin-right: 8px;
}
.type.drag-start { color: #4fc3f7; }
.type.drag-update { color: #ffd54f; }
.type.drag-end { color: #81c784; }
.type.rotation { color: #ba68c8; }
.data {
margin: 4px 0 0 0;
color: #aaa;
font-size: 10px;
white-space: pre-wrap;
word-break: break-all;
}
</style>

View File

@@ -1,109 +0,0 @@
<script setup>
import { computed } from 'vue'
import { useRenderer } from '../composables/useRenderer'
import { useCube } from '../composables/useCube'
import CubeCSS from './renderers/CubeCSS.vue'
import CubeSVG from './renderers/CubeSVG.vue'
import CubeCanvas from './renderers/CubeCanvas.vue'
const { activeRenderer, RENDERERS } = useRenderer()
const { cubeState } = useCube()
const currentRendererComponent = computed(() => {
switch (activeRenderer.value) {
case RENDERERS.CSS:
return CubeCSS
case RENDERERS.SVG:
return CubeSVG
case RENDERERS.CANVAS:
return CubeCanvas
default:
return CubeCSS
}
})
const formattedState = computed(() => {
if (!cubeState.value) return '{}'
// Custom formatter to keep faces compact
const s = cubeState.value
const faces = Object.keys(s)
// Helper to shorten colors
const shortColor = (c) => c && typeof c === 'string' ? c[0].toUpperCase() : '-'
let out = '{\n'
faces.forEach((face, i) => {
const matrix = s[face]
// Format as ["WWW", "WWW", "WWW"]
const rows = matrix.map(row => `"${row.map(shortColor).join('')}"`).join(', ')
out += ` "${face}": [ ${rows} ]`
if (i < faces.length - 1) out += ','
out += '\n'
})
out += '}'
return out
})
</script>
<template>
<div class="wrapper">
<div class="renderer-container">
<component :is="currentRendererComponent" />
</div>
<div class="state-panel">
<h3>Cube State</h3>
<pre>{{ formattedState }}</pre>
</div>
</div>
</template>
<style scoped>
.wrapper {
display: flex;
flex-direction: row;
align-items: flex-start;
justify-content: center;
gap: 2rem;
width: 100%;
height: 100%;
padding: 2rem;
box-sizing: border-box;
}
.renderer-container {
flex: 2;
display: flex;
justify-content: center;
align-items: center;
height: 100%;
min-width: 300px;
}
.state-panel {
flex: 1;
max-width: 400px;
background: rgba(0, 0, 0, 0.05);
padding: 1rem;
border-radius: 8px;
max-height: 80vh;
overflow-y: auto;
font-family: monospace;
font-size: 0.8rem;
border: 1px solid rgba(0, 0, 0, 0.1);
}
h3 {
margin-top: 0;
margin-bottom: 1rem;
font-size: 1.2rem;
color: #333;
}
pre {
margin: 0;
white-space: pre-wrap;
word-wrap: break-word;
}
</style>

View File

@@ -1,40 +1,16 @@
<script setup>
import { Sun, Moon } from 'lucide-vue-next';
import { ref, onMounted } from 'vue';
import { useRenderer } from '../composables/useRenderer';
const { activeRenderer, setRenderer, RENDERERS } = useRenderer();
import { Sun, Moon, Grid2x2 } from "lucide-vue-next";
import { ref, onMounted } from "vue";
import { useSettings } from "../composables/useSettings";
const { isCubeTranslucent, toggleCubeTranslucent } = 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;
if (dark) {
document.documentElement.style.setProperty('--bg-gradient', 'linear-gradient(135deg, #2c3e50 0%, #000000 100%)');
document.documentElement.style.setProperty('--text-color', '#ffffff');
document.documentElement.style.setProperty('--text-strong', '#ffffff');
document.documentElement.style.setProperty('--text-muted', 'rgba(255, 255, 255, 0.7)');
document.documentElement.style.setProperty('--glass-bg', 'rgba(255, 255, 255, 0.05)');
document.documentElement.style.setProperty('--glass-border', 'rgba(255, 255, 255, 0.1)');
document.documentElement.style.setProperty('--panel-bg', 'rgba(255, 255, 255, 0.05)');
document.documentElement.style.setProperty('--panel-border', 'rgba(255, 255, 255, 0.1)');
document.documentElement.style.setProperty('--cube-edge-color', '#333333');
document.documentElement.style.setProperty('--title-gradient', 'linear-gradient(45deg, #00f2fe, #4facfe)');
} else {
document.documentElement.style.setProperty('--bg-gradient', 'linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%)');
document.documentElement.style.setProperty('--text-color', '#0f172a');
document.documentElement.style.setProperty('--text-strong', '#0f172a');
document.documentElement.style.setProperty('--text-muted', 'rgba(15, 23, 42, 0.6)');
document.documentElement.style.setProperty('--glass-bg', 'rgba(255, 255, 255, 0.75)');
document.documentElement.style.setProperty('--glass-border', 'rgba(15, 23, 42, 0.12)');
document.documentElement.style.setProperty('--panel-bg', 'rgba(255, 255, 255, 0.7)');
document.documentElement.style.setProperty('--panel-border', 'rgba(15, 23, 42, 0.12)');
document.documentElement.style.setProperty('--cube-edge-color', '#000000');
document.documentElement.style.setProperty('--title-gradient', 'linear-gradient(45deg, #0ea5e9, #6366f1)');
}
localStorage.setItem("theme", theme);
};
const toggleTheme = () => {
@@ -42,7 +18,12 @@ const toggleTheme = () => {
};
onMounted(() => {
setTheme(true);
const savedTheme = localStorage.getItem("theme");
if (savedTheme) {
setTheme(savedTheme === "dark");
} else {
setTheme(true);
}
});
</script>
@@ -53,20 +34,26 @@ onMounted(() => {
</div>
<div class="nav-container">
<div class="renderer-selector">
<button
v-for="renderer in RENDERERS"
:key="renderer"
@click="setRenderer(renderer)"
class="renderer-btn"
:class="{ active: activeRenderer === renderer }"
>
{{ renderer }}
</button>
</div>
<!-- Cube Opacity Toggle -->
<button
class="btn-neon nav-btn icon-only"
@click="toggleCubeTranslucent"
:title="
isCubeTranslucent
? 'Wyłącz przezroczystość kostki'
: 'Włącz przezroczystość kostki'
"
:class="{ active: isCubeTranslucent }"
>
<Grid2x2 :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 ? 'Przełącz na jasny' : 'Przełącz na ciemny'"
>
<Sun v-if="isDark" :size="20" />
<Moon v-else :size="20" />
</button>
@@ -80,17 +67,18 @@ onMounted(() => {
justify-content: space-between;
align-items: center;
padding: 0 20px;
height: 50px;
height: 70px;
width: 100%;
box-sizing: border-box;
position: sticky;
position: absolute;
top: 0;
left: 0;
z-index: 100;
margin-bottom: 0;
background: var(--glass-bg);
backdrop-filter: blur(10px);
border-bottom: 1px solid var(--glass-border);
box-shadow: var(--glass-shadow);
/* Glass panel styles handle background/border/shadow */
border-radius: 0;
border-top: none;
border-left: none;
border-right: none;
}
.logo-container {
@@ -100,10 +88,10 @@ onMounted(() => {
}
.logo-text {
font-size: 1.2rem;
font-size: 1.5rem;
font-weight: 700;
color: var(--text-color);
text-shadow: 0 0 20px var(--title-glow);
color: var(--text-strong);
letter-spacing: 1px;
}
.nav-container {
@@ -112,67 +100,25 @@ onMounted(() => {
align-items: center;
}
.renderer-selector {
display: flex;
gap: 5px;
background: rgba(0, 0, 0, 0.2);
padding: 3px;
border-radius: 6px;
}
.renderer-btn {
background: transparent;
border: none;
color: var(--text-muted);
padding: 4px 10px;
border-radius: 4px;
cursor: pointer;
font-size: 0.8rem;
font-weight: 600;
transition: all 0.2s;
}
.renderer-btn:hover {
color: var(--text-color);
}
.renderer-btn.active {
background: var(--glass-border);
color: var(--text-color);
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
}
.nav-btn {
background: transparent;
border: none;
color: var(--text-color);
font-size: 1rem;
color: var(--text-strong);
cursor: pointer;
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
border-radius: 8px;
padding: 12px;
border-radius: 50%;
transition: all 0.3s ease;
}
.nav-btn:hover {
background: rgba(255, 255, 255, 0.1);
transform: translateY(-2px);
.nav-btn:hover,
.nav-btn.active {
background: rgba(255, 255, 255, 0.2);
}
.btn-neon {
border: 1px solid var(--toggle-btn-border);
box-shadow: 0 0 5px rgba(0,0,0,0.1);
}
.desktop-only {
display: flex;
}
@media (max-width: 768px) {
.desktop-only {
display: none;
}
.nav-btn.active {
color: var(--color-primary);
box-shadow: 0 0 10px rgba(255, 255, 255, 0.1);
}
</style>

View File

@@ -0,0 +1,59 @@
<script setup>
import { computed } from "vue";
const props = defineProps({
start: {
type: Object,
required: true, // {x, y, z}
},
end: {
type: Object,
required: true, // {x, y, z}
},
color: {
type: String,
default: "var(--text-color, #fff)",
},
thickness: {
type: Number,
default: 1,
},
});
const style = computed(() => {
const dx = props.end.x - props.start.x;
const dy = props.end.y - props.start.y;
const dz = props.end.z - props.start.z;
const length = Math.sqrt(dx * dx + dy * dy + dz * dz);
if (length === 0) return {};
const midX = (props.start.x + props.end.x) / 2;
const midY = (props.start.y + props.end.y) / 2;
const midZ = (props.start.z + props.end.z) / 2;
// Rotation
// Yaw (around Y axis)
const yaw = Math.atan2(dz, dx);
// Pitch (around Z axis)
const pitch = Math.atan2(dy, Math.sqrt(dx * dx + dz * dz));
return {
width: `${length}px`,
height: `${props.thickness}px`,
backgroundColor: props.color,
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",
};
});
</script>
<template>
<div class="line-3d" :style="style"></div>
</template>

View File

@@ -1,448 +0,0 @@
<script setup>
import { ref, computed, onMounted, onUnmounted } from 'vue'
import { useCube } from '../../composables/useCube'
import { useDebug } from '../../composables/useDebug'
import { useInteractionLogger } from '../../composables/useInteractionLogger'
const { cubies, initCube, rotateLayer, FACES } = useCube()
const { settings: debugSettings } = useDebug()
const { addLog } = useInteractionLogger()
// --- State ---
const rx = ref(25)
const ry = ref(25)
const rz = ref(0)
const isDragging = ref(false)
const dragMode = ref('view') // 'view' or 'layer'
const startMouseX = ref(0)
const startMouseY = ref(0)
const lastMouseX = ref(0)
const lastMouseY = ref(0)
const selectedCubieId = ref(null) // ID of the cubie where drag started
const selectedFaceNormal = ref(null) // Normal vector of the face clicked
// Animation state
const activeLayer = ref(null) // { axis: 'x'|'y'|'z', index: -1|0|1 }
const layerRotation = ref(0)
const isSnapping = ref(false)
const velocity = ref(0)
const lastTime = ref(0)
const rafId = ref(null)
// Mouse Interaction
const onMouseDown = (event) => {
if (isSnapping.value) return
isDragging.value = true
startMouseX.value = event.clientX
startMouseY.value = event.clientY
lastMouseX.value = event.clientX
lastMouseY.value = event.clientY
lastTime.value = performance.now()
velocity.value = 0 // Reset velocity
const target = event.target
const stickerEl = target.closest('.sticker-face')
if (stickerEl) {
// Clicked on a cubie face
const cubieId = parseInt(stickerEl.dataset.cubieId)
const faceName = stickerEl.dataset.face
selectedCubieId.value = cubieId
selectedFaceNormal.value = faceName // 'up', 'down', etc.
const cubie = cubies.value.find(c => c.id === cubieId)
const isCenter = (Math.abs(cubie.x) + Math.abs(cubie.y) + Math.abs(cubie.z)) === 1
if (isCenter) {
dragMode.value = 'view'
document.body.style.cursor = 'move'
addLog('drag-start', { mode: 'view', cubieId, face: faceName })
} else {
dragMode.value = 'layer'
document.body.style.cursor = 'grab'
addLog('drag-start', { mode: 'layer', cubieId, face: faceName })
}
} else {
dragMode.value = 'view'
selectedCubieId.value = null
document.body.style.cursor = 'move'
addLog('drag-start', { mode: 'view', target: 'background' })
}
}
const onMouseMove = (event) => {
if (!isDragging.value) return
if (dragMode.value === 'layer') {
document.body.style.cursor = 'grabbing'
}
const deltaX = event.clientX - lastMouseX.value
const deltaY = event.clientY - lastMouseY.value
if (dragMode.value === 'view') {
const s = debugSettings.viewRotation
const speed = s.speed || 0.5
// Use debug settings for direction
ry.value += deltaX * speed * (s.invertY ? -1 : 1)
rx.value += deltaY * speed * (s.invertX ? -1 : 1)
velocity.value = 0
} else if (dragMode.value === 'layer' && selectedCubieId.value !== null) {
const totalDeltaX = event.clientX - startMouseX.value
const totalDeltaY = event.clientY - startMouseY.value
const now = performance.now()
const dt = now - lastTime.value
lastTime.value = now
updateLayerDrag(totalDeltaX, totalDeltaY, dt)
}
lastMouseX.value = event.clientX
lastMouseY.value = event.clientY
}
const getRotationMapping = (face) => {
const m = debugSettings.dragMapping[face]
// Default structure but with signs from debug settings
const defaults = {
[FACES.FRONT]: [
{ axis: 'x', rotAxis: 'y', sign: m ? m.x : -1 },
{ axis: 'y', rotAxis: 'x', sign: m ? m.y : -1 }
],
[FACES.BACK]: [
{ axis: 'x', rotAxis: 'y', sign: m ? m.x : 1 },
{ axis: 'y', rotAxis: 'x', sign: m ? m.y : 1 }
],
[FACES.RIGHT]: [
{ axis: 'z', rotAxis: 'y', sign: m ? m.x : -1 },
{ axis: 'y', rotAxis: 'z', sign: m ? m.y : 1 }
],
[FACES.LEFT]: [
{ axis: 'z', rotAxis: 'y', sign: m ? m.x : -1 },
{ axis: 'y', rotAxis: 'z', sign: m ? m.y : -1 }
],
[FACES.UP]: [
{ axis: 'x', rotAxis: 'z', sign: m ? m.x : 1 },
{ axis: 'z', rotAxis: 'x', sign: m ? m.y : 1 }
],
[FACES.DOWN]: [
{ axis: 'x', rotAxis: 'z', sign: m ? m.x : -1 },
{ axis: 'z', rotAxis: 'x', sign: m ? m.y : -1 }
]
}
return defaults[face]
}
const ROTATION_MAPPING = {
// Kept for reference or initial state if needed, but we use getRotationMapping now
}
// Helper to project 3D vector to 2D screen space based on current view rotation
const projectVector = (vector) => {
const radX = rx.value * Math.PI / 180
const radY = ry.value * Math.PI / 180
const radZ = rz.value * Math.PI / 180
const { x, y, z } = vector
// v1 = Rz * v
let x1 = x * Math.cos(radZ) - y * Math.sin(radZ)
let y1 = x * Math.sin(radZ) + y * Math.cos(radZ)
let z1 = z
// v2 = Ry * v1
let x2 = x1 * Math.cos(radY) + z1 * Math.sin(radY)
let y2 = y1
let z2 = -x1 * Math.sin(radY) + z1 * Math.cos(radY)
// v3 = Rx * v2
let x3 = x2
let y3 = y2 * Math.cos(radX) - z2 * Math.sin(radX)
let z3 = y2 * Math.sin(radX) + z2 * Math.cos(radX)
return { x: x3, y: y3 }
}
const updateLayerDrag = (dx, dy, dt) => {
const cubie = cubies.value.find(c => c.id === selectedCubieId.value)
if (!cubie) return
let axis = null
let index = 0
let dragVector = null
if (activeLayer.value) {
axis = activeLayer.value.axis
index = activeLayer.value.index
dragVector = activeLayer.value.dragVector
} else {
if (Math.abs(dx) < 5 && Math.abs(dy) < 5) return
const face = selectedFaceNormal.value
// Use dynamic mapping from debug settings if available, else fallback to constant
// But better to make ROTATION_MAPPING computed or access directly
const mapping = getRotationMapping(face)
if (!mapping) return
// Create basis vectors for the two possible tangent axes
const vectors = mapping.map(m => {
const v = { x: 0, y: 0, z: 0 }
v[m.axis] = 1
return { ...m, vector: v }
})
// Project them to screen space
const projected = vectors.map(v => {
const p = projectVector(v.vector)
const len = Math.sqrt(p.x * p.x + p.y * p.y)
return { ...v, px: p.x, py: p.y, len }
})
const mouseLen = Math.sqrt(dx * dx + dy * dy)
if (mouseLen === 0) return
const ndx = dx / mouseLen
const ndy = dy / mouseLen
let bestMatch = null
let maxDot = -1
projected.forEach(p => {
if (p.len < 0.1) return
const npx = p.px / p.len
const npy = p.py / p.len
const dot = Math.abs(ndx * npx + ndy * npy)
if (dot > maxDot) {
maxDot = dot
bestMatch = p
}
})
if (!bestMatch) return
axis = bestMatch.rotAxis
if (axis === 'x') index = cubie.x
if (axis === 'y') index = cubie.y
if (axis === 'z') index = cubie.z
dragVector = { x: bestMatch.px, y: bestMatch.py, sign: bestMatch.sign }
activeLayer.value = { axis, index, dragVector }
addLog('layer-select', { axis, index, vector: dragVector, face: selectedFaceNormal.value })
}
const { x: vx, y: vy, sign } = activeLayer.value.dragVector
const vLen = Math.sqrt(vx * vx + vy * vy)
if (vLen === 0) return
const nvx = vx / vLen
const nvy = vy / vLen
const moveAmount = dx * nvx + dy * nvy
const newRotation = moveAmount * sign * 0.5
if (dt > 0) {
const dRot = newRotation - layerRotation.value
velocity.value = 0.6 * velocity.value + 0.4 * (dRot / dt)
}
layerRotation.value = newRotation
}
const onMouseUp = async () => {
if (!isDragging.value) return
isDragging.value = false
document.body.style.cursor = ''
if (dragMode.value === 'layer' && activeLayer.value) {
isSnapping.value = true
const projection = velocity.value * 200
const projectedRot = layerRotation.value + projection
const steps = Math.round(projectedRot / 90)
const targetRot = steps * 90
const startRot = layerRotation.value
const startTime = performance.now()
const duration = 300
const easeOut = (t) => 1 - Math.pow(1 - t, 3)
return new Promise(resolve => {
const animate = (time) => {
const elapsed = time - startTime
const progress = Math.min(elapsed / duration, 1)
const ease = easeOut(progress)
layerRotation.value = startRot + (targetRot - startRot) * ease
if (progress < 1) {
rafId.value = requestAnimationFrame(animate)
} else {
finishRotation(steps)
resolve()
}
}
rafId.value = requestAnimationFrame(animate)
})
} else {
selectedCubieId.value = null
}
}
const finishRotation = (steps) => {
if (steps !== 0) {
const { axis, index } = activeLayer.value
// Calculate logical direction
// We found that Visual Rotation direction is inverted relative to Logical Rotation direction
// for all axes due to coordinate system differences (Y-down vs Y-up).
// Visual Positive -> Logical Negative.
const direction = steps > 0 ? -1 : 1
const count = Math.abs(steps)
for (let i = 0; i < count; i++) {
rotateLayer(axis, index, direction)
}
addLog('rotation-finish', { axis, index, direction, steps, count })
}
activeLayer.value = null
layerRotation.value = 0
isSnapping.value = false
velocity.value = 0
}
onMounted(() => {
initCube()
window.addEventListener('mousemove', onMouseMove)
window.addEventListener('mouseup', onMouseUp)
})
onUnmounted(() => {
window.removeEventListener('mousemove', onMouseMove)
window.removeEventListener('mouseup', onMouseUp)
if (rafId.value) cancelAnimationFrame(rafId.value)
})
const cubeStyle = computed(() => ({
transform: `rotateX(${rx.value}deg) rotateY(${ry.value}deg) rotateZ(${rz.value}deg)`
}))
const getCubieStyle = (cubie) => {
const tx = cubie.x * 100
const ty = cubie.y * -100
const tz = cubie.z * 100
let transform = `translate3d(${tx}px, ${ty}px, ${tz}px)`
if (activeLayer.value) {
const { axis, index } = activeLayer.value
let match = false
if (axis === 'x' && cubie.x === index) match = true
if (axis === 'y' && cubie.y === index) match = true
if (axis === 'z' && cubie.z === index) match = true
if (match) {
transform = `rotate${axis.toUpperCase()}(${layerRotation.value}deg) ${transform}`
}
}
return { transform }
}
</script>
<template>
<div class="scene" @mousedown="onMouseDown">
<div class="container">
<div class="cube-group" :style="cubeStyle">
<div v-for="cubie in cubies" :key="cubie.id" class="cubie" :style="getCubieStyle(cubie)">
<div v-for="(color, face) in cubie.faces" :key="face"
class="sticker-face"
:class="face"
:data-cubie-id="cubie.id"
:data-face="face"
:style="{ backgroundColor: color }">
<div class="sticker-border"></div>
</div>
</div>
</div>
</div>
</div>
</template>
<style scoped>
.scene {
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
}
.container {
width: 300px;
height: 300px;
perspective: 900px;
pointer-events: auto;
}
.cube-group {
width: 100%;
height: 100%;
position: relative;
transform-style: preserve-3d;
transition: transform 0.1s;
}
.cubie {
position: absolute;
width: 100px;
height: 100px;
top: 100px; /* Center it: 300/2 - 100/2 = 100 */
left: 100px;
transform-style: preserve-3d;
}
.sticker-face {
position: absolute;
width: 100px;
height: 100px;
border: 1px solid rgba(0,0,0,0.8); /* Plastic edge */
box-sizing: border-box;
display: flex;
justify-content: center;
align-items: center;
backface-visibility: hidden; /* Optimization? Or we want to see inside? */
}
.sticker-border {
width: 92%;
height: 92%;
border: 2px solid rgba(0,0,0,0.5);
border-radius: 8px; /* Rounded sticker */
background: inherit; /* Sticker color */
}
/* Face transforms relative to Cubie Center */
.sticker-face.front { transform: rotateY(0deg) translateZ(50px); }
.sticker-face.back { transform: rotateY(180deg) translateZ(50px); }
.sticker-face.right { transform: rotateY(90deg) translateZ(50px); }
.sticker-face.left { transform: rotateY(-90deg) translateZ(50px); }
.sticker-face.up { transform: rotateX(90deg) translateZ(50px); }
.sticker-face.down { transform: rotateX(-90deg) translateZ(50px); }
</style>

View File

@@ -1,34 +0,0 @@
<script setup>
import { onMounted } from 'vue'
onMounted(() => {
console.log('Canvas Renderer mounted (placeholder)')
})
</script>
<template>
<div class="canvas-container">
<canvas width="300" height="300"></canvas>
<div class="overlay">
Canvas Renderer (Coming Soon)
</div>
</div>
</template>
<style scoped>
.canvas-container {
width: 300px;
height: 300px;
position: relative;
display: flex;
justify-content: center;
align-items: center;
border: 1px dashed rgba(255, 255, 255, 0.3);
}
.overlay {
position: absolute;
color: white;
pointer-events: none;
}
</style>

View File

@@ -0,0 +1,200 @@
<script setup>
import { ref, onMounted, onUnmounted } from "vue";
const emit = defineEmits(["move", "scramble", "solve"]);
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>
</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);
}
</style>

View File

@@ -1,28 +0,0 @@
<script setup>
import { onMounted } from 'vue'
onMounted(() => {
console.log('SVG Renderer mounted (placeholder)')
})
</script>
<template>
<div class="svg-container">
<svg width="300" height="300" viewBox="0 0 300 300">
<text x="50%" y="50%" dominant-baseline="middle" text-anchor="middle" fill="white">
SVG Renderer (Coming Soon)
</text>
</svg>
</div>
</template>
<style scoped>
.svg-container {
width: 300px;
height: 300px;
display: flex;
justify-content: center;
align-items: center;
border: 1px dashed rgba(255, 255, 255, 0.3);
}
</style>

View File

@@ -0,0 +1,223 @@
<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;
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,43 +1,61 @@
import { ref, computed } from 'vue';
import { Cube, COLORS, FACES } from '../utils/Cube';
import { ref, computed } from "vue";
import { COLORS, FACES } from "../utils/CubeModel";
// Singleton worker
const worker = new Worker(
new URL("../workers/Cube.worker.js", import.meta.url),
{ type: "module" },
);
// Reactive state
const cubies = ref([]);
const isReady = ref(false);
const validationResult = ref(null);
worker.onmessage = (e) => {
const { type, payload } = e.data;
if (type === "STATE_UPDATE") {
cubies.value = payload.cubies;
isReady.value = true;
} else if (type === "VALIDATION_RESULT") {
validationResult.value = payload;
} else if (type === "ERROR") {
console.error("Worker Error:", payload);
}
};
// Init worker
worker.postMessage({ type: "INIT" });
export function useCube() {
const cube = ref(new Cube());
// Make cubies reactive so Vue tracks changes
// We can just expose the cube instance, but better to expose reactive properties
// Since `cube` is a ref, `cube.value.cubies` is not deeply reactive by default unless `cube.value` is reactive.
// But `ref` wraps the object. If we mutate properties of the object, it might not trigger.
// Let's rely on triggering updates manually or creating a new instance on reset.
// For rotation, we will force update.
const cubies = computed(() => cube.value.cubies);
// Compute the 6-face state matrix for display/debug
const cubeState = computed(() => cube.value.getState());
const initCube = () => {
cube.value.reset();
triggerUpdate();
worker.postMessage({ type: "RESET" });
};
const triggerUpdate = () => {
// Force Vue to notice change
cube.value = Object.assign(Object.create(Object.getPrototypeOf(cube.value)), cube.value);
const rotateLayer = (axis, index, direction, steps = 1) => {
worker.postMessage({
type: "ROTATE_LAYER",
payload: { axis, index, direction, steps },
});
};
const rotateLayer = (axis, index, direction) => {
cube.value.rotateLayer(axis, index, direction);
triggerUpdate();
const turn = (move) => {
worker.postMessage({ type: "TURN", payload: { move } });
};
const validate = () => {
worker.postMessage({ type: "VALIDATE" });
};
return {
cube,
cubies,
cubeState,
cubies: computed(() => cubies.value),
isReady: computed(() => isReady.value),
validationResult: computed(() => validationResult.value),
initCube,
rotateLayer,
turn,
validate,
COLORS,
FACES
FACES,
};
}

View File

@@ -1,50 +0,0 @@
import { reactive, watch } from 'vue'
const settings = reactive({
viewRotation: {
invertX: false, // Inverts Up/Down view rotation
invertY: false, // Inverts Left/Right view rotation (Drag Right -> Increase Angle -> Rotate Right)
speed: 0.5
},
dragMapping: {
// Multipliers for drag direction on faces
front: { x: 1, y: -1 }, // Changed x to 1
back: { x: 1, y: 1 },
right: { x: -1, y: 1 },
left: { x: -1, y: -1 },
up: { x: 1, y: 1 },
down: { x: -1, y: -1 }
},
physics: {
enabled: true,
tension: 200,
friction: 10 // Not currently used but good for future
}
})
// Persist to localStorage for convenience during reload
const STORAGE_KEY = 'rubik-debug-settings-v2' // Changed key to force reset settings
try {
const saved = localStorage.getItem(STORAGE_KEY)
if (saved) {
const parsed = JSON.parse(saved)
// Merge deeply? For now just top level sections
Object.assign(settings.viewRotation, parsed.viewRotation)
Object.assign(settings.dragMapping, parsed.dragMapping)
Object.assign(settings.physics, parsed.physics)
}
} catch (e) {
console.warn('Failed to load debug settings', e)
}
watch(settings, (newSettings) => {
localStorage.setItem(STORAGE_KEY, JSON.stringify(newSettings))
}, { deep: true })
export function useDebug() {
return {
settings
}
}

View File

@@ -1,50 +0,0 @@
import { ref, reactive } from 'vue'
// Global state for logs so it persists across component re-mounts
const logs = ref([])
const isRecording = ref(true)
const maxLogs = 500 // Limit history size
export function useInteractionLogger() {
const addLog = (type, data) => {
if (!isRecording.value) return
const timestamp = Date.now()
const logEntry = {
id: timestamp + Math.random().toString(36).substr(2, 9),
timestamp,
type,
data: JSON.parse(JSON.stringify(data)) // Deep copy to snapshot state
}
logs.value.push(logEntry)
if (logs.value.length > maxLogs) {
logs.value.shift()
}
}
const clearLogs = () => {
logs.value = []
}
const exportLogs = () => {
return JSON.stringify(logs.value, null, 2)
}
// Helper to format logs for LLM analysis
const getRecentLogsForAnalysis = (count = 50) => {
const recent = logs.value.slice(-count)
return JSON.stringify(recent, null, 2)
}
return {
logs,
isRecording,
addLog,
clearLogs,
exportLogs,
getRecentLogsForAnalysis
}
}

View File

@@ -1,23 +0,0 @@
import { ref } from 'vue';
const RENDERERS = {
CSS: 'CSS',
SVG: 'SVG',
CANVAS: 'Canvas'
};
const activeRenderer = ref(RENDERERS.CSS);
export function useRenderer() {
const setRenderer = (renderer) => {
if (Object.values(RENDERERS).includes(renderer)) {
activeRenderer.value = renderer;
}
};
return {
activeRenderer,
setRenderer,
RENDERERS
};
}

View File

@@ -0,0 +1,25 @@
import { ref } from "vue";
let initialCubeTranslucent = false;
try {
const stored = localStorage.getItem("cubeTranslucent");
if (stored !== null) {
initialCubeTranslucent = stored === "true";
}
} catch (e) {}
const isCubeTranslucent = ref(initialCubeTranslucent);
export function useSettings() {
const toggleCubeTranslucent = () => {
isCubeTranslucent.value = !isCubeTranslucent.value;
try {
localStorage.setItem("cubeTranslucent", String(isCubeTranslucent.value));
} catch (e) {}
};
return {
isCubeTranslucent,
toggleCubeTranslucent,
};
}

View File

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

View File

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

View File

@@ -13,9 +13,9 @@
-moz-osx-font-smoothing: grayscale;
/* --- Glassmorphism Design System (from Nonograms) --- */
--bg-gradient: linear-gradient(135deg, #2c3e50 0%, #000000 100%);
--glass-bg: rgba(255, 255, 255, 0.05);
--glass-border: rgba(255, 255, 255, 0.1);
--bg-gradient: radial-gradient(circle at center, #444 0%, #000000 100%);
--glass-bg: rgba(255, 255, 255, 0.1);
--glass-border: rgba(255, 255, 255, 0.2);
--glass-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.37);
--text-color: #ffffff;
--text-strong: #ffffff;
@@ -25,10 +25,55 @@
--accent-purple: #4facfe;
--primary-accent: #00f2fe;
--title-glow: rgba(0, 255, 255, 0.2);
--toggle-bg: rgba(255, 255, 255, 0.08);
--toggle-border: rgba(255, 255, 255, 0.2);
--toggle-shadow: 0 0 15px rgba(0, 0, 0, 0.2);
--toggle-btn-border: rgba(255, 255, 255, 0.2);
--panel-bg: rgba(255, 255, 255, 0.05);
--toggle-hover-border: #ffffff;
--toggle-active-shadow: 0 0 10px rgba(0, 242, 255, 0.3);
--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);
--button-border: rgba(255, 255, 255, 0.2);
--button-text: #ffffff;
--button-hover-bg: rgba(255, 255, 255, 0.25);
--button-hover-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
--button-active-shadow: 0 0 20px rgba(79, 172, 254, 0.4);
--cube-edge-color: #333333;
--title-gradient: linear-gradient(45deg, #00f2fe, #4facfe);
}
:root[data-theme="light"] {
--bg-gradient: radial-gradient(circle at center, #ffffff 0%, #cccccc 100%);
--glass-bg: rgba(255, 255, 255, 0.75);
--glass-border: rgba(15, 23, 42, 0.12);
--glass-shadow: 0 8px 32px 0 rgba(15, 23, 42, 0.12);
--text-color: #0f172a;
--text-strong: #0f172a;
--text-secondary: rgba(15, 23, 42, 0.7);
--text-muted: rgba(15, 23, 42, 0.6);
--accent-cyan: #0ea5e9;
--accent-purple: #6366f1;
--primary-accent: #0ea5e9;
--title-glow: rgba(14, 165, 233, 0.35);
--toggle-bg: rgba(255, 255, 255, 0.85);
--toggle-border: rgba(15, 23, 42, 0.12);
--toggle-shadow: 0 8px 20px rgba(15, 23, 42, 0.12);
--toggle-btn-border: rgba(15, 23, 42, 0.18);
--toggle-hover-border: rgba(15, 23, 42, 0.5);
--toggle-active-shadow: 0 0 12px rgba(14, 165, 233, 0.25);
--panel-bg: rgba(255, 255, 255, 0.7);
--panel-border: rgba(15, 23, 42, 0.12);
--panel-shadow: 0 12px 24px rgba(15, 23, 42, 0.12);
--button-bg: rgba(255, 255, 255, 0.85);
--button-border: rgba(15, 23, 42, 0.16);
--button-text: #0f172a;
--button-hover-bg: rgba(255, 255, 255, 1);
--button-hover-shadow: 0 6px 18px rgba(15, 23, 42, 0.18);
--button-active-shadow: 0 0 18px rgba(14, 165, 233, 0.25);
--cube-edge-color: #000000;
--title-gradient: linear-gradient(45deg, #0ea5e9, #6366f1);
}
a {
@@ -44,11 +89,12 @@ body {
margin: 0;
display: flex;
min-width: 320px;
min-height: 100vh;
height: 100vh;
flex-direction: column;
background: var(--bg-gradient);
background-attachment: fixed;
color: var(--text-color);
overflow: hidden;
}
h1 {
@@ -81,7 +127,7 @@ button:focus-visible {
#app {
width: 100%;
min-height: 100vh;
height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
@@ -90,11 +136,56 @@ button:focus-visible {
/* Glassmorphism utility class */
.glass-panel {
background: var(--glass-bg);
backdrop-filter: blur(10px);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border-radius: 16px;
border: 1px solid var(--glass-border);
box-shadow: var(--glass-shadow);
}
/* Button Styles */
button.btn-neon {
background: var(--button-bg);
border: 1px solid var(--button-border);
color: var(--button-text);
padding: 10px 20px;
font-size: 0.95rem;
border-radius: 30px;
cursor: pointer;
transition: all 0.3s ease;
backdrop-filter: blur(4px);
font-weight: 500;
letter-spacing: 0.5px;
text-transform: uppercase;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
outline: none;
}
button.btn-neon:hover {
background: var(--button-hover-bg);
transform: translateY(-2px);
box-shadow: var(--button-hover-shadow);
border-color: var(--toggle-hover-border);
}
button.btn-neon.active {
background: linear-gradient(90deg, var(--accent-cyan), var(--accent-purple));
border-color: transparent;
box-shadow: var(--button-active-shadow);
font-weight: 700;
color: #fff;
}
button.btn-neon.icon-only {
padding: 10px;
border-radius: 50%;
width: 40px;
height: 40px;
}
@media (prefers-color-scheme: light) {
:root {
color: #213547;

View File

@@ -1,500 +0,0 @@
// Enum for colors
export const COLORS = {
WHITE: 'white',
YELLOW: 'yellow',
ORANGE: 'orange',
RED: 'red',
GREEN: 'green',
BLUE: 'blue',
BLACK: 'black'
};
// Faces enum
export const FACES = {
UP: 'up',
DOWN: 'down',
LEFT: 'left',
RIGHT: 'right',
FRONT: 'front',
BACK: 'back',
};
class Cubie {
constructor(id, x, y, z) {
this.id = id;
this.x = x;
this.y = y;
this.z = z;
this.faces = {
[FACES.UP]: COLORS.BLACK,
[FACES.DOWN]: COLORS.BLACK,
[FACES.LEFT]: COLORS.BLACK,
[FACES.RIGHT]: COLORS.BLACK,
[FACES.FRONT]: COLORS.BLACK,
[FACES.BACK]: COLORS.BLACK,
};
// Assign initial colors based on position (Solved State)
if (y === 1) this.faces[FACES.UP] = COLORS.WHITE;
if (y === -1) this.faces[FACES.DOWN] = COLORS.YELLOW;
if (x === -1) this.faces[FACES.LEFT] = COLORS.ORANGE;
if (x === 1) this.faces[FACES.RIGHT] = COLORS.RED;
if (z === 1) this.faces[FACES.FRONT] = COLORS.GREEN;
if (z === -1) this.faces[FACES.BACK] = COLORS.BLUE;
}
}
export class Cube {
constructor() {
this.cubies = [];
this.reset();
}
reset() {
this.cubies = [];
let id = 0;
for (let x = -1; x <= 1; x++) {
for (let y = -1; y <= 1; y++) {
for (let z = -1; z <= 1; z++) {
this.cubies.push(new Cubie(id++, x, y, z));
}
}
}
}
// Perform a standard move (U, D, L, R, F, B, M, E, S, x, y, z)
// Modifier: ' (prime) or 2 (double)
move(moveStr) {
let move = moveStr[0];
let modifier = moveStr.length > 1 ? moveStr[1] : '';
let direction = 1; // CW
let times = 1;
if (modifier === "'") {
direction = -1;
} else if (modifier === '2') {
times = 2;
}
// Standard Notation Mapping to (axis, index, direction)
// Note: Direction 1 in rotateLayer is "Positive Axis Rotation".
// We need to map Standard CW to Axis Direction.
// U (Up): y=1. Top face CW.
// Looking from Top (y+), CW is Rotation around Y (-). Wait.
// Right Hand Rule on Y axis: Thumb up, fingers curl CCW.
// So Positive Y Rotation is CCW from Top.
// So U (CW) is Negative Y Rotation.
// Let's verify _rotateCubiePosition for 'y'.
// dir > 0 (Pos): nx = z, nz = -x. (z, -x).
// (1,0) -> (0,-1). Right -> Back.
// Top View: Right is 3 o'clock. Back is 12 o'clock? No, Back is Up.
// Top View:
// B (z=-1)
// L(x=-1) R(x=1)
// F (z=1)
// Right (x=1) -> Back (z=-1).
// This is CCW.
// So `direction > 0` (Positive Y) is CCW from Top.
// Standard U is CW. So U is `direction = -1`.
// D (Down): y=-1. Bottom face CW.
// Looking from Bottom (y-), CW.
// If I look from bottom, Y axis points away.
// Positive Y is CCW from Top -> CW from Bottom?
// Let's check.
// Pos Y: Right -> Back.
// Bottom View: Right is Right. Back is "Down"?
// It's confusing.
// Let's use simple logic: D moves same direction as U' (visually from side?). No.
// U and D turn "same way" if you hold cube? No, opposite layers turn opposite relative to axis.
// D (CW) matches Y (Pos) ?
// Let's check movement of Front face on D.
// D moves Front -> Right.
// Y (Pos) moves Front (z=1) -> Right (x=1)?
// Pos Y: (0, 1) -> (1, 0). z=1 -> x=1.
// Yes. Front -> Right.
// So D (CW) = Y (Pos). `direction = 1`.
// L (Left): x=-1. Left face CW.
// L moves Front -> Down.
// X (Pos) moves Front (z=1) -> Up (y=1)?
// _rotateCubiePosition 'x':
// dir > 0: ny = -z. z=1 -> y=-1 (Down).
// So X (Pos) moves Front -> Down.
// So L (CW) = X (Pos). `direction = 1`.
// R (Right): x=1. Right face CW.
// R moves Front -> Up.
// X (Pos) moves Front -> Down.
// So R (CW) = X (Neg). `direction = -1`.
// F (Front): z=1. Front face CW.
// F moves Up -> Right.
// Z (Pos) moves Up (y=1) -> Left (x=-1)?
// _rotateCubiePosition 'z':
// dir > 0: nx = -y. y=1 -> x=-1 (Left).
// So Z (Pos) moves Up -> Left.
// F (CW) moves Up -> Right.
// So F (CW) = Z (Neg). `direction = -1`.
// Wait. My `rotateLayer` logic for Z was flipped in previous turn to match Visual.
// Let's re-read `_rotateCubieFaces` for Z.
// dir > 0 (CCW in Math/Pos): Left <- Up. Up moves to Left.
// So Pos Z moves Up to Left.
// F (CW) needs Up to Right.
// So F (CW) is Neg Z. `direction = -1`.
// B (Back): z=-1. Back face CW.
// B moves Up -> Left.
// Z (Pos) moves Up -> Left.
// So B (CW) = Z (Pos). `direction = 1`.
const layerOps = [];
switch (move) {
case 'U': layerOps.push({ axis: 'y', index: 1, dir: -1 }); break;
case 'D': layerOps.push({ axis: 'y', index: -1, dir: 1 }); break;
case 'L': layerOps.push({ axis: 'x', index: -1, dir: 1 }); break;
case 'R': layerOps.push({ axis: 'x', index: 1, dir: -1 }); break;
case 'F': layerOps.push({ axis: 'z', index: 1, dir: -1 }); break;
case 'B': layerOps.push({ axis: 'z', index: -1, dir: 1 }); break;
// Slices
case 'M': // Middle (between L and R), follows L direction
layerOps.push({ axis: 'x', index: 0, dir: 1 }); break;
case 'E': // Equator (between U and D), follows D direction
layerOps.push({ axis: 'y', index: 0, dir: 1 }); break;
case 'S': // Standing (between F and B), follows F direction
layerOps.push({ axis: 'z', index: 0, dir: -1 }); break;
// Whole Cube Rotations
case 'x': // Follows R
layerOps.push({ axis: 'x', index: -1, dir: -1 });
layerOps.push({ axis: 'x', index: 0, dir: -1 });
layerOps.push({ axis: 'x', index: 1, dir: -1 });
break;
case 'y': // Follows U
layerOps.push({ axis: 'y', index: -1, dir: -1 });
layerOps.push({ axis: 'y', index: 0, dir: -1 });
layerOps.push({ axis: 'y', index: 1, dir: -1 });
break;
case 'z': // Follows F
layerOps.push({ axis: 'z', index: -1, dir: -1 });
layerOps.push({ axis: 'z', index: 0, dir: -1 });
layerOps.push({ axis: 'z', index: 1, dir: -1 });
break;
}
// Apply operations
for (let i = 0; i < times; i++) {
layerOps.forEach(op => {
this.rotateLayer(op.axis, op.index, op.dir * direction);
});
}
}
// Rotate a layer
// axis: 'x', 'y', 'z'
// Helper: Rotate a 2D matrix
// direction: 1 (CW), -1 (CCW)
_rotateMatrix(matrix, direction) {
const N = matrix.length;
// Transpose
for (let i = 0; i < N; i++) {
for (let j = i; j < N; j++) {
[matrix[i][j], matrix[j][i]] = [matrix[j][i], matrix[i][j]];
}
}
// Reverse Rows (for CW) or Columns (for CCW)
if (direction > 0) {
// CW: Reverse each row
matrix.forEach(row => row.reverse());
} else {
// CCW: Reverse columns (or Reverse rows before transpose? No.)
// Transpose + Reverse Rows = CW.
// Transpose + Reverse Cols = CCW?
// Let's check:
// [1 2] T [1 3] RevCol [3 1] -> CCW?
// [3 4] [2 4] [4 2]
// 1 (0,0) -> (0,1). (Top-Left -> Top-Right). This is CW.
// Wait.
// CW: (x,y) -> (y, -x).
// (0,0) -> (0, 0).
// (1,0) -> (0, -1).
// Let's stick to standard:
// CW: Transpose -> Reverse Rows.
// CCW: Reverse Rows -> Transpose.
// Since I already transposed:
// To get CCW from Transpose:
// [1 2] T [1 3]
// [3 4] [2 4]
// Target CCW:
// [2 4]
// [1 3]
// This is reversing columns of Transpose.
// Or reversing rows of original, then transpose.
// Since I modify in place and already transposed:
// I need to reverse columns.
// Alternatively, re-implement:
// Undo transpose for CCW case and do correct order?
// No, let's just reverse columns.
for (let i = 0; i < N; i++) {
for (let j = 0; j < N / 2; j++) {
[matrix[j][i], matrix[N - 1 - j][i]] = [matrix[N - 1 - j][i], matrix[j][i]];
}
}
}
}
// index: -1, 0, 1
// direction: 1 (Positive Axis), -1 (Negative Axis)
rotateLayer(axis, index, direction) {
// 1. Select cubies in the layer
const layerCubies = this.cubies.filter(c => c[axis] === index);
// 2. Map cubies to 3x3 Matrix based on Axis View
// We need a consistent mapping from (u, v) -> Matrix[row][col]
// such that RotateMatrix(CW) corresponds to Physical CW Rotation.
// Grid coordinates:
// Row: 0..2, Col: 0..2
// Mapping function: returns {row, col} for a cubie
// Inverse function: updates cubie coordinates from {row, col}
let mapToGrid, updateFromGrid;
if (axis === 'z') {
// Front (z=1): X=Right, Y=Up.
// Matrix: Row 0 is Top (y=1). Col 0 is Left (x=-1).
mapToGrid = (c) => ({ row: 1 - c.y, col: c.x + 1 });
updateFromGrid = (c, row, col) => { c.y = 1 - row; c.x = col - 1; };
} else if (axis === 'x') {
// Right (x=1): Y=Up, Z=Back?
// CW Rotation around X (Right face):
// Up -> Front -> Down -> Back.
// Matrix: Row 0 is Top (y=1).
// Col 0 is Front (z=1)?
// If Col 0 is Front, Col 2 is Back (z=-1).
// Let's check CW:
// Top (y=1) -> Front (z=1).
// Matrix (0, ?) -> (?, 0).
// (0, 1) [Top-Center] -> (1, 0) [Front-Center].
// Row 0 -> Col 0. (Transpose).
// Then Reverse Rows?
// (0, 1) -> (1, 0).
// (0, 0) [Top-Front] -> (0, 0) [Front-Top]? No.
// Top-Front (y=1, z=1).
// Rot X CW: (y, z) -> (-z, y).
// (1, 1) -> (-1, 1). (Back-Top).
// Wait.
// Rot X CW:
// Y->Z->-Y->-Z.
// Up(y=1) -> Front(z=1)? No.
// Standard Axis Rotation (Right Hand Rule):
// Thumb +X. Fingers Y -> Z.
// So Y axis moves towards Z axis.
// (0, 1, 0) -> (0, 0, 1).
// Up -> Front.
// So Top (y=1) moves to Front (z=1).
// Let's map:
// Row 0 (Top, y=1). Row 2 (Bottom, y=-1).
// Col 0 (Front, z=1). Col 2 (Back, z=-1).
mapToGrid = (c) => ({ row: 1 - c.y, col: 1 - c.z });
updateFromGrid = (c, row, col) => { c.y = 1 - row; c.z = 1 - col; };
} else if (axis === 'y') {
// Up (y=1): Z=Back, X=Right.
// Rot Y CW:
// Z -> X.
// Back (z=-1) -> Right (x=1).
// Matrix: Row 0 (Back, z=-1). Row 2 (Front, z=1).
// Col 0 (Left, x=-1). Col 2 (Right, x=1).
mapToGrid = (c) => ({ row: c.z + 1, col: c.x + 1 });
updateFromGrid = (c, row, col) => { c.z = row - 1; c.x = col - 1; };
}
// 3. Create Matrix
const matrix = Array(3).fill(null).map(() => Array(3).fill(null));
layerCubies.forEach(c => {
const { row, col } = mapToGrid(c);
matrix[row][col] = c;
});
// 4. Rotate Matrix
// Note: Direction 1 is Physical CW (CCW in Math).
// Mapping analysis shows that for all axes (X, Y, Z),
// Physical CW corresponds to Matrix CW.
// However, rotateLayer receives direction -1 for CW (from move() notation).
// _rotateMatrix expects direction 1 for CW.
// So we must invert the direction for all axes.
const matrixDirection = -direction;
this._rotateMatrix(matrix, matrixDirection);
// 5. Update Cubie Coordinates
for (let r = 0; r < 3; r++) {
for (let c = 0; c < 3; c++) {
const cubie = matrix[r][c];
if (cubie) {
updateFromGrid(cubie, r, c);
}
}
}
// 6. Rotate Faces of each cubie
layerCubies.forEach(cubie => {
this._rotateCubieFaces(cubie, axis, direction);
});
}
_rotateCubieFaces(cubie, axis, direction) {
const f = { ...cubie.faces };
// Helper to swap faces
// We map: newFace <- oldFace
// Axis X Rotation (Right/Left)
// CW (dir > 0): Up -> Front -> Down -> Back -> Up
if (axis === 'x') {
if (direction > 0) {
// Corrected cycle for +X rotation:
// Up face moves to Front face
// Front face moves to Down face
// Down face moves to Back face
// Back face moves to Up face
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];
} else {
// Reverse cycle for -X
cubie.faces[FACES.UP] = f[FACES.FRONT];
cubie.faces[FACES.FRONT] = f[FACES.DOWN];
cubie.faces[FACES.DOWN] = f[FACES.BACK];
cubie.faces[FACES.BACK] = f[FACES.UP];
}
}
// Axis Y Rotation (Up/Down)
// CW (dir > 0): Front -> Right -> Back -> Left -> Front
// Front -> Right, Right -> Back, Back -> Left, Left -> Front
if (axis === 'y') {
if (direction > 0) {
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 {
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];
}
}
// Axis Z Rotation (Front/Back)
// CW (dir > 0) in Math is CCW visually: Top -> Left -> Bottom -> Right -> Top
if (axis === 'z') {
if (direction > 0) {
// CCW
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
cubie.faces[FACES.RIGHT] = f[FACES.UP];
cubie.faces[FACES.DOWN] = f[FACES.RIGHT];
cubie.faces[FACES.LEFT] = f[FACES.DOWN];
cubie.faces[FACES.UP] = f[FACES.LEFT];
}
}
}
// Get current state as standard 6-face matrices (for display/export)
getState() {
const state = {
[FACES.UP]: [[],[],[]],
[FACES.DOWN]: [[],[],[]],
[FACES.LEFT]: [[],[],[]],
[FACES.RIGHT]: [[],[],[]],
[FACES.FRONT]: [[],[],[]],
[FACES.BACK]: [[],[],[]]
};
this.cubies.forEach(c => {
// Map x,y,z to matrix indices
// UP: y=1. row = z (-1->0, 0->1, 1->2)?
// In `CubeCSS` I reversed this logic to match `Cube.js`.
// Let's stick to standard visual mapping.
// UP Face (Top View):
// Row 0 is Back (z=-1). Row 2 is Front (z=1).
// Col 0 is Left (x=-1). Col 2 is Right (x=1).
if (c.y === 1) {
const row = c.z + 1;
const col = c.x + 1;
state[FACES.UP][row][col] = c.faces[FACES.UP];
}
// DOWN Face (Bottom View):
// Usually "unfolded". Top of Down face is Front (z=1).
// Row 0 is Front (z=1). Row 2 is Back (z=-1).
// Col 0 is Left (x=-1). Col 2 is Right (x=1).
if (c.y === -1) {
const row = 1 - c.z;
const col = c.x + 1;
state[FACES.DOWN][row][col] = c.faces[FACES.DOWN];
}
// FRONT Face (z=1):
// Row 0 is Top (y=1). Row 2 is Bottom (y=-1).
// Col 0 is Left (x=-1). Col 2 is Right (x=1).
if (c.z === 1) {
const row = 1 - c.y;
const col = c.x + 1;
state[FACES.FRONT][row][col] = c.faces[FACES.FRONT];
}
// BACK Face (z=-1):
// Viewed from Back.
// Row 0 is Top (y=1).
// Col 0 is Right (x=1) (Viewer's Left). Col 2 is Left (x=-1).
if (c.z === -1) {
const row = 1 - c.y;
const col = 1 - c.x;
state[FACES.BACK][row][col] = c.faces[FACES.BACK];
}
// LEFT Face (x=-1):
// Viewed from Left.
// Row 0 is Top (y=1).
// Col 0 is Back (z=-1). Col 2 is Front (z=1).
if (c.x === -1) {
const row = 1 - c.y;
const col = c.z + 1;
state[FACES.LEFT][row][col] = c.faces[FACES.LEFT];
}
// RIGHT Face (x=1):
// Viewed from Right.
// Row 0 is Top (y=1).
// Col 0 is Front (z=1). Col 2 is Back (z=-1).
if (c.x === 1) {
const row = 1 - c.y;
const col = 1 - c.z;
state[FACES.RIGHT][row][col] = c.faces[FACES.RIGHT];
}
});
return state;
}
}

View File

@@ -0,0 +1,59 @@
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);
}
toCubies() {
return this.visual.toCubies();
}
validate() {
const valid = this.state.isValid();
return { valid, errors: valid ? [] : ['Invalid cube configuration (Parity or Orientation rules violated)'] };
}
}

398
src/utils/CubeModel.js Normal file
View File

@@ -0,0 +1,398 @@
/**
* Dedicated 3x3x3 Rubik's Cube Model
*
* Representation:
* A collection of 27 Cubie objects, each with position (x, y, z) and face colors.
* Coordinate System:
* x: Left (-1) to Right (1)
* y: Bottom (-1) to Top (1)
* z: Back (-1) to Front (1)
*
* This logical model maintains the state of the cube and handles rotations.
*/
export const COLORS = {
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",
};
// Standard Face Colors (Solved State)
const SOLVED_COLORS = {
[FACES.UP]: COLORS.WHITE,
[FACES.DOWN]: COLORS.YELLOW,
[FACES.LEFT]: COLORS.ORANGE,
[FACES.RIGHT]: COLORS.RED,
[FACES.FRONT]: COLORS.GREEN,
[FACES.BACK]: COLORS.BLUE,
};
class Cubie {
constructor(id, x, y, z) {
this.id = id;
this.x = x;
this.y = y;
this.z = z;
this.faces = {
[FACES.UP]: COLORS.BLACK,
[FACES.DOWN]: COLORS.BLACK,
[FACES.LEFT]: COLORS.BLACK,
[FACES.RIGHT]: COLORS.BLACK,
[FACES.FRONT]: COLORS.BLACK,
[FACES.BACK]: COLORS.BLACK,
};
this.initColors();
}
// Set initial colors based on position in solved state
initColors() {
if (this.y === 1) this.faces[FACES.UP] = SOLVED_COLORS[FACES.UP];
if (this.y === -1) this.faces[FACES.DOWN] = SOLVED_COLORS[FACES.DOWN];
if (this.x === -1) this.faces[FACES.LEFT] = SOLVED_COLORS[FACES.LEFT];
if (this.x === 1) this.faces[FACES.RIGHT] = SOLVED_COLORS[FACES.RIGHT];
if (this.z === 1) this.faces[FACES.FRONT] = SOLVED_COLORS[FACES.FRONT];
if (this.z === -1) this.faces[FACES.BACK] = SOLVED_COLORS[FACES.BACK];
}
}
export class CubeModel {
constructor() {
this.size = 3;
this.cubies = [];
this.init();
}
init() {
this.cubies = [];
let id = 0;
for (let x = -1; x <= 1; x++) {
for (let y = -1; y <= 1; y++) {
for (let z = -1; z <= 1; z++) {
this.cubies.push(new Cubie(id++, x, y, z));
}
}
}
}
reset() {
this.init();
}
/**
* Rotates a layer around an axis.
* @param {string} axis - 'x', 'y', 'z'
* @param {number} index - -1 (Left/Bottom/Back), 0 (Middle), 1 (Right/Top/Front)
* @param {number} direction - 1 (CW), -1 (CCW) relative to axis positive direction
*/
rotateLayer(axis, index, direction) {
// Determine the relevant cubies in the slice
const slice = this.cubies.filter((c) => c[axis] === index);
// Coordinate rotation (Matrix Logic)
// 90 deg CW rotation formulas:
// X-Axis: (y, z) -> (-z, y)
// Y-Axis: (x, z) -> (z, -x)
// Z-Axis: (x, y) -> (-y, x)
// Note: direction 1 is usually CCW in math (right hand rule around axis).
// Let's verify standard:
// Right Hand Rule with Thumb along Axis: Fingers curl in Positive Rotation direction.
// X (Right): Curl from Y (Up) to Z (Front). (0,1,0)->(0,0,1).
// y' = -z, z' = y?
// Let's check (0,1): y=1, z=0 -> y'=0, z'=1. Correct.
// So if direction is 1 (Positive/CW around axis):
// X: y' = -z, z' = y
// Y: z' = -x, x' = z
// Z: x' = -y, y' = x
// If direction is -1: Inverse.
slice.forEach((cubie) => {
this._rotateCubieCoordinates(cubie, axis, direction);
this._rotateCubieFaces(cubie, axis, direction);
});
}
_rotateCubieCoordinates(cubie, axis, direction) {
const { x, y, z } = cubie;
if (axis === "x") {
if (direction === 1) {
cubie.y = -z;
cubie.z = y;
} else {
cubie.y = z;
cubie.z = -y;
}
} else if (axis === "y") {
if (direction === 1) {
cubie.z = -x;
cubie.x = z;
} else {
cubie.z = x;
cubie.x = -z;
}
} else if (axis === "z") {
if (direction === 1) {
// CW
cubie.x = -y;
cubie.y = x;
} else {
// CCW
cubie.x = y;
cubie.y = -x;
}
}
}
_rotateCubieFaces(cubie, axis, direction) {
// When a cubie rotates, its faces move to new positions.
// We swap the COLORS on the faces.
// Example: Rotate X (Roll Forward). Up Face becomes Front Face.
// So new Front Color = old Up Color.
// cubie.faces[FRONT] = old_faces[UP]
const f = { ...cubie.faces };
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
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
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
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
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
cubie.faces[FACES.RIGHT] = f[FACES.UP];
cubie.faces[FACES.DOWN] = f[FACES.RIGHT];
cubie.faces[FACES.LEFT] = f[FACES.DOWN];
cubie.faces[FACES.UP] = f[FACES.LEFT];
}
}
}
toCubies() {
// Return copy of state for rendering
// CubeCSS expects array of objects with x, y, z, faces
return this.cubies.map((c) => ({
id: c.id,
x: c.x,
y: c.y,
z: c.z,
faces: { ...c.faces },
}));
}
/**
* Applies a standard Rubik's Cube move
* @param {string} move - e.g. "U", "R'", "F2"
*/
applyMove(move) {
const base = move[0];
const modifier = move.substring(1);
let direction = -1; // Standard CW is -1 for U, L, F, B? Let's check.
// Direction Mapping based on rotateLayer Math:
// X(1) = CW (Up->Front)
// Y(1) = CW (Right->Front)
// Z(1) = CCW (Right->Up)
// R (CW around X): 1
// L (CW around -X): -1
// U (CW around Y): 1
// D (CW around -Y): -1
// F (CW around Z): -1 (since Z(1) is CCW)
// 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;
}
if (modifier === "'") direction *= -1;
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;
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;
}
}
}
// Debug printer for tests
toString() {
let out = "Cube State (3x3x3):\n";
// We can print faces.
// Order: U, D, F, B, L, R
const printFace = (face, name) => {
out += `Face ${name}:\n`;
// Grid 3x3.
// U: y=1. x from -1 to 1. z from -1 to 1.
// Coordinate mapping depends on face.
// Let's iterate standard grid rows/cols.
for (let r = 0; r < 3; r++) {
let rowStr = "";
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).
// 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.
// 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.
// 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,
);
if (cubie) {
rowStr += cubie.faces[face][0].toUpperCase() + " ";
} else {
rowStr += "? ";
}
}
out += rowStr + "\n";
}
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");
return out;
}
scramble(n = 20) {
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)
// D: y=-1, dir 1
// L: x=-1, dir -1
// R: x=1, dir 1
// F: z=1, dir 1
// B: z=-1, dir -1
// We can just generate random rotateLayer calls
for (let i = 0; i < n; i++) {
const axis = axes[Math.floor(Math.random() * axes.length)];
// Allow middle layer? Standard Scramble usually doesnt.
const index = indices[Math.floor(Math.random() * indices.length)];
const dir = Math.random() > 0.5 ? 1 : -1;
this.rotateLayer(axis, index, dir);
}
}
}

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

@@ -0,0 +1,398 @@
// 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;
}
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,112 +0,0 @@
export class Matrix4 {
constructor() {
this.elements = [
1, 0, 0, 0,
0, 1, 0, 0,
0, 0, 1, 0,
0, 0, 0, 1
];
}
static identity() {
return new Matrix4();
}
multiply(other) {
const a = this.elements;
const b = other.elements;
const result = new Matrix4();
const r = result.elements;
// a is row-major? No, CSS matrix3d is column-major.
// Let's stick to column-major as per WebGL/CSS standard.
// r[0] = a[0]*b[0] + a[4]*b[1] + a[8]*b[2] + a[12]*b[3] ...
for (let i = 0; i < 4; i++) { // Column of B
for (let j = 0; j < 4; j++) { // Row of A
let sum = 0;
for (let k = 0; k < 4; k++) {
sum += a[j + k * 4] * b[i * 4 + k]; // Correct for column-major storage
}
r[j + i * 4] = sum;
}
}
return result;
}
// Multiply this * other
multiplySelf(other) {
this.elements = this.multiply(other).elements;
return this;
}
// Multiply other * this (pre-multiply)
premultiply(other) {
this.elements = other.multiply(this).elements;
return this;
}
static translation(x, y, z) {
const m = new Matrix4();
m.elements[12] = x;
m.elements[13] = y;
m.elements[14] = z;
return m;
}
static rotationX(angleRad) {
const m = new Matrix4();
const c = Math.cos(angleRad);
const s = Math.sin(angleRad);
m.elements[5] = c;
m.elements[6] = s;
m.elements[9] = -s;
m.elements[10] = c;
return m;
}
static rotationY(angleRad) {
const m = new Matrix4();
const c = Math.cos(angleRad);
const s = Math.sin(angleRad);
m.elements[0] = c;
m.elements[2] = -s;
m.elements[8] = s;
m.elements[10] = c;
return m;
}
static rotationZ(angleRad) {
const m = new Matrix4();
const c = Math.cos(angleRad);
const s = Math.sin(angleRad);
m.elements[0] = c;
m.elements[1] = s;
m.elements[4] = -s;
m.elements[5] = c;
return m;
}
translate(x, y, z) {
return this.multiplySelf(Matrix4.translation(x, y, z));
}
rotateX(deg) {
return this.multiplySelf(Matrix4.rotationX(deg * Math.PI / 180));
}
rotateY(deg) {
return this.multiplySelf(Matrix4.rotationY(deg * Math.PI / 180));
}
rotateZ(deg) {
return this.multiplySelf(Matrix4.rotationZ(deg * Math.PI / 180));
}
toCSS() {
// CSS matrix3d takes comma-separated values
// Round to avoid scientific notation like 1e-15 which CSS hates
const rounded = this.elements.map(v => Math.abs(v) < 1e-10 ? 0 : v);
return `matrix3d(${rounded.join(',')})`;
}
}

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,139 @@
import Cube from "cubejs";
// Initialize the core pruning tables on module load
Cube.initSolver();
import { DeepCube, CORNERS, EDGES } from "../DeepCube.js";
export class KociembaSolver {
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}`,
);
}
}
}

View File

@@ -0,0 +1,55 @@
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);
postMessage({
type: "STATE_UPDATE",
payload: {
cubies,
},
});
} catch (e) {
console.error("[Worker] Error generating cubies:", e);
postMessage({ type: "ERROR", payload: e.message });
}
};
self.onmessage = (e) => {
const { type, payload } = e.data;
switch (type) {
case "INIT":
case "RESET":
cube.reset();
sendUpdate();
break;
case "ROTATE_LAYER": {
const { axis, index, direction, steps = 1 } = payload;
cube.rotateLayer(axis, index, direction, steps);
sendUpdate();
break;
}
case "TURN": {
const { move } = payload;
cube.applyTurn(move);
sendUpdate();
break;
}
case "VALIDATE":
const validation = cube.validate();
postMessage({
type: "VALIDATION_RESULT",
payload: { valid: validation.valid, errors: validation.errors },
});
break;
}
};

166
test/cube_integrity.test.js Normal file
View File

@@ -0,0 +1,166 @@
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,32 +1,33 @@
import { Cube, FACES, COLORS } from "../src/utils/Cube.js";
import assert from "assert";
import { Cube, FACES, COLORS } from '../src/utils/Cube.js';
import assert from 'assert';
console.log('Running Cube Logic Tests...');
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);
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}`);
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');
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');
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'.
@@ -42,13 +43,20 @@ checkFace(1, 1, 1, FACES.RIGHT, COLORS.RED, 'Initial Top-Right-Front RIGHT');
// 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);
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');
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).
@@ -68,29 +76,42 @@ const result1 = checkFace(1, -1, 1, FACES.FRONT, COLORS.WHITE, 'After X+90: Old
// 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');
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)');
console.log("PASS: X Axis Rotation Logic seems correct (if fixed)");
} else {
console.log('FAIL: X Axis Rotation Logic is broken');
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)');
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');
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');
console.log("PASS: Y Axis Rotation Logic seems correct");
} else {
console.log('FAIL: Y Axis Rotation Logic is broken');
console.log("FAIL: Y Axis Rotation Logic is broken");
}

View File

@@ -1,19 +1,20 @@
import { Cube, FACES, COLORS } from "../src/utils/Cube.js";
import assert from "assert";
import { Cube, FACES, COLORS } from '../src/utils/Cube.js';
import assert from 'assert';
console.log('Running Cube Matrix Rotation Tests...');
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);
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;
@@ -24,14 +25,14 @@ const checkCubie = (origX, origY, origZ, newX, newY, newZ, faceCheck) => {
// 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)');
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);
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?)
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).
@@ -40,27 +41,26 @@ cube.rotateLayer('z', 1, -1); // CW (direction -1 in move(), but rotateLayer tak
// 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);
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');
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)');
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);
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()?)
cube.rotateLayer("x", 1, -1); // CW (direction -1 for R in move()?)
// move('R') calls rotateLayer('x', 1, -1).
// So let's test -1.
@@ -69,15 +69,14 @@ cube.rotateLayer('x', 1, -1); // CW (direction -1 for R in move()?)
// 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);
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');
console.log("PASS X-Axis CW");
// Test 3: Y-Axis Rotation (Up Face)
// Up Face is y=1.
@@ -86,24 +85,23 @@ console.log('PASS X-Axis CW');
// 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)');
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);
const upFront = cube.cubies.find((c) => c.x === 0 && c.y === 1 && c.z === 1);
cube.rotateLayer('y', 1, -1); // CW (direction -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);
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');
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]
)

126
test/simulate_moves.js Normal file
View File

@@ -0,0 +1,126 @@
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(" ")),
);
};
// Helper to check if a face matches expected color (center color)
const checkFaceColor = (matrix, expectedColor) => {
return matrix.every((row) => row.every((c) => c === expectedColor));
};
console.log("=== RUBIK'S CUBE SIMULATION & DIAGNOSTIC ===");
const cube = new Cube();
// 1. Initial State Check
console.log("\n1. Checking Initial State...");
let state = cube.getState();
const isSolved =
checkFaceColor(state[FACES.UP], COLORS.WHITE) &&
checkFaceColor(state[FACES.DOWN], COLORS.YELLOW) &&
checkFaceColor(state[FACES.FRONT], COLORS.GREEN) &&
checkFaceColor(state[FACES.BACK], COLORS.BLUE) &&
checkFaceColor(state[FACES.LEFT], COLORS.ORANGE) &&
checkFaceColor(state[FACES.RIGHT], COLORS.RED);
if (isSolved) {
console.log("✅ Initial state is SOLVED.");
} else {
console.error("❌ Initial state is BROKEN.");
process.exit(1);
}
// 2. Simulate Move: Front Face Drag Down
// Visual: Drag Down on Front Face (Left Layer).
// Axis: X. Index: -1 (Left).
// Physical expectation: The Left slice moves "towards the user" (if looking from top) or "down" (if looking from front).
// Standard Notation: L (Left CW).
// L move: Top -> Front -> Down -> Back -> Top.
// Let's verify what L does.
// Standard L: Front gets Top color.
// If I drag Left Layer DOWN on Front face, the Front face pieces move DOWN.
// So Front gets Top pieces.
// So Drag Down = L = Front gets Top.
console.log("\n2. Simulating: Left Layer (x=-1) Rotation (L-like move)...");
// We need to find which 'direction' in our engine corresponds to L.
// Our engine: rotateLayer('x', -1, direction).
// Try direction = 1
console.log("-> Applying 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],
];
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).",
);
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).",
);
console.log("=> CONCLUSION: direction=1 corresponds to Drag Up (L').");
} else {
console.error("❌ Unexpected colors:", frontLeftCol);
}
// Reset for next test
cube.reset();
// 3. Simulate Move: Front Face Drag Right
// Visual: Drag Right on Front Face (Top Layer).
// Axis: Y. Index: 1 (Top).
// Physical expectation: Top slice moves Right.
// Standard Notation: U' (Up CCW) ? No.
// If I hold cube, drag Top Layer to Right.
// Front face pieces move to Right face.
// Standard U (CW): Front -> Left.
// So Drag Right is U' (CCW).
// Let's verify.
// Drag Right: Front -> Right.
console.log("\n3. Simulating: Top Layer (y=1) Rotation...");
// Try direction = 1
console.log("-> Applying rotateLayer('y', 1, 1)...");
cube.rotateLayer("y", 1, 1);
state = cube.getState();
// Check result on Top Row of Front Face
// Front is Green. Left is Orange. Right is Red.
// If Drag Right: Front-Top-Row should be Orange? No.
// Drag Right: The pieces move Right. So Front REPLACES Right?
// Or Front GETS Left?
// Visually: The face moves Right. So the Green pieces go to Right face.
// And Front face gets Left (Orange) pieces.
// So Front-Top-Row should be Orange.
const frontTopRow = state[FACES.FRONT][0];
console.log("Front Top Row colors:", frontTopRow);
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)) {
console.log("⚠️ Result: Front got Red (Right). This matches 'Drag Left'.");
console.log("=> CONCLUSION: direction=1 corresponds to Drag Left.");
} else {
console.error("❌ Unexpected colors:", frontTopRow);
}
console.log("\n=== END SIMULATION ===");

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)]);

70
test/test_new_model.js Normal file
View File

@@ -0,0 +1,70 @@
import { CubeModel, FACES, COLORS } from "../src/utils/CubeModel.js";
console.log("Running CubeModel Rotation Logic Tests...");
const cube1 = new CubeModel();
const cube2 = new CubeModel();
const compareCubes = (c1, c2, message) => {
const s1 = c1.toString();
const s2 = c2.toString();
if (s1 === s2) {
console.log(`✅ PASS: ${message}`);
return true;
} else {
console.error(`❌ FAIL: ${message}`);
console.log("Expected (Standard Move):");
console.log(s2);
console.log("Actual (Layer Rotation):");
console.log(s1);
return false;
}
};
// 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");
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");
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");
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");
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");
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");
compareCubes(cube1, cube2, "Back Layer CW matches B");

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

View File

@@ -1,11 +1,12 @@
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import pkg from './package.json'
// https://vite.dev/config/
export default defineConfig({
plugins: [vue()],
define: {
'__APP_VERSION__': JSON.stringify(process.env.npm_package_version),
'__APP_VERSION__': JSON.stringify(pkg.version),
},
server: {
port: 5174,