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.
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -24,3 +24,4 @@ dist-ssr
|
|||||||
*.sw?
|
*.sw?
|
||||||
|
|
||||||
.agent/
|
.agent/
|
||||||
|
cache/
|
||||||
|
|||||||
4362
package-lock.json
generated
4362
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -9,8 +9,8 @@
|
|||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"cubejs": "^1.3.2",
|
||||||
"lucide-vue-next": "^0.564.0",
|
"lucide-vue-next": "^0.564.0",
|
||||||
"rubiks-js": "^1.0.0",
|
|
||||||
"vue": "^3.5.13"
|
"vue": "^3.5.13"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import SmartCube from './components/renderers/SmartCube.vue'
|
import SmartCube from "./components/renderers/SmartCube.vue";
|
||||||
import NavBar from './components/NavBar.vue'
|
import NavBar from "./components/NavBar.vue";
|
||||||
import Footer from './components/Footer.vue'
|
import Footer from "./components/Footer.vue";
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { Sun, Moon, Grid2x2 } from 'lucide-vue-next';
|
import { Sun, Moon, Grid2x2 } from "lucide-vue-next";
|
||||||
import { ref, onMounted } from 'vue';
|
import { ref, onMounted } from "vue";
|
||||||
import { useSettings } from '../composables/useSettings';
|
import { useSettings } from "../composables/useSettings";
|
||||||
|
|
||||||
const { isCubeTranslucent, toggleCubeTranslucent } = useSettings();
|
const { isCubeTranslucent, toggleCubeTranslucent } = useSettings();
|
||||||
const isDark = ref(true);
|
const isDark = ref(true);
|
||||||
|
|
||||||
const setTheme = (dark) => {
|
const setTheme = (dark) => {
|
||||||
isDark.value = dark;
|
isDark.value = dark;
|
||||||
const theme = dark ? 'dark' : 'light';
|
const theme = dark ? "dark" : "light";
|
||||||
document.documentElement.dataset.theme = theme;
|
document.documentElement.dataset.theme = theme;
|
||||||
localStorage.setItem('theme', theme);
|
localStorage.setItem("theme", theme);
|
||||||
};
|
};
|
||||||
|
|
||||||
const toggleTheme = () => {
|
const toggleTheme = () => {
|
||||||
@@ -18,9 +18,9 @@ const toggleTheme = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
const savedTheme = localStorage.getItem('theme');
|
const savedTheme = localStorage.getItem("theme");
|
||||||
if (savedTheme) {
|
if (savedTheme) {
|
||||||
setTheme(savedTheme === 'dark');
|
setTheme(savedTheme === "dark");
|
||||||
} else {
|
} else {
|
||||||
setTheme(true);
|
setTheme(true);
|
||||||
}
|
}
|
||||||
@@ -38,14 +38,22 @@ onMounted(() => {
|
|||||||
<button
|
<button
|
||||||
class="btn-neon nav-btn icon-only"
|
class="btn-neon nav-btn icon-only"
|
||||||
@click="toggleCubeTranslucent"
|
@click="toggleCubeTranslucent"
|
||||||
:title="isCubeTranslucent ? 'Wyłącz przezroczystość kostki' : 'Włącz przezroczystość kostki'"
|
:title="
|
||||||
|
isCubeTranslucent
|
||||||
|
? 'Wyłącz przezroczystość kostki'
|
||||||
|
: 'Włącz przezroczystość kostki'
|
||||||
|
"
|
||||||
:class="{ active: isCubeTranslucent }"
|
:class="{ active: isCubeTranslucent }"
|
||||||
>
|
>
|
||||||
<Grid2x2 :size="20" />
|
<Grid2x2 :size="20" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<!-- Theme Toggle -->
|
<!-- 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" />
|
<Sun v-if="isDark" :size="20" />
|
||||||
<Moon v-else :size="20" />
|
<Moon v-else :size="20" />
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -1,23 +1,23 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { computed } from 'vue';
|
import { computed } from "vue";
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
start: {
|
start: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: true // {x, y, z}
|
required: true, // {x, y, z}
|
||||||
},
|
},
|
||||||
end: {
|
end: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: true // {x, y, z}
|
required: true, // {x, y, z}
|
||||||
},
|
},
|
||||||
color: {
|
color: {
|
||||||
type: String,
|
type: String,
|
||||||
default: 'var(--text-color, #fff)'
|
default: "var(--text-color, #fff)",
|
||||||
},
|
},
|
||||||
thickness: {
|
thickness: {
|
||||||
type: Number,
|
type: Number,
|
||||||
default: 1
|
default: 1,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const style = computed(() => {
|
const style = computed(() => {
|
||||||
@@ -43,13 +43,13 @@ const style = computed(() => {
|
|||||||
width: `${length}px`,
|
width: `${length}px`,
|
||||||
height: `${props.thickness}px`,
|
height: `${props.thickness}px`,
|
||||||
backgroundColor: props.color,
|
backgroundColor: props.color,
|
||||||
position: 'absolute',
|
position: "absolute",
|
||||||
top: '0',
|
top: "0",
|
||||||
left: '0',
|
left: "0",
|
||||||
transformOrigin: 'center center',
|
transformOrigin: "center center",
|
||||||
transform: `translate3d(${midX}px, ${midY}px, ${midZ}px) rotateY(${-yaw}rad) rotateZ(${pitch}rad) translate(-50%, -50%)`,
|
transform: `translate3d(${midX}px, ${midY}px, ${midZ}px) rotateY(${-yaw}rad) rotateZ(${pitch}rad) translate(-50%, -50%)`,
|
||||||
opacity: 0.3, // Delicate
|
opacity: 0.3, // Delicate
|
||||||
pointerEvents: 'none'
|
pointerEvents: "none",
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,5 +1,31 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
const emit = defineEmits(['move', 'scramble'])
|
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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -11,14 +37,26 @@ const emit = defineEmits(['move', 'scramble'])
|
|||||||
<button class="btn-neon move-btn" @click="emit('move', 'L')">L</button>
|
<button class="btn-neon move-btn" @click="emit('move', 'L')">L</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="controls-row">
|
<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', 'U-prime')">
|
||||||
<button class="btn-neon move-btn" @click="emit('move', 'D-prime')">D'</button>
|
U'
|
||||||
<button class="btn-neon move-btn" @click="emit('move', 'L-prime')">L'</button>
|
</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>
|
||||||
<div class="controls-row">
|
<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', 'U2')">
|
||||||
<button class="btn-neon move-btn" @click="emit('move', 'D2')">D2</button>
|
U2
|
||||||
<button class="btn-neon move-btn" @click="emit('move', 'L2')">L2</button>
|
</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>
|
</div>
|
||||||
|
|
||||||
@@ -29,14 +67,41 @@ const emit = defineEmits(['move', 'scramble'])
|
|||||||
<button class="btn-neon move-btn" @click="emit('move', 'B')">B</button>
|
<button class="btn-neon move-btn" @click="emit('move', 'B')">B</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="controls-row">
|
<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', 'R-prime')">
|
||||||
<button class="btn-neon move-btn" @click="emit('move', 'F-prime')">F'</button>
|
R'
|
||||||
<button class="btn-neon move-btn" @click="emit('move', 'B-prime')">B'</button>
|
</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>
|
||||||
<div class="controls-row">
|
<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', 'R2')">
|
||||||
<button class="btn-neon move-btn" @click="emit('move', 'F2')">F2</button>
|
R2
|
||||||
<button class="btn-neon move-btn" @click="emit('move', 'B2')">B2</button>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -44,6 +109,7 @@ const emit = defineEmits(['move', 'scramble'])
|
|||||||
Scramble
|
Scramble
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@@ -77,11 +143,58 @@ const emit = defineEmits(['move', 'scramble'])
|
|||||||
padding: 0 10px;
|
padding: 0 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.scramble-btn {
|
.bottom-left-controls {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: 72px;
|
bottom: 72px;
|
||||||
left: 24px;
|
left: 24px;
|
||||||
z-index: 50;
|
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>
|
</style>
|
||||||
|
|
||||||
|
|||||||
@@ -1,96 +1,99 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, onMounted, onUnmounted, watch, nextTick } from 'vue'
|
import { ref, computed, onMounted, onUnmounted, watch, nextTick } from "vue";
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
moves: {
|
moves: {
|
||||||
type: Array,
|
type: Array,
|
||||||
required: true
|
required: true,
|
||||||
}
|
},
|
||||||
})
|
});
|
||||||
|
|
||||||
const emit = defineEmits(['reset', 'copy', 'add-moves', 'open-add-modal'])
|
const emit = defineEmits(["reset", "copy", "add-moves", "open-add-modal"]);
|
||||||
|
|
||||||
const MIN_MOVES_COLUMN_GAP = 6
|
const MIN_MOVES_COLUMN_GAP = 6;
|
||||||
|
|
||||||
const movesHistoryEl = ref(null)
|
const movesHistoryEl = ref(null);
|
||||||
const samplePillEl = ref(null)
|
const samplePillEl = ref(null);
|
||||||
const movesPerRow = ref(0)
|
const movesPerRow = ref(0);
|
||||||
const movesColumnGap = ref(MIN_MOVES_COLUMN_GAP)
|
const movesColumnGap = ref(MIN_MOVES_COLUMN_GAP);
|
||||||
|
|
||||||
const displayMoves = computed(() => props.moves || [])
|
const displayMoves = computed(() => props.moves || []);
|
||||||
|
|
||||||
const moveRows = computed(() => {
|
const moveRows = computed(() => {
|
||||||
const perRow = movesPerRow.value || displayMoves.value.length || 1
|
const perRow = movesPerRow.value || displayMoves.value.length || 1;
|
||||||
const rows = []
|
const rows = [];
|
||||||
const all = displayMoves.value
|
const all = displayMoves.value;
|
||||||
for (let i = 0; i < all.length; i += perRow) {
|
for (let i = 0; i < all.length; i += perRow) {
|
||||||
rows.push(all.slice(i, i + perRow))
|
rows.push(all.slice(i, i + perRow));
|
||||||
}
|
}
|
||||||
return rows
|
return rows;
|
||||||
})
|
});
|
||||||
|
|
||||||
const hasMoves = computed(() => displayMoves.value.length > 0)
|
const hasMoves = computed(() => displayMoves.value.length > 0);
|
||||||
|
|
||||||
const copyQueueToClipboard = () => {
|
const copyQueueToClipboard = () => {
|
||||||
emit('copy')
|
emit("copy");
|
||||||
}
|
};
|
||||||
|
|
||||||
const resetQueue = () => {
|
const resetQueue = () => {
|
||||||
emit('reset')
|
emit("reset");
|
||||||
}
|
};
|
||||||
|
|
||||||
const setSamplePill = (el) => {
|
const setSamplePill = (el) => {
|
||||||
if (el && !samplePillEl.value) {
|
if (el && !samplePillEl.value) {
|
||||||
samplePillEl.value = el
|
samplePillEl.value = el;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const recalcMovesLayout = () => {
|
const recalcMovesLayout = () => {
|
||||||
const container = movesHistoryEl.value
|
const container = movesHistoryEl.value;
|
||||||
const pill = samplePillEl.value
|
const pill = samplePillEl.value;
|
||||||
if (!container || !pill) return
|
if (!container || !pill) return;
|
||||||
|
|
||||||
const containerWidth = container.clientWidth
|
const containerWidth = container.clientWidth;
|
||||||
const pillWidth = pill.offsetWidth
|
const pillWidth = pill.offsetWidth;
|
||||||
if (pillWidth <= 0) return
|
if (pillWidth <= 0) return;
|
||||||
|
|
||||||
const totalWidth = (cols) => {
|
const totalWidth = (cols) => {
|
||||||
if (cols <= 0) return 0
|
if (cols <= 0) return 0;
|
||||||
if (cols === 1) return pillWidth
|
if (cols === 1) return pillWidth;
|
||||||
return cols * pillWidth + (cols - 1) * MIN_MOVES_COLUMN_GAP
|
return cols * pillWidth + (cols - 1) * MIN_MOVES_COLUMN_GAP;
|
||||||
}
|
};
|
||||||
|
|
||||||
let cols = Math.floor((containerWidth + MIN_MOVES_COLUMN_GAP) / (pillWidth + MIN_MOVES_COLUMN_GAP))
|
let cols = Math.floor(
|
||||||
if (cols < 1) cols = 1
|
(containerWidth + MIN_MOVES_COLUMN_GAP) /
|
||||||
|
(pillWidth + MIN_MOVES_COLUMN_GAP),
|
||||||
|
);
|
||||||
|
if (cols < 1) cols = 1;
|
||||||
while (cols > 1 && totalWidth(cols) > containerWidth) {
|
while (cols > 1 && totalWidth(cols) > containerWidth) {
|
||||||
cols -= 1
|
cols -= 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
let gap = 0
|
let gap = 0;
|
||||||
if (cols > 1) {
|
if (cols > 1) {
|
||||||
gap = (containerWidth - cols * pillWidth) / (cols - 1)
|
gap = (containerWidth - cols * pillWidth) / (cols - 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
movesPerRow.value = cols
|
movesPerRow.value = cols;
|
||||||
movesColumnGap.value = gap
|
movesColumnGap.value = gap;
|
||||||
}
|
};
|
||||||
|
|
||||||
const openAddModal = () => {
|
const openAddModal = () => {
|
||||||
emit('open-add-modal')
|
emit("open-add-modal");
|
||||||
}
|
};
|
||||||
|
|
||||||
watch(displayMoves, () => {
|
watch(displayMoves, () => {
|
||||||
nextTick(recalcMovesLayout)
|
nextTick(recalcMovesLayout);
|
||||||
})
|
});
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
window.addEventListener('resize', recalcMovesLayout)
|
window.addEventListener("resize", recalcMovesLayout);
|
||||||
nextTick(recalcMovesLayout)
|
nextTick(recalcMovesLayout);
|
||||||
})
|
});
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
window.removeEventListener('resize', recalcMovesLayout)
|
window.removeEventListener("resize", recalcMovesLayout);
|
||||||
})
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -108,7 +111,7 @@ onUnmounted(() => {
|
|||||||
class="move-pill"
|
class="move-pill"
|
||||||
:class="{
|
:class="{
|
||||||
'move-pill-active': m.status === 'in_progress',
|
'move-pill-active': m.status === 'in_progress',
|
||||||
'move-pill-pending': m.status === 'pending'
|
'move-pill-pending': m.status === 'pending',
|
||||||
}"
|
}"
|
||||||
:ref="rowIndex === 0 && idx === 0 ? setSamplePill : null"
|
:ref="rowIndex === 0 && idx === 0 ? setSamplePill : null"
|
||||||
>
|
>
|
||||||
@@ -135,7 +138,6 @@ onUnmounted(() => {
|
|||||||
reset
|
reset
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -218,5 +220,4 @@ onUnmounted(() => {
|
|||||||
outline: none;
|
outline: none;
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,9 +1,11 @@
|
|||||||
|
import { ref, computed } from "vue";
|
||||||
import { ref, computed } from 'vue';
|
import { COLORS, FACES } from "../utils/CubeModel";
|
||||||
import { COLORS, FACES } from '../utils/CubeModel';
|
|
||||||
|
|
||||||
// Singleton worker
|
// Singleton worker
|
||||||
const worker = new Worker(new URL('../workers/Cube.worker.js', import.meta.url), { type: 'module' });
|
const worker = new Worker(
|
||||||
|
new URL("../workers/Cube.worker.js", import.meta.url),
|
||||||
|
{ type: "module" },
|
||||||
|
);
|
||||||
|
|
||||||
// Reactive state
|
// Reactive state
|
||||||
const cubies = ref([]);
|
const cubies = ref([]);
|
||||||
@@ -12,35 +14,37 @@ const validationResult = ref(null);
|
|||||||
|
|
||||||
worker.onmessage = (e) => {
|
worker.onmessage = (e) => {
|
||||||
const { type, payload } = e.data;
|
const { type, payload } = e.data;
|
||||||
if (type === 'STATE_UPDATE') {
|
if (type === "STATE_UPDATE") {
|
||||||
cubies.value = payload.cubies;
|
cubies.value = payload.cubies;
|
||||||
isReady.value = true;
|
isReady.value = true;
|
||||||
} else if (type === 'VALIDATION_RESULT') {
|
} else if (type === "VALIDATION_RESULT") {
|
||||||
validationResult.value = payload;
|
validationResult.value = payload;
|
||||||
} else if (type === 'ERROR') {
|
} else if (type === "ERROR") {
|
||||||
console.error('Worker Error:', payload);
|
console.error("Worker Error:", payload);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Init worker
|
// Init worker
|
||||||
worker.postMessage({ type: 'INIT' });
|
worker.postMessage({ type: "INIT" });
|
||||||
|
|
||||||
export function useCube() {
|
export function useCube() {
|
||||||
|
|
||||||
const initCube = () => {
|
const initCube = () => {
|
||||||
worker.postMessage({ type: 'RESET' });
|
worker.postMessage({ type: "RESET" });
|
||||||
};
|
};
|
||||||
|
|
||||||
const rotateLayer = (axis, index, direction) => {
|
const rotateLayer = (axis, index, direction, steps = 1) => {
|
||||||
worker.postMessage({ type: 'ROTATE_LAYER', payload: { axis, index, direction } });
|
worker.postMessage({
|
||||||
|
type: "ROTATE_LAYER",
|
||||||
|
payload: { axis, index, direction, steps },
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const turn = (move) => {
|
const turn = (move) => {
|
||||||
worker.postMessage({ type: 'TURN', payload: { move } });
|
worker.postMessage({ type: "TURN", payload: { move } });
|
||||||
};
|
};
|
||||||
|
|
||||||
const validate = () => {
|
const validate = () => {
|
||||||
worker.postMessage({ type: 'VALIDATE' });
|
worker.postMessage({ type: "VALIDATE" });
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -52,6 +56,6 @@ export function useCube() {
|
|||||||
turn,
|
turn,
|
||||||
validate,
|
validate,
|
||||||
COLORS,
|
COLORS,
|
||||||
FACES
|
FACES,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { ref } from 'vue';
|
import { ref } from "vue";
|
||||||
|
|
||||||
let initialCubeTranslucent = false;
|
let initialCubeTranslucent = false;
|
||||||
try {
|
try {
|
||||||
const stored = localStorage.getItem('cubeTranslucent');
|
const stored = localStorage.getItem("cubeTranslucent");
|
||||||
if (stored !== null) {
|
if (stored !== null) {
|
||||||
initialCubeTranslucent = stored === 'true';
|
initialCubeTranslucent = stored === "true";
|
||||||
}
|
}
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
|
|
||||||
@@ -14,12 +14,12 @@ export function useSettings() {
|
|||||||
const toggleCubeTranslucent = () => {
|
const toggleCubeTranslucent = () => {
|
||||||
isCubeTranslucent.value = !isCubeTranslucent.value;
|
isCubeTranslucent.value = !isCubeTranslucent.value;
|
||||||
try {
|
try {
|
||||||
localStorage.setItem('cubeTranslucent', String(isCubeTranslucent.value));
|
localStorage.setItem("cubeTranslucent", String(isCubeTranslucent.value));
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
isCubeTranslucent,
|
isCubeTranslucent,
|
||||||
toggleCubeTranslucent
|
toggleCubeTranslucent,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
export const LAYER_ANIMATION_DURATION = 200
|
export const LAYER_ANIMATION_DURATION = 200;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { createApp } from 'vue'
|
import { createApp } from "vue";
|
||||||
import './style.css'
|
import "./style.css";
|
||||||
import App from './App.vue'
|
import App from "./App.vue";
|
||||||
|
|
||||||
createApp(App).mount('#app')
|
createApp(App).mount("#app");
|
||||||
|
|||||||
59
src/utils/CubeLogicAdapter.js
Normal file
59
src/utils/CubeLogicAdapter.js
Normal 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)'] };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -12,22 +12,22 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
export const COLORS = {
|
export const COLORS = {
|
||||||
WHITE: 'white',
|
WHITE: "white",
|
||||||
YELLOW: 'yellow',
|
YELLOW: "yellow",
|
||||||
ORANGE: 'orange',
|
ORANGE: "orange",
|
||||||
RED: 'red',
|
RED: "red",
|
||||||
GREEN: 'green',
|
GREEN: "green",
|
||||||
BLUE: 'blue',
|
BLUE: "blue",
|
||||||
BLACK: 'black'
|
BLACK: "black",
|
||||||
};
|
};
|
||||||
|
|
||||||
export const FACES = {
|
export const FACES = {
|
||||||
UP: 'up',
|
UP: "up",
|
||||||
DOWN: 'down',
|
DOWN: "down",
|
||||||
LEFT: 'left',
|
LEFT: "left",
|
||||||
RIGHT: 'right',
|
RIGHT: "right",
|
||||||
FRONT: 'front',
|
FRONT: "front",
|
||||||
BACK: 'back',
|
BACK: "back",
|
||||||
};
|
};
|
||||||
|
|
||||||
// Standard Face Colors (Solved State)
|
// Standard Face Colors (Solved State)
|
||||||
@@ -99,7 +99,7 @@ export class CubeModel {
|
|||||||
*/
|
*/
|
||||||
rotateLayer(axis, index, direction) {
|
rotateLayer(axis, index, direction) {
|
||||||
// Determine the relevant cubies in the slice
|
// Determine the relevant cubies in the slice
|
||||||
const slice = this.cubies.filter(c => c[axis] === index);
|
const slice = this.cubies.filter((c) => c[axis] === index);
|
||||||
|
|
||||||
// Coordinate rotation (Matrix Logic)
|
// Coordinate rotation (Matrix Logic)
|
||||||
// 90 deg CW rotation formulas:
|
// 90 deg CW rotation formulas:
|
||||||
@@ -120,7 +120,7 @@ export class CubeModel {
|
|||||||
|
|
||||||
// If direction is -1: Inverse.
|
// If direction is -1: Inverse.
|
||||||
|
|
||||||
slice.forEach(cubie => {
|
slice.forEach((cubie) => {
|
||||||
this._rotateCubieCoordinates(cubie, axis, direction);
|
this._rotateCubieCoordinates(cubie, axis, direction);
|
||||||
this._rotateCubieFaces(cubie, axis, direction);
|
this._rotateCubieFaces(cubie, axis, direction);
|
||||||
});
|
});
|
||||||
@@ -129,7 +129,7 @@ export class CubeModel {
|
|||||||
_rotateCubieCoordinates(cubie, axis, direction) {
|
_rotateCubieCoordinates(cubie, axis, direction) {
|
||||||
const { x, y, z } = cubie;
|
const { x, y, z } = cubie;
|
||||||
|
|
||||||
if (axis === 'x') {
|
if (axis === "x") {
|
||||||
if (direction === 1) {
|
if (direction === 1) {
|
||||||
cubie.y = -z;
|
cubie.y = -z;
|
||||||
cubie.z = y;
|
cubie.z = y;
|
||||||
@@ -137,7 +137,7 @@ export class CubeModel {
|
|||||||
cubie.y = z;
|
cubie.y = z;
|
||||||
cubie.z = -y;
|
cubie.z = -y;
|
||||||
}
|
}
|
||||||
} else if (axis === 'y') {
|
} else if (axis === "y") {
|
||||||
if (direction === 1) {
|
if (direction === 1) {
|
||||||
cubie.z = -x;
|
cubie.z = -x;
|
||||||
cubie.x = z;
|
cubie.x = z;
|
||||||
@@ -145,11 +145,13 @@ export class CubeModel {
|
|||||||
cubie.z = x;
|
cubie.z = x;
|
||||||
cubie.x = -z;
|
cubie.x = -z;
|
||||||
}
|
}
|
||||||
} else if (axis === 'z') {
|
} else if (axis === "z") {
|
||||||
if (direction === 1) { // CW
|
if (direction === 1) {
|
||||||
|
// CW
|
||||||
cubie.x = -y;
|
cubie.x = -y;
|
||||||
cubie.y = x;
|
cubie.y = x;
|
||||||
} else { // CCW
|
} else {
|
||||||
|
// CCW
|
||||||
cubie.x = y;
|
cubie.x = y;
|
||||||
cubie.y = -x;
|
cubie.y = -x;
|
||||||
}
|
}
|
||||||
@@ -165,38 +167,44 @@ export class CubeModel {
|
|||||||
|
|
||||||
const f = { ...cubie.faces };
|
const f = { ...cubie.faces };
|
||||||
|
|
||||||
if (axis === 'x') {
|
if (axis === "x") {
|
||||||
if (direction === 1) { // Up -> Front -> Down -> Back -> Up
|
if (direction === 1) {
|
||||||
|
// Up -> Front -> Down -> Back -> Up
|
||||||
cubie.faces[FACES.FRONT] = f[FACES.UP];
|
cubie.faces[FACES.FRONT] = f[FACES.UP];
|
||||||
cubie.faces[FACES.DOWN] = f[FACES.FRONT];
|
cubie.faces[FACES.DOWN] = f[FACES.FRONT];
|
||||||
cubie.faces[FACES.BACK] = f[FACES.DOWN];
|
cubie.faces[FACES.BACK] = f[FACES.DOWN];
|
||||||
cubie.faces[FACES.UP] = f[FACES.BACK];
|
cubie.faces[FACES.UP] = f[FACES.BACK];
|
||||||
// Left/Right unchanged in position, but might rotate? No, faces are solid colors.
|
// Left/Right unchanged in position, but might rotate? No, faces are solid colors.
|
||||||
} else { // Up -> Back -> Down -> Front -> Up
|
} else {
|
||||||
|
// Up -> Back -> Down -> Front -> Up
|
||||||
cubie.faces[FACES.BACK] = f[FACES.UP];
|
cubie.faces[FACES.BACK] = f[FACES.UP];
|
||||||
cubie.faces[FACES.DOWN] = f[FACES.BACK];
|
cubie.faces[FACES.DOWN] = f[FACES.BACK];
|
||||||
cubie.faces[FACES.FRONT] = f[FACES.DOWN];
|
cubie.faces[FACES.FRONT] = f[FACES.DOWN];
|
||||||
cubie.faces[FACES.UP] = f[FACES.FRONT];
|
cubie.faces[FACES.UP] = f[FACES.FRONT];
|
||||||
}
|
}
|
||||||
} else if (axis === 'y') {
|
} else if (axis === "y") {
|
||||||
if (direction === 1) { // Front -> Right -> Back -> Left -> Front
|
if (direction === 1) {
|
||||||
|
// Front -> Right -> Back -> Left -> Front
|
||||||
cubie.faces[FACES.RIGHT] = f[FACES.FRONT];
|
cubie.faces[FACES.RIGHT] = f[FACES.FRONT];
|
||||||
cubie.faces[FACES.BACK] = f[FACES.RIGHT];
|
cubie.faces[FACES.BACK] = f[FACES.RIGHT];
|
||||||
cubie.faces[FACES.LEFT] = f[FACES.BACK];
|
cubie.faces[FACES.LEFT] = f[FACES.BACK];
|
||||||
cubie.faces[FACES.FRONT] = f[FACES.LEFT];
|
cubie.faces[FACES.FRONT] = f[FACES.LEFT];
|
||||||
} else { // Front -> Left -> Back -> Right -> Front
|
} else {
|
||||||
|
// Front -> Left -> Back -> Right -> Front
|
||||||
cubie.faces[FACES.LEFT] = f[FACES.FRONT];
|
cubie.faces[FACES.LEFT] = f[FACES.FRONT];
|
||||||
cubie.faces[FACES.BACK] = f[FACES.LEFT];
|
cubie.faces[FACES.BACK] = f[FACES.LEFT];
|
||||||
cubie.faces[FACES.RIGHT] = f[FACES.BACK];
|
cubie.faces[FACES.RIGHT] = f[FACES.BACK];
|
||||||
cubie.faces[FACES.FRONT] = f[FACES.RIGHT];
|
cubie.faces[FACES.FRONT] = f[FACES.RIGHT];
|
||||||
}
|
}
|
||||||
} else if (axis === 'z') {
|
} else if (axis === "z") {
|
||||||
if (direction === 1) { // CCW: Up -> Left -> Down -> Right -> Up
|
if (direction === 1) {
|
||||||
|
// CCW: Up -> Left -> Down -> Right -> Up
|
||||||
cubie.faces[FACES.LEFT] = f[FACES.UP];
|
cubie.faces[FACES.LEFT] = f[FACES.UP];
|
||||||
cubie.faces[FACES.DOWN] = f[FACES.LEFT];
|
cubie.faces[FACES.DOWN] = f[FACES.LEFT];
|
||||||
cubie.faces[FACES.RIGHT] = f[FACES.DOWN];
|
cubie.faces[FACES.RIGHT] = f[FACES.DOWN];
|
||||||
cubie.faces[FACES.UP] = f[FACES.RIGHT];
|
cubie.faces[FACES.UP] = f[FACES.RIGHT];
|
||||||
} else { // CW: Up -> Right -> Down -> Left -> Up
|
} else {
|
||||||
|
// CW: Up -> Right -> Down -> Left -> Up
|
||||||
cubie.faces[FACES.RIGHT] = f[FACES.UP];
|
cubie.faces[FACES.RIGHT] = f[FACES.UP];
|
||||||
cubie.faces[FACES.DOWN] = f[FACES.RIGHT];
|
cubie.faces[FACES.DOWN] = f[FACES.RIGHT];
|
||||||
cubie.faces[FACES.LEFT] = f[FACES.DOWN];
|
cubie.faces[FACES.LEFT] = f[FACES.DOWN];
|
||||||
@@ -208,12 +216,12 @@ export class CubeModel {
|
|||||||
toCubies() {
|
toCubies() {
|
||||||
// Return copy of state for rendering
|
// Return copy of state for rendering
|
||||||
// CubeCSS expects array of objects with x, y, z, faces
|
// CubeCSS expects array of objects with x, y, z, faces
|
||||||
return this.cubies.map(c => ({
|
return this.cubies.map((c) => ({
|
||||||
id: c.id,
|
id: c.id,
|
||||||
x: c.x,
|
x: c.x,
|
||||||
y: c.y,
|
y: c.y,
|
||||||
z: c.z,
|
z: c.z,
|
||||||
faces: { ...c.faces }
|
faces: { ...c.faces },
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -238,30 +246,54 @@ export class CubeModel {
|
|||||||
// B (CW around -Z): 1 (since Z(1) is CW around -Z)
|
// B (CW around -Z): 1 (since Z(1) is CW around -Z)
|
||||||
|
|
||||||
switch (base) {
|
switch (base) {
|
||||||
case 'U': direction = 1; break;
|
case "U":
|
||||||
case 'D': direction = -1; break;
|
direction = 1;
|
||||||
case 'L': direction = -1; break;
|
break;
|
||||||
case 'R': direction = 1; break;
|
case "D":
|
||||||
case 'F': direction = -1; break;
|
direction = -1;
|
||||||
case 'B': direction = 1; break;
|
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 === "'") direction *= -1;
|
||||||
if (modifier === '2') {
|
if (modifier === "2") {
|
||||||
// 2 moves. Direction doesn't matter for 180, but let's keep it.
|
// 2 moves. Direction doesn't matter for 180, but let's keep it.
|
||||||
// We will call rotateLayer twice.
|
// We will call rotateLayer twice.
|
||||||
}
|
}
|
||||||
|
|
||||||
const count = modifier === '2' ? 2 : 1;
|
const count = modifier === "2" ? 2 : 1;
|
||||||
|
|
||||||
for (let i = 0; i < count; i++) {
|
for (let i = 0; i < count; i++) {
|
||||||
switch (base) {
|
switch (base) {
|
||||||
case 'U': this.rotateLayer('y', 1, direction); break;
|
case "U":
|
||||||
case 'D': this.rotateLayer('y', -1, direction); break;
|
this.rotateLayer("y", 1, direction);
|
||||||
case 'L': this.rotateLayer('x', -1, direction); break;
|
break;
|
||||||
case 'R': this.rotateLayer('x', 1, direction); break;
|
case "D":
|
||||||
case 'F': this.rotateLayer('z', 1, direction); break;
|
this.rotateLayer("y", -1, direction);
|
||||||
case 'B': this.rotateLayer('z', -1, direction); break;
|
break;
|
||||||
|
case "L":
|
||||||
|
this.rotateLayer("x", -1, direction);
|
||||||
|
break;
|
||||||
|
case "R":
|
||||||
|
this.rotateLayer("x", 1, direction);
|
||||||
|
break;
|
||||||
|
case "F":
|
||||||
|
this.rotateLayer("z", 1, direction);
|
||||||
|
break;
|
||||||
|
case "B":
|
||||||
|
this.rotateLayer("z", -1, direction);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -282,25 +314,46 @@ export class CubeModel {
|
|||||||
for (let c = 0; c < 3; c++) {
|
for (let c = 0; c < 3; c++) {
|
||||||
let cubie;
|
let cubie;
|
||||||
// Map r,c to x,y,z based on face
|
// Map r,c to x,y,z based on face
|
||||||
if (face === FACES.UP) { // y=1. r=0->z=-1 (Back), r=2->z=1 (Front). c=0->x=-1 (Left).
|
if (face === FACES.UP) {
|
||||||
|
// y=1. r=0->z=-1 (Back), r=2->z=1 (Front). c=0->x=-1 (Left).
|
||||||
// Standard U face view: Top Left is Back Left (-1, 1, -1).
|
// Standard U face view: Top Left is Back Left (-1, 1, -1).
|
||||||
// Row 0 (Top of U face) is Back.
|
// Row 0 (Top of U face) is Back.
|
||||||
// Row 2 (Bottom of U face) is Front.
|
// Row 2 (Bottom of U face) is Front.
|
||||||
cubie = this.cubies.find(cu => cu.y === 1 && cu.x === (c - 1) && cu.z === (r - 1)); // Wait.
|
cubie = this.cubies.find(
|
||||||
|
(cu) => cu.y === 1 && cu.x === c - 1 && cu.z === r - 1,
|
||||||
|
); // Wait.
|
||||||
// Back is z=-1. Front is z=1.
|
// Back is z=-1. Front is z=1.
|
||||||
// Visual Top of U face is Back (z=-1).
|
// Visual Top of U face is Back (z=-1).
|
||||||
// Visual Bottom of U face is Front (z=1).
|
// Visual Bottom of U face is Front (z=1).
|
||||||
cubie = this.cubies.find(cu => cu.y === 1 && cu.x === (c - 1) && cu.z === (r - 1 - 2 * r)); // Complicated.
|
cubie = this.cubies.find(
|
||||||
|
(cu) => cu.y === 1 && cu.x === c - 1 && cu.z === r - 1 - 2 * r,
|
||||||
|
); // Complicated.
|
||||||
// Let's just find by strict coordinates
|
// Let's just find by strict coordinates
|
||||||
// r=0 -> z=-1. r=1 -> z=0. r=2 -> z=1.
|
// r=0 -> z=-1. r=1 -> z=0. r=2 -> z=1.
|
||||||
// c=0 -> x=-1. c=1 -> x=0. c=2 -> x=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));
|
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.DOWN)
|
||||||
else if (face === FACES.BACK) cubie = this.cubies.find(cu => cu.z === -1 && cu.x === (1 - c) && cu.y === (1 - r));
|
cubie = this.cubies.find(
|
||||||
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?
|
(cu) => cu.y === -1 && cu.x === c - 1 && cu.z === 1 - r,
|
||||||
else if (face === FACES.RIGHT) cubie = this.cubies.find(cu => cu.x === 1 && cu.z === (c - 1) && cu.y === (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) {
|
if (cubie) {
|
||||||
rowStr += cubie.faces[face][0].toUpperCase() + " ";
|
rowStr += cubie.faces[face][0].toUpperCase() + " ";
|
||||||
@@ -313,16 +366,16 @@ export class CubeModel {
|
|||||||
out += "\n";
|
out += "\n";
|
||||||
};
|
};
|
||||||
|
|
||||||
printFace(FACES.UP, 'U');
|
printFace(FACES.UP, "U");
|
||||||
printFace(FACES.DOWN, 'D');
|
printFace(FACES.DOWN, "D");
|
||||||
printFace(FACES.FRONT, 'F');
|
printFace(FACES.FRONT, "F");
|
||||||
printFace(FACES.BACK, 'B');
|
printFace(FACES.BACK, "B");
|
||||||
printFace(FACES.LEFT, 'L');
|
printFace(FACES.LEFT, "L");
|
||||||
printFace(FACES.RIGHT, 'R');
|
printFace(FACES.RIGHT, "R");
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
scramble(n = 20) {
|
scramble(n = 20) {
|
||||||
const axes = ['x', 'y', 'z'];
|
const axes = ["x", "y", "z"];
|
||||||
const indices = [-1, 1]; // Usually rotate outer layers for scramble
|
const indices = [-1, 1]; // Usually rotate outer layers for scramble
|
||||||
// Actually, scrambling usually involves random face moves (U, D, L, R, F, B)
|
// Actually, scrambling usually involves random face moves (U, D, L, R, F, B)
|
||||||
// U: y=1, dir -1 (Standard CW)
|
// U: y=1, dir -1 (Standard CW)
|
||||||
|
|||||||
398
src/utils/DeepCube.js
Normal file
398
src/utils/DeepCube.js
Normal 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;
|
||||||
|
});
|
||||||
@@ -1,172 +0,0 @@
|
|||||||
import { State } from 'rubiks-js/src/state/index.js';
|
|
||||||
import { CubeModel } from './CubeModel.js';
|
|
||||||
|
|
||||||
// Static order definitions from rubiks-js source
|
|
||||||
const CORNER_ORDER = ['URF', 'ULF', 'ULB', 'URB', 'DRF', 'DLF', 'DLB', 'DRB'];
|
|
||||||
const EDGE_ORDER = ['UF', 'UL', 'UB', 'UR', 'FR', 'FL', 'BL', 'BR', 'DF', 'DL', 'DB', 'DR'];
|
|
||||||
|
|
||||||
// Coordinate mapping for visualization
|
|
||||||
// Coordinates match the visual grid positions
|
|
||||||
const CORNER_SLOTS = [
|
|
||||||
{ id: 'URF', x: 1, y: 1, z: 1 },
|
|
||||||
{ id: 'ULF', x: -1, y: 1, z: 1 },
|
|
||||||
{ id: 'ULB', x: -1, y: 1, z: -1 },
|
|
||||||
{ id: 'URB', x: 1, y: 1, z: -1 },
|
|
||||||
{ id: 'DRF', x: 1, y: -1, z: 1 },
|
|
||||||
{ id: 'DLF', x: -1, y: -1, z: 1 },
|
|
||||||
{ id: 'DLB', x: -1, y: -1, z: -1 },
|
|
||||||
{ id: 'DRB', x: 1, y: -1, z: -1 }
|
|
||||||
];
|
|
||||||
|
|
||||||
const EDGE_SLOTS = [
|
|
||||||
{ id: 'UF', x: 0, y: 1, z: 1 },
|
|
||||||
{ id: 'UL', x: -1, y: 1, z: 0 },
|
|
||||||
{ id: 'UB', x: 0, y: 1, z: -1 },
|
|
||||||
{ id: 'UR', x: 1, y: 1, z: 0 },
|
|
||||||
{ id: 'FR', x: 1, y: 0, z: 1 },
|
|
||||||
{ id: 'FL', x: -1, y: 0, z: 1 },
|
|
||||||
{ id: 'BL', x: -1, y: 0, z: -1 },
|
|
||||||
{ id: 'BR', x: 1, y: 0, z: -1 },
|
|
||||||
{ id: 'DF', x: 0, y: -1, z: 1 },
|
|
||||||
{ id: 'DL', x: -1, y: -1, z: 0 },
|
|
||||||
{ id: 'DB', x: 0, y: -1, z: -1 },
|
|
||||||
{ id: 'DR', x: 1, y: -1, z: 0 }
|
|
||||||
];
|
|
||||||
|
|
||||||
const CENTERS = [
|
|
||||||
{ id: 'c0', x: 0, y: 1, z: 0, faces: { up: 'white' } },
|
|
||||||
{ id: 'c1', x: 0, y: -1, z: 0, faces: { down: 'yellow' } },
|
|
||||||
{ id: 'c2', x: 0, y: 0, z: 1, faces: { front: 'green' } },
|
|
||||||
{ id: 'c3', x: 0, y: 0, z: -1, faces: { back: 'blue' } },
|
|
||||||
{ id: 'c4', x: -1, y: 0, z: 0, faces: { left: 'orange' } },
|
|
||||||
{ id: 'c5', x: 1, y: 0, z: 0, faces: { right: 'red' } },
|
|
||||||
{ id: 'core', x: 0, y: 0, z: 0, faces: {} }
|
|
||||||
];
|
|
||||||
|
|
||||||
// Face mapping for pieces
|
|
||||||
// Each piece (e.g. URF) has 3 faces. We need to map them to colors based on orientation.
|
|
||||||
// Standard color scheme: U=white, D=yellow, F=green, B=blue, L=orange, R=red
|
|
||||||
const FACE_COLORS = {
|
|
||||||
U: 'white', D: 'yellow', F: 'green', B: 'blue', L: 'orange', R: 'red'
|
|
||||||
};
|
|
||||||
|
|
||||||
// Map piece name (e.g. 'URF') to its primary face keys
|
|
||||||
const CORNER_FACES = {
|
|
||||||
'URF': ['up', 'right', 'front'],
|
|
||||||
'ULF': ['up', 'front', 'left'],
|
|
||||||
'ULB': ['up', 'left', 'back'],
|
|
||||||
'URB': ['up', 'back', 'right'],
|
|
||||||
'DRF': ['down', 'right', 'front'],
|
|
||||||
'DLF': ['down', 'left', 'front'],
|
|
||||||
'DLB': ['down', 'back', 'left'],
|
|
||||||
'DRB': ['down', 'right', 'back']
|
|
||||||
};
|
|
||||||
|
|
||||||
const EDGE_FACES = {
|
|
||||||
'UF': ['up', 'front'],
|
|
||||||
'UL': ['up', 'left'],
|
|
||||||
'UB': ['up', 'back'],
|
|
||||||
'UR': ['up', 'right'],
|
|
||||||
'FR': ['front', 'right'],
|
|
||||||
'FL': ['front', 'left'],
|
|
||||||
'BL': ['back', 'left'],
|
|
||||||
'BR': ['back', 'right'],
|
|
||||||
'DF': ['down', 'front'],
|
|
||||||
'DL': ['down', 'left'],
|
|
||||||
'DB': ['down', 'back'],
|
|
||||||
'DR': ['down', 'right']
|
|
||||||
};
|
|
||||||
|
|
||||||
// Map piece name to its solved colors
|
|
||||||
const getCornerColors = (name) => {
|
|
||||||
// URF -> white, red, green
|
|
||||||
const map = {
|
|
||||||
'URF': ['white', 'red', 'green'],
|
|
||||||
'ULF': ['white', 'green', 'orange'],
|
|
||||||
'ULB': ['white', 'orange', 'blue'],
|
|
||||||
'URB': ['white', 'blue', 'red'],
|
|
||||||
'DRF': ['yellow', 'red', 'green'],
|
|
||||||
'DLF': ['yellow', 'orange', 'green'], // Adjusted to match DLF face order (D, L, F)
|
|
||||||
'DLB': ['yellow', 'blue', 'orange'], // Adjusted to match DLB face order (D, B, L)
|
|
||||||
'DRB': ['yellow', 'red', 'blue'] // Adjusted to match DRB face order (D, R, B)
|
|
||||||
};
|
|
||||||
return map[name];
|
|
||||||
};
|
|
||||||
|
|
||||||
const getEdgeColors = (name) => {
|
|
||||||
const map = {
|
|
||||||
'UF': ['white', 'green'],
|
|
||||||
'UL': ['white', 'orange'],
|
|
||||||
'UB': ['white', 'blue'],
|
|
||||||
'UR': ['white', 'red'],
|
|
||||||
'FR': ['green', 'red'],
|
|
||||||
'FL': ['green', 'orange'],
|
|
||||||
'BL': ['blue', 'orange'],
|
|
||||||
'BR': ['blue', 'red'],
|
|
||||||
'DF': ['yellow', 'green'],
|
|
||||||
'DL': ['yellow', 'orange'],
|
|
||||||
'DB': ['yellow', 'blue'],
|
|
||||||
'DR': ['yellow', 'red']
|
|
||||||
};
|
|
||||||
return map[name];
|
|
||||||
};
|
|
||||||
|
|
||||||
export class RubiksJSModel {
|
|
||||||
constructor() {
|
|
||||||
this.state = new State(false); // trackCenters=false
|
|
||||||
this.visual = new CubeModel();
|
|
||||||
}
|
|
||||||
|
|
||||||
reset() {
|
|
||||||
this.state = new State(false);
|
|
||||||
this.visual = new CubeModel();
|
|
||||||
}
|
|
||||||
|
|
||||||
rotateLayer(axis, index, dir) {
|
|
||||||
let move = '';
|
|
||||||
if (axis === 'y') {
|
|
||||||
if (index === 1) move = dir === 1 ? "U'" : "U";
|
|
||||||
else if (index === -1) move = dir === 1 ? "D'" : "D";
|
|
||||||
}
|
|
||||||
else if (axis === 'x') {
|
|
||||||
if (index === 1) move = dir === 1 ? "R'" : "R";
|
|
||||||
else if (index === -1) move = dir === 1 ? "L'" : "L";
|
|
||||||
}
|
|
||||||
else if (axis === 'z') {
|
|
||||||
if (index === 1) move = dir === 1 ? "F'" : "F";
|
|
||||||
else if (index === -1) move = dir === 1 ? "B'" : "B";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (move) {
|
|
||||||
console.log('[RubiksJSModel] Applying move:', move);
|
|
||||||
try {
|
|
||||||
this.state.applyTurn(move);
|
|
||||||
console.log('[RubiksJSModel] Move applied successfully');
|
|
||||||
} catch (e) {
|
|
||||||
console.error('[RubiksJSModel] Failed to apply move:', move, e);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.visual.rotateLayer(axis, index, dir);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
applyTurn(move) {
|
|
||||||
if (!move) return;
|
|
||||||
try {
|
|
||||||
this.state.applyTurn(move);
|
|
||||||
} catch (e) {
|
|
||||||
console.error('[RubiksJSModel] Failed to apply direct move:', move, e);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.visual.applyMove(move);
|
|
||||||
}
|
|
||||||
|
|
||||||
toCubies() {
|
|
||||||
return this.visual.toCubies();
|
|
||||||
}
|
|
||||||
|
|
||||||
validate() {
|
|
||||||
// State doesn't expose validate, but we can assume it's valid if using the library
|
|
||||||
return { valid: true, errors: [] };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
376
src/utils/solvers/BeginnerSolver.js
Normal file
376
src/utils/solvers/BeginnerSolver.js
Normal file
@@ -0,0 +1,376 @@
|
|||||||
|
import { DeepCube, MOVES } from "../DeepCube.js";
|
||||||
|
|
||||||
|
export class BeginnerSolver {
|
||||||
|
constructor(cube) {
|
||||||
|
this.cube = cube.clone();
|
||||||
|
this.solution = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
apply(moveStr) {
|
||||||
|
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 = [];
|
||||||
|
|
||||||
|
// Safety check - is it already solved?
|
||||||
|
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();
|
||||||
|
|
||||||
|
// Optional: align U face
|
||||||
|
this.alignUFace();
|
||||||
|
|
||||||
|
// Collapse redundant moves like U U -> U2, R R' -> nothing
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generalized Depth-Limited Search for localized piece-by-piece goals
|
||||||
|
solveGoal(condition, maxDepth, allowedMoves = Object.keys(MOVES)) {
|
||||||
|
if (condition(this.cube)) return true;
|
||||||
|
|
||||||
|
const opposing = { U: "D", D: "U", L: "R", R: "L", F: "B", B: "F" };
|
||||||
|
|
||||||
|
const dfs = (node, depth, lastMove) => {
|
||||||
|
if (depth === 0) return condition(node) ? [] : null;
|
||||||
|
const lastFace = lastMove ? lastMove[0] : null;
|
||||||
|
|
||||||
|
for (let m of allowedMoves) {
|
||||||
|
const face = m[0];
|
||||||
|
if (face === lastFace) continue;
|
||||||
|
if (opposing[face] === lastFace && face > lastFace) continue;
|
||||||
|
|
||||||
|
const nextNode = node.multiply(MOVES[m]);
|
||||||
|
if (depth === 1) {
|
||||||
|
if (condition(nextNode)) return [m];
|
||||||
|
} else {
|
||||||
|
const path = dfs(nextNode, depth - 1, m);
|
||||||
|
if (path) return [m, ...path];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
for (let d = 1; d <= maxDepth; d++) {
|
||||||
|
const res = dfs(this.cube, d, "");
|
||||||
|
if (res) {
|
||||||
|
res.forEach((m) => this.apply(m));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
solveCross() {
|
||||||
|
// White Cross on D face
|
||||||
|
// Edge Pieces: DF(5), DR(4), DB(7), DL(6)
|
||||||
|
const targets = [5, 4, 7, 6];
|
||||||
|
for (let i = 0; i < targets.length; i++) {
|
||||||
|
const goal = (state) => {
|
||||||
|
for (let j = 0; j <= i; j++) {
|
||||||
|
const t = targets[j];
|
||||||
|
if (state.ep[t] !== t || state.eo[t] !== 0) return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
if (!this.solveGoal(goal, 7)) throw new Error("Failed Cross depth 7");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
solveF2LCorners() {
|
||||||
|
// DFR(4), DRB(7), DBL(6), DLF(5)
|
||||||
|
const corners = [4, 7, 6, 5];
|
||||||
|
for (let i = 0; i < corners.length; i++) {
|
||||||
|
const goal = (state) => {
|
||||||
|
// Must preserve cross
|
||||||
|
if (state.ep[5] !== 5 || state.eo[5] !== 0) return false;
|
||||||
|
if (state.ep[4] !== 4 || state.eo[4] !== 0) return false;
|
||||||
|
if (state.ep[7] !== 7 || state.eo[7] !== 0) return false;
|
||||||
|
if (state.ep[6] !== 6 || state.eo[6] !== 0) return false;
|
||||||
|
|
||||||
|
// Must preserve prior corners
|
||||||
|
for (let j = 0; j <= i; j++) {
|
||||||
|
const c = corners[j];
|
||||||
|
if (state.cp[c] !== c || state.co[c] !== 0) return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
if (!this.solveGoal(goal, 8))
|
||||||
|
throw new Error("Failed F2L Corners depth 8");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
solveF2LEdges() {
|
||||||
|
// FR(8), BR(11), BL(10), FL(9)
|
||||||
|
const edges = [8, 11, 10, 9];
|
||||||
|
const allowed = [
|
||||||
|
"U",
|
||||||
|
"U'",
|
||||||
|
"U2",
|
||||||
|
"R",
|
||||||
|
"R'",
|
||||||
|
"R2",
|
||||||
|
"F",
|
||||||
|
"F'",
|
||||||
|
"F2",
|
||||||
|
"L",
|
||||||
|
"L'",
|
||||||
|
"L2",
|
||||||
|
"B",
|
||||||
|
"B'",
|
||||||
|
"B2",
|
||||||
|
]; // Avoid D moves
|
||||||
|
|
||||||
|
for (let i = 0; i < edges.length; i++) {
|
||||||
|
const goal = (state) => {
|
||||||
|
// Preserve cross
|
||||||
|
if (state.ep[5] !== 5 || state.eo[5] !== 0) return false;
|
||||||
|
if (state.ep[4] !== 4 || state.eo[4] !== 0) return false;
|
||||||
|
if (state.ep[7] !== 7 || state.eo[7] !== 0) return false;
|
||||||
|
if (state.ep[6] !== 6 || state.eo[6] !== 0) return false;
|
||||||
|
// Preserve all D corners
|
||||||
|
if (state.cp[4] !== 4 || state.co[4] !== 0) return false;
|
||||||
|
if (state.cp[7] !== 7 || state.co[7] !== 0) return false;
|
||||||
|
if (state.cp[6] !== 6 || state.co[6] !== 0) return false;
|
||||||
|
if (state.cp[5] !== 5 || state.co[5] !== 0) return false;
|
||||||
|
|
||||||
|
// Preserve prior edges
|
||||||
|
for (let j = 0; j <= i; j++) {
|
||||||
|
const e = edges[j];
|
||||||
|
if (state.ep[e] !== e || state.eo[e] !== 0) return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
// Depth 10 is needed for inserting edge from bad positions
|
||||||
|
if (!this.solveGoal(goal, 10, allowed))
|
||||||
|
throw new Error("Failed F2L Edges depth 10");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- BEGIN CFOP LAST LAYER MACROS ---
|
||||||
|
solveYellowCross() {
|
||||||
|
// Find yellow cross edges: UR(0), UF(1), UL(2), UB(3)
|
||||||
|
// They just need orientation eo=0.
|
||||||
|
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) {
|
||||||
|
// Line (opposite) or L-shape (adjacent)
|
||||||
|
const [a, b] = oriented;
|
||||||
|
if (Math.abs(a - b) === 2) {
|
||||||
|
// Line geometry (UR and UL, or UF and UB)
|
||||||
|
// To apply F R U R' U' F', the line must be horizontal.
|
||||||
|
// If line is UR(0) and UL(2), it's horizontal from F perspective.
|
||||||
|
if (a === 0 && b === 2) {
|
||||||
|
this.apply("F R U R' U' F'");
|
||||||
|
} else {
|
||||||
|
this.apply("U"); // turn line so it is UR/UL
|
||||||
|
this.apply("F R U R' U' F'");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// L-shape geometry
|
||||||
|
// Macro: F U R U' R' F' requires L to be at Back and Left (UB=3, UL=2)
|
||||||
|
if (oriented.includes(3) && oriented.includes(2)) {
|
||||||
|
this.apply("F U R U' R' F'");
|
||||||
|
} else {
|
||||||
|
this.apply("U");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
orientYellowCorners() {
|
||||||
|
// Sune / Antisune to orient yellow corners (co=0)
|
||||||
|
// OLL for corners alone using the repetitive beginner algorithm (R' D' R D)
|
||||||
|
// Position the unsolved corner at URF (0) and repeat R' D' R D until co[0] === 0
|
||||||
|
|
||||||
|
let safetyCount = 0;
|
||||||
|
while (safetyCount++ < 20) {
|
||||||
|
let solvedCount = [0, 1, 2, 3].filter(
|
||||||
|
(i) => this.cube.co[i] === 0,
|
||||||
|
).length;
|
||||||
|
if (solvedCount === 4) break;
|
||||||
|
|
||||||
|
if (this.cube.co[0] === 0) {
|
||||||
|
this.apply("U"); // Next
|
||||||
|
} else {
|
||||||
|
// Apply R' D' R D twice (1 cycle)
|
||||||
|
this.apply("R' D' R D R' D' R D");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
permuteYellowCorners() {
|
||||||
|
// Beginner method: look for "headlights" (two corners on the same face with the same color targeting that face).
|
||||||
|
// Mathematically: two adjacent U corners whose permutation matches their distance.
|
||||||
|
// E.g., URF(0) and UBR(3). If their permuted values are also adjacent, they are headlights.
|
||||||
|
|
||||||
|
const hasHeadlightsOnBack = () => {
|
||||||
|
// Back corners are ULB(2) and UBR(3)
|
||||||
|
// To be headlights on the back, they must belong to the same face in their solved state.
|
||||||
|
// Wait, comparing corner colors mathematically:
|
||||||
|
// In a solved cube, the U face has 4 corners: 0, 1, 2, 3.
|
||||||
|
// The distance between cp[2] and cp[3] modulo 4 should be exactly 1 or 3 (adjacent).
|
||||||
|
// Actually, if they are correctly relative to EACH OTHER:
|
||||||
|
return (
|
||||||
|
(this.cube.cp[2] - this.cube.cp[3] + 4) % 4 === 3 ||
|
||||||
|
(this.cube.cp[2] - this.cube.cp[3] + 4) % 4 === 1
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Simplified: Just keep applying the A-perm (headlight creator) until all 4 corners are relatively solved.
|
||||||
|
let safetyCount = 0;
|
||||||
|
while (safetyCount++ < 10) {
|
||||||
|
// Check if corners are relatively solved (i.e. 0->1->2->3 in order)
|
||||||
|
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; // All corners are cyclically ordered
|
||||||
|
}
|
||||||
|
|
||||||
|
// If Back corners are cyclically ordered (Headlights on Back)
|
||||||
|
if ((c2 - c3 + 4) % 4 === 1 || (c3 - c2 + 4) % 4 === 3) {
|
||||||
|
// Wait, order must be 3->0->1->2. So ULB(2) should be next after UBR(3).
|
||||||
|
// If cp[2] comes immediately after cp[3] cyclically:
|
||||||
|
if ((this.cube.cp[2] - this.cube.cp[3] + 4) % 4 === 1) {
|
||||||
|
this.apply("R' F R' B2 R F' R' B2 R2");
|
||||||
|
} else {
|
||||||
|
this.apply("U");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Find ANY headlights and put them on the back
|
||||||
|
if ((c1 - c2 + 4) % 4 === 1) {
|
||||||
|
this.apply("U");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if ((c0 - c1 + 4) % 4 === 1) {
|
||||||
|
this.apply("U2");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if ((c3 - c0 + 4) % 4 === 1) {
|
||||||
|
this.apply("U'");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// No headlights at all (diagonal swap), just apply the alg anywhere
|
||||||
|
this.apply("R' F R' B2 R F' R' B2 R2");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
permuteYellowEdges() {
|
||||||
|
// Corners are properly ordered now. We just need to align them to the centers first.
|
||||||
|
while (this.cube.cp[0] !== 0) {
|
||||||
|
this.apply("U");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now corners are 100% solved. Check edges.
|
||||||
|
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; // fully solved!
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if one edge is solved. If so, put it at the BACK (UB=3)
|
||||||
|
if (this.cube.ep[3] === 3) {
|
||||||
|
// Apply U-perm
|
||||||
|
this.apply("R U' R U R U R U' R' U' R2");
|
||||||
|
} else if (this.cube.ep[0] === 0) {
|
||||||
|
this.apply("U"); // turn whole cube or just U... wait, standard alg rotates U.
|
||||||
|
// To keep corners solved but move edges, we'd have to do y rotations.
|
||||||
|
// Let's just use the U-perm and see if it solves.
|
||||||
|
// If UR(0) is solved, we want it at UB(3). So we do a U move, apply alg, do U'.
|
||||||
|
// Wait, applying U moves the corners! We can't do that.
|
||||||
|
// Standard solving often uses `y` cube rotations. Since our model only supports face moves,
|
||||||
|
// we can use the transposed algorithms.
|
||||||
|
// Alg for solved Right(0): F U' F U F U F U' F' U' F2
|
||||||
|
this.apply("F U' F U F U F U' F' U' F2");
|
||||||
|
} else if (this.cube.ep[1] === 1) {
|
||||||
|
this.apply("L U' L U L U L U' L' U' L2");
|
||||||
|
} else if (this.cube.ep[2] === 2) {
|
||||||
|
this.apply("B U' B U B U B U' B' U' B2");
|
||||||
|
} else {
|
||||||
|
// No edges solved. Apply U-perm from anywhere.
|
||||||
|
this.apply("R U' R U R U R U' R' U' R2");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
alignUFace() {
|
||||||
|
while (this.cube.cp[0] !== 0) {
|
||||||
|
this.apply("U");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
optimizeSolution() {
|
||||||
|
// A quick pass to cancel out redundant moves like U U' -> nothing, U U -> U2, U2 U -> U'
|
||||||
|
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]) {
|
||||||
|
// Same face! Let's sum the rotation.
|
||||||
|
const val = (m) => (m.includes("'") ? -1 : m.includes("2") ? 2 : 1);
|
||||||
|
let sum = (val(a) + val(b)) % 4;
|
||||||
|
if (sum < 0) sum += 4; // normalize to positive
|
||||||
|
|
||||||
|
this.solution.splice(i, 2); // remove both
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
139
src/utils/solvers/KociembaSolver.js
Normal file
139
src/utils/solvers/KociembaSolver.js
Normal 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}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,21 +1,22 @@
|
|||||||
import { RubiksJSModel } from '../utils/RubiksJSModel.js';
|
import { RubiksJSModel } from "../utils/CubeLogicAdapter.js";
|
||||||
|
|
||||||
const cube = new RubiksJSModel();
|
const cube = new RubiksJSModel();
|
||||||
|
|
||||||
|
|
||||||
// Helper to send state update
|
// Helper to send state update
|
||||||
const sendUpdate = () => {
|
const sendUpdate = () => {
|
||||||
try {
|
try {
|
||||||
const cubies = cube.toCubies();
|
const cubies = cube.toCubies();
|
||||||
// console.log('[Worker] Sending update with cubies:', cubies.length);
|
// console.log('[Worker] Sending update with cubies:', cubies.length);
|
||||||
postMessage({
|
postMessage({
|
||||||
type: 'STATE_UPDATE',
|
type: "STATE_UPDATE",
|
||||||
payload: {
|
payload: {
|
||||||
cubies
|
cubies,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('[Worker] Error generating cubies:', e);
|
console.error("[Worker] Error generating cubies:", e);
|
||||||
postMessage({ type: 'ERROR', payload: e.message });
|
postMessage({ type: "ERROR", payload: e.message });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -23,31 +24,31 @@ self.onmessage = (e) => {
|
|||||||
const { type, payload } = e.data;
|
const { type, payload } = e.data;
|
||||||
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'INIT':
|
case "INIT":
|
||||||
case 'RESET':
|
case "RESET":
|
||||||
cube.reset();
|
cube.reset();
|
||||||
sendUpdate();
|
sendUpdate();
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'ROTATE_LAYER': {
|
case "ROTATE_LAYER": {
|
||||||
const { axis, index, direction } = payload;
|
const { axis, index, direction, steps = 1 } = payload;
|
||||||
cube.rotateLayer(axis, index, direction);
|
cube.rotateLayer(axis, index, direction, steps);
|
||||||
sendUpdate();
|
sendUpdate();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'TURN': {
|
case "TURN": {
|
||||||
const { move } = payload;
|
const { move } = payload;
|
||||||
cube.applyTurn(move);
|
cube.applyTurn(move);
|
||||||
sendUpdate();
|
sendUpdate();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'VALIDATE':
|
case "VALIDATE":
|
||||||
const validation = cube.validate();
|
const validation = cube.validate();
|
||||||
postMessage({
|
postMessage({
|
||||||
type: 'VALIDATION_RESULT',
|
type: "VALIDATION_RESULT",
|
||||||
payload: { valid: validation.valid, errors: validation.errors }
|
payload: { valid: validation.valid, errors: validation.errors },
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
|
import { Cube, FACES, COLORS } from "../src/utils/Cube.js";
|
||||||
|
import assert from "assert";
|
||||||
|
|
||||||
import { Cube, FACES, COLORS } from '../src/utils/Cube.js';
|
console.log("Running Cube Integrity Tests...");
|
||||||
import assert from 'assert';
|
|
||||||
|
|
||||||
console.log('Running Cube Integrity Tests...');
|
|
||||||
|
|
||||||
const cube = new Cube();
|
const cube = new Cube();
|
||||||
|
|
||||||
@@ -15,11 +14,11 @@ const countColors = () => {
|
|||||||
[COLORS.RED]: 0,
|
[COLORS.RED]: 0,
|
||||||
[COLORS.GREEN]: 0,
|
[COLORS.GREEN]: 0,
|
||||||
[COLORS.BLUE]: 0,
|
[COLORS.BLUE]: 0,
|
||||||
[COLORS.BLACK]: 0 // Should be ignored or internal
|
[COLORS.BLACK]: 0, // Should be ignored or internal
|
||||||
};
|
};
|
||||||
|
|
||||||
cube.cubies.forEach(cubie => {
|
cube.cubies.forEach((cubie) => {
|
||||||
Object.values(cubie.faces).forEach(color => {
|
Object.values(cubie.faces).forEach((color) => {
|
||||||
if (counts[color] !== undefined) {
|
if (counts[color] !== undefined) {
|
||||||
counts[color]++;
|
counts[color]++;
|
||||||
}
|
}
|
||||||
@@ -36,12 +35,12 @@ const verifyCounts = (counts) => {
|
|||||||
// 27 cubies * 6 faces = 162 total faces.
|
// 27 cubies * 6 faces = 162 total faces.
|
||||||
// 162 - 54 = 108 black faces (internal).
|
// 162 - 54 = 108 black faces (internal).
|
||||||
|
|
||||||
assert.strictEqual(counts[COLORS.WHITE], 9, 'White count should be 9');
|
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.YELLOW], 9, "Yellow count should be 9");
|
||||||
assert.strictEqual(counts[COLORS.ORANGE], 9, 'Orange 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.RED], 9, "Red count should be 9");
|
||||||
assert.strictEqual(counts[COLORS.GREEN], 9, 'Green 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');
|
assert.strictEqual(counts[COLORS.BLUE], 9, "Blue count should be 9");
|
||||||
};
|
};
|
||||||
|
|
||||||
// Helper: Verify piece integrity
|
// Helper: Verify piece integrity
|
||||||
@@ -55,19 +54,24 @@ const verifyPieceTypes = () => {
|
|||||||
let centers = 0;
|
let centers = 0;
|
||||||
let cores = 0;
|
let cores = 0;
|
||||||
|
|
||||||
cube.cubies.forEach(cubie => {
|
cube.cubies.forEach((cubie) => {
|
||||||
const coloredFaces = Object.values(cubie.faces).filter(c => c !== COLORS.BLACK).length;
|
const coloredFaces = Object.values(cubie.faces).filter(
|
||||||
|
(c) => c !== COLORS.BLACK,
|
||||||
|
).length;
|
||||||
if (coloredFaces === 3) corners++;
|
if (coloredFaces === 3) corners++;
|
||||||
else if (coloredFaces === 2) edges++;
|
else if (coloredFaces === 2) edges++;
|
||||||
else if (coloredFaces === 1) centers++;
|
else if (coloredFaces === 1) centers++;
|
||||||
else if (coloredFaces === 0) cores++;
|
else if (coloredFaces === 0) cores++;
|
||||||
else assert.fail(`Invalid cubie with ${coloredFaces} colors at (${cubie.x},${cubie.y},${cubie.z})`);
|
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(corners, 8, "Should have 8 corners");
|
||||||
assert.strictEqual(edges, 12, 'Should have 12 edges');
|
assert.strictEqual(edges, 12, "Should have 12 edges");
|
||||||
assert.strictEqual(centers, 6, 'Should have 6 centers');
|
assert.strictEqual(centers, 6, "Should have 6 centers");
|
||||||
assert.strictEqual(cores, 1, 'Should have 1 core');
|
assert.strictEqual(cores, 1, "Should have 1 core");
|
||||||
};
|
};
|
||||||
|
|
||||||
// Helper: Verify specific relative positions of centers (they never change relative to each other)
|
// Helper: Verify specific relative positions of centers (they never change relative to each other)
|
||||||
@@ -75,12 +79,14 @@ const verifyPieceTypes = () => {
|
|||||||
// Front (Green) opposite Back (Blue)
|
// Front (Green) opposite Back (Blue)
|
||||||
// Left (Orange) opposite Right (Red)
|
// Left (Orange) opposite Right (Red)
|
||||||
const verifyCenters = () => {
|
const verifyCenters = () => {
|
||||||
const centers = cube.cubies.filter(c =>
|
const centers = cube.cubies.filter(
|
||||||
Object.values(c.faces).filter(f => f !== COLORS.BLACK).length === 1
|
(c) =>
|
||||||
|
Object.values(c.faces).filter((f) => f !== COLORS.BLACK).length === 1,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Find center by color
|
// Find center by color
|
||||||
const findCenter = (color) => centers.find(c => Object.values(c.faces).includes(color));
|
const findCenter = (color) =>
|
||||||
|
centers.find((c) => Object.values(c.faces).includes(color));
|
||||||
|
|
||||||
const white = findCenter(COLORS.WHITE);
|
const white = findCenter(COLORS.WHITE);
|
||||||
const yellow = findCenter(COLORS.YELLOW);
|
const yellow = findCenter(COLORS.YELLOW);
|
||||||
@@ -105,31 +111,30 @@ const verifyCenters = () => {
|
|||||||
assert.strictEqual(c1.z + c2.z, 0, `${name} Z mismatch`);
|
assert.strictEqual(c1.z + c2.z, 0, `${name} Z mismatch`);
|
||||||
};
|
};
|
||||||
|
|
||||||
checkOpposite(white, yellow, 'White-Yellow');
|
checkOpposite(white, yellow, "White-Yellow");
|
||||||
checkOpposite(green, blue, 'Green-Blue');
|
checkOpposite(green, blue, "Green-Blue");
|
||||||
checkOpposite(orange, red, 'Orange-Red');
|
checkOpposite(orange, red, "Orange-Red");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
// --- Test Execution ---
|
// --- Test Execution ---
|
||||||
|
|
||||||
// 1. Initial State
|
// 1. Initial State
|
||||||
console.log('Test 1: Initial State Integrity');
|
console.log("Test 1: Initial State Integrity");
|
||||||
verifyCounts(countColors());
|
verifyCounts(countColors());
|
||||||
verifyPieceTypes();
|
verifyPieceTypes();
|
||||||
verifyCenters();
|
verifyCenters();
|
||||||
console.log('PASS Initial State');
|
console.log("PASS Initial State");
|
||||||
|
|
||||||
// 2. Single Rotation (R)
|
// 2. Single Rotation (R)
|
||||||
console.log('Test 2: Single Rotation (R)');
|
console.log("Test 2: Single Rotation (R)");
|
||||||
cube.rotateLayer('x', 1, -1); // R
|
cube.rotateLayer("x", 1, -1); // R
|
||||||
verifyCounts(countColors());
|
verifyCounts(countColors());
|
||||||
verifyPieceTypes();
|
verifyPieceTypes();
|
||||||
verifyCenters();
|
verifyCenters();
|
||||||
console.log('PASS Single Rotation');
|
console.log("PASS Single Rotation");
|
||||||
|
|
||||||
// 3. Multiple Rotations (R U R' U')
|
// 3. Multiple Rotations (R U R' U')
|
||||||
console.log('Test 3: Sexy Move (R U R\' U\')');
|
console.log("Test 3: Sexy Move (R U R' U')");
|
||||||
cube.reset();
|
cube.reset();
|
||||||
cube.move("R");
|
cube.move("R");
|
||||||
cube.move("U");
|
cube.move("U");
|
||||||
@@ -138,12 +143,12 @@ cube.move("U'");
|
|||||||
verifyCounts(countColors());
|
verifyCounts(countColors());
|
||||||
verifyPieceTypes();
|
verifyPieceTypes();
|
||||||
verifyCenters();
|
verifyCenters();
|
||||||
console.log('PASS Sexy Move');
|
console.log("PASS Sexy Move");
|
||||||
|
|
||||||
// 4. Random Rotations (Fuzzing)
|
// 4. Random Rotations (Fuzzing)
|
||||||
console.log('Test 4: 100 Random Moves');
|
console.log("Test 4: 100 Random Moves");
|
||||||
cube.reset();
|
cube.reset();
|
||||||
const axes = ['x', 'y', 'z'];
|
const axes = ["x", "y", "z"];
|
||||||
const indices = [-1, 0, 1];
|
const indices = [-1, 0, 1];
|
||||||
const dirs = [1, -1];
|
const dirs = [1, -1];
|
||||||
|
|
||||||
@@ -156,6 +161,6 @@ for (let i = 0; i < 100; i++) {
|
|||||||
verifyCounts(countColors());
|
verifyCounts(countColors());
|
||||||
verifyPieceTypes();
|
verifyPieceTypes();
|
||||||
verifyCenters();
|
verifyCenters();
|
||||||
console.log('PASS 100 Random Moves');
|
console.log("PASS 100 Random Moves");
|
||||||
|
|
||||||
console.log('ALL INTEGRITY TESTS PASSED');
|
console.log("ALL INTEGRITY TESTS PASSED");
|
||||||
|
|||||||
@@ -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';
|
console.log("Running Cube Logic Tests...");
|
||||||
import assert from 'assert';
|
|
||||||
|
|
||||||
console.log('Running Cube Logic Tests...');
|
|
||||||
|
|
||||||
const cube = new Cube();
|
const cube = new Cube();
|
||||||
|
|
||||||
// Helper to check a specific face color at a position
|
// Helper to check a specific face color at a position
|
||||||
const checkFace = (x, y, z, face, expectedColor, message) => {
|
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) {
|
if (!cubie) {
|
||||||
console.error(`Cubie not found at ${x}, ${y}, ${z}`);
|
console.error(`Cubie not found at ${x}, ${y}, ${z}`);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const color = cubie.faces[face];
|
const color = cubie.faces[face];
|
||||||
if (color !== expectedColor) {
|
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 false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Test 1: Initial State
|
// 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
|
// 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.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.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.RIGHT, COLORS.RED, "Initial Top-Right-Front RIGHT");
|
||||||
|
|
||||||
// Test 2: Rotate Right Face (R) -> Axis X, index 1, direction -1 (based on previous mapping)
|
// 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'.
|
// 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.
|
// The UP face of the cubie now points FRONT.
|
||||||
// So the cubie at (1, -1, 1) should have FRONT = WHITE.
|
// So the cubie at (1, -1, 1) should have FRONT = WHITE.
|
||||||
|
|
||||||
console.log('Test 2: Rotate X Axis +90 (Right Layer)');
|
console.log("Test 2: Rotate X Axis +90 (Right Layer)");
|
||||||
cube.rotateLayer('x', 1, 1);
|
cube.rotateLayer("x", 1, 1);
|
||||||
|
|
||||||
// Cubie originally at (1, 1, 1) [White Up] moves to (1, -1, 1).
|
// Cubie originally at (1, 1, 1) [White Up] moves to (1, -1, 1).
|
||||||
// Check (1, -1, 1).
|
// Check (1, -1, 1).
|
||||||
// Its Front face should be White.
|
// 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)
|
// Cubie originally at (1, 1, -1) [Blue Back, White Up] (Top-Back-Right)
|
||||||
// (1, 1, -1) -> (1, 1, 1). (Top-Front-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+)?
|
// Top (Y+) rotates to Front (Z+)?
|
||||||
// Yes.
|
// Yes.
|
||||||
// So the cubie at (1, 1, 1) (new position) should have FRONT = WHITE.
|
// 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) {
|
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 {
|
} else {
|
||||||
console.log('FAIL: X Axis Rotation Logic is broken');
|
console.log("FAIL: X Axis Rotation Logic is broken");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset for Y test
|
// Reset for Y test
|
||||||
cube.reset();
|
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).
|
// Top Layer (y=1).
|
||||||
// Rotate Y+ (direction 1).
|
// Rotate Y+ (direction 1).
|
||||||
// Front (z=1) -> Right (x=1).
|
// Front (z=1) -> Right (x=1).
|
||||||
// Cubie at (0, 1, 1) (Front-Top-Center) [Green Front, White Up].
|
// Cubie at (0, 1, 1) (Front-Top-Center) [Green Front, White Up].
|
||||||
// Moves to (1, 1, 0) (Right-Top-Center).
|
// Moves to (1, 1, 0) (Right-Top-Center).
|
||||||
// Its Front Face (Green) should move to Right Face.
|
// Its Front Face (Green) should move to Right Face.
|
||||||
cube.rotateLayer('y', 1, 1);
|
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');
|
const resultY = checkFace(
|
||||||
|
1,
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
FACES.RIGHT,
|
||||||
|
COLORS.GREEN,
|
||||||
|
"After Y+90: Old Front(Green) should be on Right",
|
||||||
|
);
|
||||||
|
|
||||||
if (resultY) {
|
if (resultY) {
|
||||||
console.log('PASS: Y Axis Rotation Logic seems correct');
|
console.log("PASS: Y Axis Rotation Logic seems correct");
|
||||||
} else {
|
} else {
|
||||||
console.log('FAIL: Y Axis Rotation Logic is broken');
|
console.log("FAIL: Y Axis Rotation Logic is broken");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,15 @@
|
|||||||
|
import { Cube, FACES, COLORS } from "../src/utils/Cube.js";
|
||||||
|
import assert from "assert";
|
||||||
|
|
||||||
import { Cube, FACES, COLORS } from '../src/utils/Cube.js';
|
console.log("Running Cube Matrix Rotation Tests...");
|
||||||
import assert from 'assert';
|
|
||||||
|
|
||||||
console.log('Running Cube Matrix Rotation Tests...');
|
|
||||||
|
|
||||||
const cube = new Cube();
|
const cube = new Cube();
|
||||||
|
|
||||||
// Helper to check position and face
|
// Helper to check position and face
|
||||||
const checkCubie = (origX, origY, origZ, newX, newY, newZ, faceCheck) => {
|
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) {
|
if (!cubie) {
|
||||||
console.error(`FAIL: Cubie not found at ${newX}, ${newY}, ${newZ}`);
|
console.error(`FAIL: Cubie not found at ${newX}, ${newY}, ${newZ}`);
|
||||||
return false;
|
return false;
|
||||||
@@ -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)?
|
// Top-Left (x=-1, y=1) -> Top-Right (x=1, y=1)?
|
||||||
// Physical CW (Z-Axis): Up -> Right.
|
// Physical CW (Z-Axis): Up -> Right.
|
||||||
// Top-Middle (0, 1) -> Right-Middle (1, 0).
|
// 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();
|
cube.reset();
|
||||||
// Find Top-Middle of Front Face: (0, 1, 1). White Up, Green Front.
|
// 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.UP], COLORS.WHITE);
|
||||||
assert.strictEqual(topMid.faces[FACES.FRONT], COLORS.GREEN);
|
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).
|
// move('F') calls rotateLayer('z', 1, -1).
|
||||||
// So let's test 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.
|
// Z-Axis CW: Up -> Right.
|
||||||
// So new pos should have Right=White.
|
// So new pos should have Right=White.
|
||||||
// Old Front (Green) stays Front.
|
// 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})`);
|
console.log(`Moved to: (${newPos.x}, ${newPos.y}, ${newPos.z})`);
|
||||||
assert.strictEqual(newPos.x, 1);
|
assert.strictEqual(newPos.x, 1);
|
||||||
assert.strictEqual(newPos.y, 0);
|
assert.strictEqual(newPos.y, 0);
|
||||||
assert.strictEqual(newPos.z, 1);
|
assert.strictEqual(newPos.z, 1);
|
||||||
assert.strictEqual(newPos.faces[FACES.RIGHT], COLORS.WHITE);
|
assert.strictEqual(newPos.faces[FACES.RIGHT], COLORS.WHITE);
|
||||||
assert.strictEqual(newPos.faces[FACES.FRONT], COLORS.GREEN);
|
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)
|
// Test 2: X-Axis Rotation (Right Face)
|
||||||
// Right Face is x=1.
|
// Right Face is x=1.
|
||||||
// Top-Front (1, 1, 1) -> Top-Back (1, 1, -1)?
|
// Top-Front (1, 1, 1) -> Top-Back (1, 1, -1)?
|
||||||
// Physical CW (X-Axis): Up -> Front.
|
// Physical CW (X-Axis): Up -> Front.
|
||||||
// Top-Middle (1, 1, 0) -> Front-Middle (1, 0, 1).
|
// 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();
|
cube.reset();
|
||||||
// Find Top-Middle of Right Face: (1, 1, 0). White Up, Red Right.
|
// 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).
|
// move('R') calls rotateLayer('x', 1, -1).
|
||||||
// So let's test -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.
|
// X-Axis CW (Right Face): Up -> Back.
|
||||||
// So new pos should have Back=White.
|
// So new pos should have Back=White.
|
||||||
// Old Right (Red) stays Right.
|
// 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})`);
|
console.log(`Moved to: (${newRightPos.x}, ${newRightPos.y}, ${newRightPos.z})`);
|
||||||
assert.strictEqual(newRightPos.x, 1);
|
assert.strictEqual(newRightPos.x, 1);
|
||||||
assert.strictEqual(newRightPos.y, 0);
|
assert.strictEqual(newRightPos.y, 0);
|
||||||
assert.strictEqual(newRightPos.z, -1);
|
assert.strictEqual(newRightPos.z, -1);
|
||||||
assert.strictEqual(newRightPos.faces[FACES.BACK], COLORS.WHITE);
|
assert.strictEqual(newRightPos.faces[FACES.BACK], COLORS.WHITE);
|
||||||
assert.strictEqual(newRightPos.faces[FACES.RIGHT], COLORS.RED);
|
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)
|
// Test 3: Y-Axis Rotation (Up Face)
|
||||||
// Up Face is y=1.
|
// Up Face is y=1.
|
||||||
@@ -86,24 +85,23 @@ console.log('PASS X-Axis CW');
|
|||||||
// Wait. move('U') calls rotateLayer('y', 1, -1).
|
// Wait. move('U') calls rotateLayer('y', 1, -1).
|
||||||
// Standard U is CW. Y-Axis direction?
|
// Standard U is CW. Y-Axis direction?
|
||||||
// move('U'): dir = -1.
|
// move('U'): dir = -1.
|
||||||
console.log('Test 3: Y-Axis CW (Up)');
|
console.log("Test 3: Y-Axis CW (Up)");
|
||||||
cube.reset();
|
cube.reset();
|
||||||
// Find Front-Middle of Up Face: (0, 1, 1). Green Front, White Up.
|
// 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).
|
// Expect: (0, 1, 1) -> (-1, 1, 0). (Left-Middle).
|
||||||
// Faces: Old Front (Green) becomes Left?
|
// Faces: Old Front (Green) becomes Left?
|
||||||
// Y-Axis CW (U): Front -> Left.
|
// Y-Axis CW (U): Front -> Left.
|
||||||
// So new pos should have Left=Green.
|
// So new pos should have Left=Green.
|
||||||
// Old Up (White) stays Up.
|
// 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})`);
|
console.log(`Moved to: (${newUpPos.x}, ${newUpPos.y}, ${newUpPos.z})`);
|
||||||
assert.strictEqual(newUpPos.x, -1);
|
assert.strictEqual(newUpPos.x, -1);
|
||||||
assert.strictEqual(newUpPos.y, 1);
|
assert.strictEqual(newUpPos.y, 1);
|
||||||
assert.strictEqual(newUpPos.z, 0);
|
assert.strictEqual(newUpPos.z, 0);
|
||||||
assert.strictEqual(newUpPos.faces[FACES.LEFT], COLORS.GREEN);
|
assert.strictEqual(newUpPos.faces[FACES.LEFT], COLORS.GREEN);
|
||||||
assert.strictEqual(newUpPos.faces[FACES.UP], COLORS.WHITE);
|
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
24
test/debug_kociemba.js
Normal 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
197
test/generate_math.js
Normal 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
36
test/math_output.txt
Normal 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]
|
||||||
|
)
|
||||||
@@ -1,15 +1,16 @@
|
|||||||
|
import { Cube, FACES, COLORS } from "../src/utils/Cube.js";
|
||||||
import { Cube, FACES, COLORS } from '../src/utils/Cube.js';
|
|
||||||
|
|
||||||
// Helper to print face
|
// Helper to print face
|
||||||
const printFace = (matrix, name) => {
|
const printFace = (matrix, name) => {
|
||||||
console.log(`--- ${name} ---`);
|
console.log(`--- ${name} ---`);
|
||||||
matrix.forEach(row => console.log(row.map(c => c ? c[0].toUpperCase() : '-').join(' ')));
|
matrix.forEach((row) =>
|
||||||
|
console.log(row.map((c) => (c ? c[0].toUpperCase() : "-")).join(" ")),
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Helper to check if a face matches expected color (center color)
|
// Helper to check if a face matches expected color (center color)
|
||||||
const checkFaceColor = (matrix, expectedColor) => {
|
const checkFaceColor = (matrix, expectedColor) => {
|
||||||
return matrix.every(row => row.every(c => c === expectedColor));
|
return matrix.every((row) => row.every((c) => c === expectedColor));
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log("=== RUBIK'S CUBE SIMULATION & DIAGNOSTIC ===");
|
console.log("=== RUBIK'S CUBE SIMULATION & DIAGNOSTIC ===");
|
||||||
@@ -52,20 +53,28 @@ console.log("\n2. Simulating: Left Layer (x=-1) Rotation (L-like move)...");
|
|||||||
|
|
||||||
// Try direction = 1
|
// Try direction = 1
|
||||||
console.log("-> Applying rotateLayer('x', -1, 1)...");
|
console.log("-> Applying rotateLayer('x', -1, 1)...");
|
||||||
cube.rotateLayer('x', -1, 1);
|
cube.rotateLayer("x", -1, 1);
|
||||||
state = cube.getState();
|
state = cube.getState();
|
||||||
|
|
||||||
// Check result on Left Column of Front Face
|
// Check result on Left Column of Front Face
|
||||||
// Front is Green. Top is White.
|
// Front is Green. Top is White.
|
||||||
// If L (Drag Down): Front-Left-Col should be White.
|
// If L (Drag Down): Front-Left-Col should be White.
|
||||||
const frontLeftCol = [state[FACES.FRONT][0][0], state[FACES.FRONT][1][0], state[FACES.FRONT][2][0]];
|
const frontLeftCol = [
|
||||||
|
state[FACES.FRONT][0][0],
|
||||||
|
state[FACES.FRONT][1][0],
|
||||||
|
state[FACES.FRONT][2][0],
|
||||||
|
];
|
||||||
console.log("Front Left Column colors:", frontLeftCol);
|
console.log("Front Left Column colors:", frontLeftCol);
|
||||||
|
|
||||||
if (frontLeftCol.every(c => c === COLORS.WHITE)) {
|
if (frontLeftCol.every((c) => c === COLORS.WHITE)) {
|
||||||
console.log("✅ Result: Front got White (Top). This matches 'Drag Down' (L move).");
|
console.log(
|
||||||
|
"✅ Result: Front got White (Top). This matches 'Drag Down' (L move).",
|
||||||
|
);
|
||||||
console.log("=> CONCLUSION: direction=1 corresponds to Drag Down (L).");
|
console.log("=> CONCLUSION: direction=1 corresponds to Drag Down (L).");
|
||||||
} else if (frontLeftCol.every(c => c === COLORS.YELLOW)) {
|
} else if (frontLeftCol.every((c) => c === COLORS.YELLOW)) {
|
||||||
console.log("⚠️ Result: Front got Yellow (Down). This matches 'Drag Up' (L' move).");
|
console.log(
|
||||||
|
"⚠️ Result: Front got Yellow (Down). This matches 'Drag Up' (L' move).",
|
||||||
|
);
|
||||||
console.log("=> CONCLUSION: direction=1 corresponds to Drag Up (L').");
|
console.log("=> CONCLUSION: direction=1 corresponds to Drag Up (L').");
|
||||||
} else {
|
} else {
|
||||||
console.error("❌ Unexpected colors:", frontLeftCol);
|
console.error("❌ Unexpected colors:", frontLeftCol);
|
||||||
@@ -89,7 +98,7 @@ cube.reset();
|
|||||||
console.log("\n3. Simulating: Top Layer (y=1) Rotation...");
|
console.log("\n3. Simulating: Top Layer (y=1) Rotation...");
|
||||||
// Try direction = 1
|
// Try direction = 1
|
||||||
console.log("-> Applying rotateLayer('y', 1, 1)...");
|
console.log("-> Applying rotateLayer('y', 1, 1)...");
|
||||||
cube.rotateLayer('y', 1, 1);
|
cube.rotateLayer("y", 1, 1);
|
||||||
state = cube.getState();
|
state = cube.getState();
|
||||||
|
|
||||||
// Check result on Top Row of Front Face
|
// Check result on Top Row of Front Face
|
||||||
@@ -104,10 +113,10 @@ state = cube.getState();
|
|||||||
const frontTopRow = state[FACES.FRONT][0];
|
const frontTopRow = state[FACES.FRONT][0];
|
||||||
console.log("Front Top Row colors:", frontTopRow);
|
console.log("Front Top Row colors:", frontTopRow);
|
||||||
|
|
||||||
if (frontTopRow.every(c => c === COLORS.ORANGE)) {
|
if (frontTopRow.every((c) => c === COLORS.ORANGE)) {
|
||||||
console.log("✅ Result: Front got Orange (Left). This matches 'Drag Right'.");
|
console.log("✅ Result: Front got Orange (Left). This matches 'Drag Right'.");
|
||||||
console.log("=> CONCLUSION: direction=1 corresponds to Drag Right.");
|
console.log("=> CONCLUSION: direction=1 corresponds to Drag Right.");
|
||||||
} else if (frontTopRow.every(c => c === COLORS.RED)) {
|
} else if (frontTopRow.every((c) => c === COLORS.RED)) {
|
||||||
console.log("⚠️ Result: Front got Red (Right). This matches 'Drag Left'.");
|
console.log("⚠️ Result: Front got Red (Right). This matches 'Drag Left'.");
|
||||||
console.log("=> CONCLUSION: direction=1 corresponds to Drag Left.");
|
console.log("=> CONCLUSION: direction=1 corresponds to Drag Left.");
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
|
import { CubeModel, FACES, COLORS } from "../src/utils/CubeModel.js";
|
||||||
|
|
||||||
import { CubeModel, FACES, COLORS } from '../src/utils/CubeModel.js';
|
console.log("Running CubeModel Rotation Logic Tests...");
|
||||||
|
|
||||||
console.log('Running CubeModel Rotation Logic Tests...');
|
|
||||||
|
|
||||||
const cube1 = new CubeModel();
|
const cube1 = new CubeModel();
|
||||||
const cube2 = new CubeModel();
|
const cube2 = new CubeModel();
|
||||||
@@ -14,9 +13,9 @@ const compareCubes = (c1, c2, message) => {
|
|||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
console.error(`❌ FAIL: ${message}`);
|
console.error(`❌ FAIL: ${message}`);
|
||||||
console.log('Expected (Standard Move):');
|
console.log("Expected (Standard Move):");
|
||||||
console.log(s2);
|
console.log(s2);
|
||||||
console.log('Actual (Layer Rotation):');
|
console.log("Actual (Layer Rotation):");
|
||||||
console.log(s1);
|
console.log(s1);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -25,48 +24,47 @@ const compareCubes = (c1, c2, message) => {
|
|||||||
// Test 1: Top Layer (y=1) CW vs U
|
// Test 1: Top Layer (y=1) CW vs U
|
||||||
cube1.reset();
|
cube1.reset();
|
||||||
cube2.reset();
|
cube2.reset();
|
||||||
console.log('Testing Top Layer CW vs U...');
|
console.log("Testing Top Layer CW vs U...");
|
||||||
cube1.rotateLayer('y', 1, 1); // Top CW
|
cube1.rotateLayer("y", 1, 1); // Top CW
|
||||||
cube2.applyMove('U');
|
cube2.applyMove("U");
|
||||||
compareCubes(cube1, cube2, "Top Layer CW matches U");
|
compareCubes(cube1, cube2, "Top Layer CW matches U");
|
||||||
|
|
||||||
// Test 2: Bottom Layer (y=-1) CW vs D
|
// Test 2: Bottom Layer (y=-1) CW vs D
|
||||||
cube1.reset();
|
cube1.reset();
|
||||||
cube2.reset();
|
cube2.reset();
|
||||||
console.log('Testing Bottom Layer CW vs D...');
|
console.log("Testing Bottom Layer CW vs D...");
|
||||||
cube1.rotateLayer('y', -1, -1); // Bottom CW (CW around -Y is CCW around Y)
|
cube1.rotateLayer("y", -1, -1); // Bottom CW (CW around -Y is CCW around Y)
|
||||||
cube2.applyMove('D');
|
cube2.applyMove("D");
|
||||||
compareCubes(cube1, cube2, "Bottom Layer CW matches D");
|
compareCubes(cube1, cube2, "Bottom Layer CW matches D");
|
||||||
|
|
||||||
// Test 3: Left Layer (x=-1) CW vs L
|
// Test 3: Left Layer (x=-1) CW vs L
|
||||||
cube1.reset();
|
cube1.reset();
|
||||||
cube2.reset();
|
cube2.reset();
|
||||||
console.log('Testing Left Layer CW vs L...');
|
console.log("Testing Left Layer CW vs L...");
|
||||||
cube1.rotateLayer('x', -1, -1); // Left CW (CW around -X is CCW around X)
|
cube1.rotateLayer("x", -1, -1); // Left CW (CW around -X is CCW around X)
|
||||||
cube2.applyMove('L');
|
cube2.applyMove("L");
|
||||||
compareCubes(cube1, cube2, "Left Layer CW matches L");
|
compareCubes(cube1, cube2, "Left Layer CW matches L");
|
||||||
|
|
||||||
// Test 4: Right Layer (x=1) CW vs R
|
// Test 4: Right Layer (x=1) CW vs R
|
||||||
cube1.reset();
|
cube1.reset();
|
||||||
cube2.reset();
|
cube2.reset();
|
||||||
console.log('Testing Right Layer CW vs R...');
|
console.log("Testing Right Layer CW vs R...");
|
||||||
cube1.rotateLayer('x', 1, 1); // Right CW
|
cube1.rotateLayer("x", 1, 1); // Right CW
|
||||||
cube2.applyMove('R');
|
cube2.applyMove("R");
|
||||||
compareCubes(cube1, cube2, "Right Layer CW matches R");
|
compareCubes(cube1, cube2, "Right Layer CW matches R");
|
||||||
|
|
||||||
// Test 5: Front Layer (z=1) CW vs F
|
// Test 5: Front Layer (z=1) CW vs F
|
||||||
cube1.reset();
|
cube1.reset();
|
||||||
cube2.reset();
|
cube2.reset();
|
||||||
console.log('Testing Front Layer CW vs F...');
|
console.log("Testing Front Layer CW vs F...");
|
||||||
cube1.rotateLayer('z', 1, 1); // Front CW
|
cube1.rotateLayer("z", 1, 1); // Front CW
|
||||||
cube2.applyMove('F');
|
cube2.applyMove("F");
|
||||||
compareCubes(cube1, cube2, "Front Layer CW matches F");
|
compareCubes(cube1, cube2, "Front Layer CW matches F");
|
||||||
|
|
||||||
// Test 6: Back Layer (z=-1) CW vs B
|
// Test 6: Back Layer (z=-1) CW vs B
|
||||||
cube1.reset();
|
cube1.reset();
|
||||||
cube2.reset();
|
cube2.reset();
|
||||||
console.log('Testing Back Layer CW vs B...');
|
console.log("Testing Back Layer CW vs B...");
|
||||||
cube1.rotateLayer('z', -1, -1); // Back CW (CW around -Z is CCW around Z)
|
cube1.rotateLayer("z", -1, -1); // Back CW (CW around -Z is CCW around Z)
|
||||||
cube2.applyMove('B');
|
cube2.applyMove("B");
|
||||||
compareCubes(cube1, cube2, "Back Layer CW matches B");
|
compareCubes(cube1, cube2, "Back Layer CW matches B");
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +0,0 @@
|
|||||||
|
|
||||||
import { State } from 'rubiks-js/src/state/index.js';
|
|
||||||
|
|
||||||
console.log('State imported successfully');
|
|
||||||
const state = new State(true);
|
|
||||||
console.log('State instantiated');
|
|
||||||
|
|
||||||
state.applyTurn('R');
|
|
||||||
console.log('Applied turn R');
|
|
||||||
|
|
||||||
const encoded = state.encode();
|
|
||||||
console.log('Encoded state:', encoded);
|
|
||||||
41
test/verify_integrity.js
Normal file
41
test/verify_integrity.js
Normal 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
75
test/verify_solvers.js
Normal 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);
|
||||||
Reference in New Issue
Block a user