diff --git a/Cargo.toml b/Cargo.toml
index d028b19..9704df4 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,7 +1,7 @@
[package]
name = "cube-lib"
version = "0.1.0"
-edition = "2021"
+edition = "2024"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
@@ -9,5 +9,6 @@ edition = "2021"
thiserror = "1.0.50"
[dev-dependencies]
+itertools = "0.14.0"
proptest = "1.7.0"
proptest-derive = "0.6.0"
diff --git a/src/coord.rs b/src/coord.rs
new file mode 100644
index 0000000..dc7309c
--- /dev/null
+++ b/src/coord.rs
@@ -0,0 +1,36 @@
+//! We give a general description of a coordinate, which is a type used to encode coset information
+//! of a puzzle.
+
+/// A coordinate type, encoding cosets of the puzzle P.
+pub trait Coordinate
: Copy + Default + Eq {
+ // TODO this API assumes that coord conversion doesn't require any additional data, perhaps
+ // this should be changed
+ /// Obtain the coordinate that corresponds to the given puzzle.
+ fn from_puzzle(puzzle: &P) -> Self;
+
+ // TODO should this be kept
+ /// Determine whether the given coordinate represents a solved state
+ fn solved(self) -> bool {
+ self.repr() == 0
+ }
+
+ /// The number of possible coordinate states.
+ fn count() -> usize;
+
+ /// A representation of this coordinate as a usize, for use in table lookups.
+ fn repr(self) -> usize;
+
+ // TODO this might not be ideal it's not very type safe idk
+ /// Convert the representation of a coordinate to the coordinate itself. We assume 0
+ /// corresponds to the solved state.
+ fn from_repr(n: usize) -> Self;
+}
+
+/// Gives the ability to set a coordinate onto a puzzle.
+pub trait FromCoordinate: Sized
+where
+ C: Coordinate,
+{
+ /// Modify the puzzle so that its coordinate for `C` is `coord`.
+ fn set_coord(&mut self, coord: C);
+}
diff --git a/src/cube333/coordcube.rs b/src/cube333/coordcube.rs
index 2e5e431..b45e53a 100644
--- a/src/cube333/coordcube.rs
+++ b/src/cube333/coordcube.rs
@@ -1,13 +1,182 @@
use super::{Corner, CornerTwist, CubieCube, Edge, EdgeFlip};
+use crate::coord::{Coordinate, FromCoordinate};
-#[derive(Debug, Default, PartialEq, Eq, Copy, Clone)]
-struct COCoord(u16);
-#[derive(Debug, Default, PartialEq, Eq, Copy, Clone)]
-struct CPCoord(u16);
-#[derive(Debug, Default, PartialEq, Eq, Copy, Clone)]
-struct EOCoord(u16);
-#[derive(Debug, Default, PartialEq, Eq, Copy, Clone)]
-struct EPCoord(u32);
+/// A coordinate representation of the corner orientation of a cube with respect to the U/F faces.
+#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Copy, Clone, Hash)]
+pub struct COCoord(u16);
+
+/// A coordinate representation of the corner permutation of a cube with respect to the U/F faces.
+#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Copy, Clone, Hash)]
+pub struct CPCoord(u16);
+
+/// A coordinate representation of the edge orientation of a cube with respect to the U/F faces.
+#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Copy, Clone, Hash)]
+pub struct EOCoord(u16);
+
+/// A coordinate representation of the edge permutation of a cube with respect to the U/F faces.
+#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Copy, Clone, Hash)]
+pub struct EPCoord(u32);
+
+// TODO AHHHH
+// this is copied from the two phase solver!!! so messy!!! AHHHH
+fn set_p_coord(
+ mut c: usize,
+ perm: &mut [P; COUNT],
+ mut bag: Vec,
+) {
+ let mut n = [0; COUNT];
+ for (i, n) in n.iter_mut().enumerate() {
+ *n = c % (i + 1);
+ c /= i + 1;
+ }
+
+ for i in (LOWER..=UPPER).rev() {
+ let index = n[i - LOWER];
+ perm[i] = bag[index];
+ bag.remove(index);
+ }
+}
+
+impl Coordinate for COCoord {
+ fn from_puzzle(puzzle: &CubieCube) -> Self {
+ COCoord(to_o_coord::<8, 3>(&puzzle.co.map(|n| n.into())))
+ }
+
+ fn count() -> usize {
+ // 3^7
+ 2187
+ }
+
+ fn repr(self) -> usize {
+ self.0 as usize
+ }
+
+ fn from_repr(n: usize) -> Self {
+ COCoord(n as u16)
+ }
+}
+
+impl FromCoordinate for CubieCube {
+ fn set_coord(&mut self, coord: COCoord) {
+ let mut first = CornerTwist::Oriented;
+ let mut n = coord.0;
+
+ for i in (1..8).rev() {
+ self.co[i] = match n % 3 {
+ 0 => CornerTwist::Oriented,
+ 1 => {
+ first = first.anticlockwise();
+ CornerTwist::Clockwise
+ }
+ 2 => {
+ first = first.clockwise();
+ CornerTwist::AntiClockwise
+ }
+ _ => unreachable!(),
+ };
+ n /= 3;
+ }
+
+ self.co[0] = first;
+ }
+}
+
+impl Coordinate for CPCoord {
+ fn from_puzzle(puzzle: &CubieCube) -> Self {
+ CPCoord(to_p_coord::<8>(&puzzle.cp.map(|n| n.into())) as u16)
+ }
+
+ fn count() -> usize {
+ 40320
+ }
+
+ fn repr(self) -> usize {
+ self.0 as usize
+ }
+
+ fn from_repr(n: usize) -> Self {
+ CPCoord(n as u16)
+ }
+}
+
+impl FromCoordinate for CubieCube {
+ fn set_coord(&mut self, coord: CPCoord) {
+ use crate::cube333::corner::Corner as C;
+ #[rustfmt::skip]
+ let bag = vec![C::DBR, C::DBL, C::DFL, C::DFR, C::UBR, C::UBL, C::UFL, C::UFR];
+
+ set_p_coord::<8, 0, 7, C>(coord.repr(), &mut self.cp, bag);
+ }
+}
+
+impl Coordinate for EOCoord {
+ fn from_puzzle(puzzle: &CubieCube) -> Self {
+ EOCoord(to_o_coord::<12, 2>(&puzzle.eo.map(|n| n.into())))
+ }
+
+ fn count() -> usize {
+ // 2^11
+ 2048
+ }
+
+ fn repr(self) -> usize {
+ self.0 as usize
+ }
+
+ fn from_repr(n: usize) -> Self {
+ EOCoord(n as u16)
+ }
+}
+
+impl FromCoordinate for CubieCube {
+ fn set_coord(&mut self, coord: EOCoord) {
+ let mut first = EdgeFlip::Oriented;
+ let mut n = coord.0;
+
+ for i in (1..12).rev() {
+ self.eo[i] = match n % 2 {
+ 0 => EdgeFlip::Oriented,
+ 1 => {
+ first = first.flip();
+ EdgeFlip::Flipped
+ }
+ _ => unreachable!(),
+ };
+ n /= 2;
+ }
+
+ self.eo[0] = first;
+ }
+}
+
+impl Coordinate for EPCoord {
+ fn from_puzzle(puzzle: &CubieCube) -> Self {
+ EPCoord(to_p_coord::<12>(&puzzle.ep.map(|n| n.into())))
+ }
+
+ fn count() -> usize {
+ // a lot
+ 479001600
+ }
+
+ fn repr(self) -> usize {
+ self.0 as usize
+ }
+
+ fn from_repr(n: usize) -> Self {
+ EPCoord(n as u32)
+ }
+}
+
+impl FromCoordinate for CubieCube {
+ fn set_coord(&mut self, coord: EPCoord) {
+ use crate::cube333::edge::Edge as E;
+ #[rustfmt::skip]
+ let bag = vec![E::BR, E::BL, E::FL, E::FR, E::DR, E::DB, E::DL, E::DF, E::UR, E::UB, E::UL, E::UF];
+
+ set_p_coord::<12, 0, 11, E>(coord.repr(), &mut self.ep, bag);
+ }
+}
/// Implementation of a coord cube, representing pieces using coordinates, which are values which
/// are isomorphic to arrays represented in a cubie cube.
@@ -146,10 +315,10 @@ impl CubieCube {
});
}
- let co = COCoord(to_o_coord::<8, 3>(&self.co.map(|n| n.into())));
- let cp = CPCoord(to_p_coord::<8>(&self.cp.map(|n| n.into())) as u16);
- let eo = EOCoord(to_o_coord::<12, 2>(&self.eo.map(|n| n.into())));
- let ep = EPCoord(to_p_coord::<12>(&self.ep.map(|n| n.into())));
+ let co = COCoord::from_puzzle(self);
+ let cp = CPCoord::from_puzzle(self);
+ let eo = EOCoord::from_puzzle(self);
+ let ep = EPCoord::from_puzzle(self);
Ok(CoordCube { co, cp, eo, ep })
}
@@ -157,10 +326,11 @@ impl CubieCube {
#[cfg(test)]
mod tests {
+ use super::*;
use crate::cube333::{
+ Corner, CornerTwist, CubieCube, Edge, EdgeFlip, StickerCube,
coordcube::{CoordCube, CubieToCoordError},
moves::{Move333, Move333Type},
- Corner, CornerTwist, CubieCube, Edge, EdgeFlip, StickerCube,
};
use crate::mv;
@@ -246,4 +416,25 @@ mod tests {
swap.cp[3] = Corner::UFR;
assert!(swap.to_coord().is_ok());
}
+
+ use proptest::prelude::*;
+
+ proptest! {
+ // TODO this test is not good, it should take a *random state* as input to test that the
+ // last piece is set correctly, but I haven't been bothered to make that yet...
+
+ #[test]
+ fn convert_invertible_co(c in (0..2187u16).prop_map(COCoord)) {
+ let mut cube = CubieCube::SOLVED;
+ cube.set_coord(c);
+ assert_eq!(c, COCoord::from_puzzle(&cube));
+ }
+
+ #[test]
+ fn convert_invertible_eo(c in (0..2048u16).prop_map(EOCoord)) {
+ let mut cube = CubieCube::SOLVED;
+ cube.set_coord(c);
+ assert_eq!(c, EOCoord::from_puzzle(&cube));
+ }
+ }
}
diff --git a/src/cube333/corner.rs b/src/cube333/corner.rs
index c2dc4b4..1721117 100644
--- a/src/cube333/corner.rs
+++ b/src/cube333/corner.rs
@@ -4,15 +4,16 @@ use crate::error::TryFromIntToEnumError;
/// An enum for every corner piece location.
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
#[allow(missing_docs)]
+#[repr(u8)]
pub enum Corner {
- UFR,
- UFL,
- UBL,
- UBR,
- DFR,
- DFL,
- DBL,
- DBR,
+ UFR = 0,
+ UFL = 1,
+ UBL = 2,
+ UBR = 3,
+ DFR = 4,
+ DFL = 5,
+ DBL = 6,
+ DBR = 7,
}
use Corner as C;
@@ -47,16 +48,7 @@ impl Corner {
impl From for u8 {
fn from(value: Corner) -> Self {
- match value {
- C::UFR => 0,
- C::UFL => 1,
- C::UBL => 2,
- C::UBR => 3,
- C::DFR => 4,
- C::DFL => 5,
- C::DBL => 6,
- C::DBR => 7,
- }
+ value as u8
}
}
@@ -81,21 +73,18 @@ impl TryFrom for Corner {
/// An enum for every corner twist case.
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
#[allow(missing_docs)]
+#[repr(u8)]
pub enum CornerTwist {
- Oriented,
- Clockwise,
- AntiClockwise,
+ Oriented = 0,
+ Clockwise = 1,
+ AntiClockwise = 2,
}
use CornerTwist as CT;
impl From for u8 {
fn from(value: CornerTwist) -> Self {
- match value {
- CornerTwist::Oriented => 0,
- CornerTwist::Clockwise => 1,
- CornerTwist::AntiClockwise => 2,
- }
+ value as u8
}
}
@@ -139,6 +128,15 @@ impl CornerTwist {
CT::AntiClockwise => self.anticlockwise(),
}
}
+
+ /// Calculate the inverse twist of a given twist.
+ pub fn inverse(self) -> CornerTwist {
+ match self {
+ CT::Oriented => CT::Oriented,
+ CT::Clockwise => CT::AntiClockwise,
+ CT::AntiClockwise => CT::Clockwise,
+ }
+ }
}
/// An enum to represent every corner sticker position.
diff --git a/src/cube333/edge.rs b/src/cube333/edge.rs
index 585e5c5..3cc01bd 100644
--- a/src/cube333/edge.rs
+++ b/src/cube333/edge.rs
@@ -4,19 +4,20 @@ use crate::error::TryFromIntToEnumError;
/// An enum for every edge piece location.
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
#[allow(missing_docs)]
+#[repr(u8)]
pub enum Edge {
- UF,
- UL,
- UB,
- UR,
- DF,
- DL,
- DB,
- DR,
- FR,
- FL,
- BL,
- BR,
+ UF = 0,
+ UL = 1,
+ UB = 2,
+ UR = 3,
+ DF = 4,
+ DL = 5,
+ DB = 6,
+ DR = 7,
+ FR = 8,
+ FL = 9,
+ BL = 10,
+ BR = 11,
}
use Edge as E;
@@ -55,24 +56,26 @@ impl Edge {
E::BL,
E::BR,
];
+
+ /// Determines whether this edge sits on the E slice.
+ pub fn e_slice(&self) -> bool {
+ matches!(self, E::FR | E::FL | E::BL | E::BR)
+ }
+
+ /// Determines whether this edge sits on the M slice.
+ pub fn m_slice(&self) -> bool {
+ matches!(self, E::UF | E::UB | E::DF | E::DB)
+ }
+
+ /// Determines whether this edge sits on the S slice.
+ pub fn s_slice(&self) -> bool {
+ matches!(self, E::UL | E::UR | E::DL | E::DR)
+ }
}
impl From for u8 {
fn from(value: Edge) -> Self {
- match value {
- E::UF => 0,
- E::UL => 1,
- E::UB => 2,
- E::UR => 3,
- E::DF => 4,
- E::DL => 5,
- E::DB => 6,
- E::DR => 7,
- E::FR => 8,
- E::FL => 9,
- E::BL => 10,
- E::BR => 11,
- }
+ value as u8
}
}
@@ -101,19 +104,17 @@ impl TryFrom for Edge {
/// An enum to tell if an edge is flipped or not.
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
#[allow(missing_docs)]
+#[repr(u8)]
pub enum EdgeFlip {
- Oriented,
- Flipped,
+ Oriented = 0,
+ Flipped = 1,
}
use EdgeFlip as EF;
impl From for u8 {
fn from(value: EdgeFlip) -> Self {
- match value {
- EF::Oriented => 0,
- EF::Flipped => 1,
- }
+ value as u8
}
}
diff --git a/src/cube333/mod.rs b/src/cube333/mod.rs
index 29a86cb..8217729 100644
--- a/src/cube333/mod.rs
+++ b/src/cube333/mod.rs
@@ -14,6 +14,8 @@ pub mod edge;
/// Defines move types and implements application of moves to the CubieCube.
pub mod moves;
+pub mod two_phase_solver;
+
mod cubiecube;
use corner::{Corner, CornerPos, CornerTwist};
@@ -417,8 +419,8 @@ impl std::fmt::Display for StickerCube {
mod tests {
#[test]
fn pieces_on_solved_cube() {
- use super::edge::EdgePos::*;
use super::StickerCube;
+ use super::edge::EdgePos::*;
assert_eq!(StickerCube::SOLVED.edge_at(UB).unwrap(), UB, "UB");
assert_eq!(StickerCube::SOLVED.edge_at(UR).unwrap(), UR, "UR");
assert_eq!(StickerCube::SOLVED.edge_at(UF).unwrap(), UF, "UF");
diff --git a/src/cube333/moves.rs b/src/cube333/moves.rs
index 6fd99a3..9df0a29 100644
--- a/src/cube333/moves.rs
+++ b/src/cube333/moves.rs
@@ -23,6 +23,20 @@ pub enum Move333Type {
B,
}
+impl Move333Type {
+ /// The move type on the face opposite to the given one.
+ pub fn opposite(self) -> Move333Type {
+ match self {
+ Move333Type::R => Move333Type::L,
+ Move333Type::L => Move333Type::R,
+ Move333Type::U => Move333Type::D,
+ Move333Type::D => Move333Type::U,
+ Move333Type::F => Move333Type::B,
+ Move333Type::B => Move333Type::F,
+ }
+ }
+}
+
/// Stores a move type and counter. An anti-clockwise move will have a count of 3.
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[cfg_attr(test, derive(Arbitrary))]
@@ -50,10 +64,7 @@ impl crate::moves::Move for Move333 {
}
}
- fn cancel(self, b: Self) -> Cancellation
- where
- Self: Sized,
- {
+ fn cancel(self, b: Self) -> Cancellation {
if self.ty == b.ty {
let count = (self.count + b.count) % 4;
if count == 0 {
@@ -155,7 +166,7 @@ const CO_OFFSETS: [[u8; 8]; 6] = [
[1, 2, 0, 0, 2, 1, 0, 0],
[0, 0, 1, 2, 0, 0, 2, 1],
];
-const CP_OFFSETS: [[usize; 8]; 6] = [
+const CP_OFFSETS: [[u8; 8]; 6] = [
[4, 1, 2, 0, 7, 5, 6, 3],
[0, 2, 6, 3, 4, 1, 5, 7],
[3, 0, 1, 2, 4, 5, 6, 7],
@@ -171,7 +182,7 @@ const EO_OFFSETS: [[u8; 12]; 6] = [
[1, 0, 0, 0, 1, 0, 0, 0, 1, 1, 0, 0],
[0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 1],
];
-const EP_OFFSETS: [[usize; 12]; 6] = [
+const EP_OFFSETS: [[u8; 12]; 6] = [
[0, 1, 2, 8, 4, 5, 6, 11, 7, 9, 10, 3],
[0, 10, 2, 3, 4, 9, 6, 7, 8, 1, 5, 11],
[3, 0, 1, 2, 4, 5, 6, 7, 8, 9, 10, 11],
@@ -181,27 +192,21 @@ const EP_OFFSETS: [[usize; 12]; 6] = [
];
impl CubieCube {
- // This should really not be borrowing...
- /// Copy the cube and apply an algorithm to it.
- pub fn make_moves(&self, mvs: MoveSequence) -> CubieCube {
- let mut r = self.clone();
- for mv in mvs.0 {
- r = r.make_move(mv);
- }
- r
+ /// Apply an algorithm to a cube
+ pub fn make_moves(self, mvs: MoveSequence) -> CubieCube {
+ mvs.0.into_iter().fold(self, |c, m| c.make_move(m))
}
// This function doesn't really need to be fast since coordinates exist
- /// Copy the cube, apply a move to it, then return the new cube.
- pub fn make_move(&self, mv: Move333) -> CubieCube {
- let mut r = self.clone();
- for _ in 0..mv.count {
- r = r.make_move_type(mv.ty);
- }
- r
+ /// Apply a move to a cube.
+ pub fn make_move(self, mv: Move333) -> CubieCube {
+ (0..mv.count).fold(self, |c, _| c.make_move_type(mv.ty))
}
- fn make_move_type(&self, mv: Move333Type) -> CubieCube {
+ // TODO make this const please that would be very handy :)
+
+ /// Make a single application of a move
+ pub fn make_move_type(self, mv: Move333Type) -> CubieCube {
let co_offsets = CO_OFFSETS[mv as usize];
let cp_offsets = CP_OFFSETS[mv as usize];
let eo_offsets = EO_OFFSETS[mv as usize];
@@ -218,13 +223,13 @@ impl CubieCube {
let mut ep = [0; 12];
for i in 0..8 {
- co[i] = (selfco[cp_offsets[i]] + co_offsets[i]) % 3;
- cp[i] = selfcp[cp_offsets[i]];
+ co[i] = (selfco[cp_offsets[i] as usize] + co_offsets[i]) % 3;
+ cp[i] = selfcp[cp_offsets[i] as usize];
}
for i in 0..12 {
- eo[i] = (selfeo[ep_offsets[i]] + eo_offsets[i]) % 2;
- ep[i] = selfep[ep_offsets[i]];
+ eo[i] = (selfeo[ep_offsets[i] as usize] + eo_offsets[i]) % 2;
+ ep[i] = selfep[ep_offsets[i] as usize];
}
let co = co.map(|n| n.try_into().unwrap());
@@ -234,6 +239,46 @@ impl CubieCube {
CubieCube { co, cp, eo, ep }
}
+
+ /// Multiply two cube states in the Rubik's cube group.
+ pub fn multiply_cube(self, other: CubieCube) -> CubieCube {
+ let mut result = CubieCube::SOLVED;
+
+ for i in 0..8 {
+ let oa = self.co[other.cp[i] as usize];
+ let ob = other.co[i];
+ let o = oa.twist_by(ob);
+ result.co[i] = o;
+ result.cp[i] = self.cp[other.cp[i] as usize];
+ }
+
+ for i in 0..12 {
+ let oa = self.eo[other.ep[i] as usize];
+ let ob = other.eo[i];
+ let o = oa.flip_by(ob);
+ result.eo[i] = o;
+ result.ep[i] = self.ep[other.ep[i] as usize];
+ }
+
+ result
+ }
+
+ /// Get the inverse in the Rubik's cube group.
+ pub fn inverse(self) -> CubieCube {
+ let mut result = CubieCube::SOLVED;
+
+ for i in 0..8 {
+ result.co[self.cp[i] as usize] = self.co[i].inverse();
+ result.cp[self.cp[i] as usize] = (i as u8).try_into().unwrap();
+ }
+
+ for i in 0..12 {
+ result.eo[self.ep[i] as usize] = self.eo[i];
+ result.ep[self.ep[i] as usize] = (i as u8).try_into().unwrap();
+ }
+
+ result
+ }
}
#[cfg(test)]
@@ -257,23 +302,31 @@ mod tests {
proptest! {
#[test]
- fn cancel_same_moves(mvs in vec(any::(), 0..20).prop_map(|v| MoveSequence(v))) {
+ fn cancel_same_moves(mvs in vec(any::(), 0..20).prop_map(MoveSequence)) {
let cancelled = mvs.clone().cancel();
assert!(cancelled.len() <= mvs.len());
assert_eq!(CubieCube::SOLVED.make_moves(mvs), CubieCube::SOLVED.make_moves(cancelled));
}
#[test]
- fn invert_identity(mvs in vec(any::(), 0..20).prop_map(|v| MoveSequence(v))) {
+ fn invert_identity(mvs in vec(any::(), 0..20).prop_map(MoveSequence)) {
let cancelled = mvs.clone().cancel();
assert_eq!(CubieCube::SOLVED.make_moves(mvs.clone()).make_moves(mvs.inverse()), CubieCube::SOLVED);
assert!(cancelled.clone().append(cancelled.clone().inverse()).cancel().is_empty());
}
#[test]
- fn cancel_idemotent(mvs in vec(any::(), 0..20).prop_map(|v| MoveSequence(v))) {
+ fn cancel_idemotent(mvs in vec(any::(), 0..20).prop_map(MoveSequence)) {
let cancelled = mvs.clone().cancel();
assert_eq!(cancelled.clone().cancel(), cancelled);
}
+
+ #[test]
+ fn inverse_apply(mvs in vec(any::(), 0..20).prop_map(MoveSequence)) {
+ // TODO use real random state for this proptest! doing random moves is silly!
+ let fake_random_state = CubieCube::SOLVED.make_moves(mvs);
+ assert_eq!(CubieCube::SOLVED, fake_random_state.clone().multiply_cube(fake_random_state.clone().inverse()));
+ assert_eq!(CubieCube::SOLVED, fake_random_state.clone().inverse().multiply_cube(fake_random_state.clone()));
+ }
}
}
diff --git a/src/cube333/two_phase_solver/coords.rs b/src/cube333/two_phase_solver/coords.rs
new file mode 100644
index 0000000..14d2f5e
--- /dev/null
+++ b/src/cube333/two_phase_solver/coords.rs
@@ -0,0 +1,507 @@
+//! This module contains the coordinate representations of cube states relevant to the two phases
+//! of these solver.
+
+use super::symmetry::{DrSymmetry, HalfSymmetry, Symmetry};
+use crate::coord::{Coordinate, FromCoordinate};
+use crate::cube333::{
+ CubieCube,
+ coordcube::{COCoord, CPCoord, EOCoord},
+};
+
+// TODO this is kinda unreadable lol
+// this is copied from coordcube.rs then modified hmmm maybe copy pasting isn't ideal
+fn to_p_coord(
+ arr: &[u8; COUNT],
+) -> u16 {
+ (0..UPPER - LOWER).rev().fold(0, |acc, idx| {
+ (acc * (idx + 1) as u16)
+ + arr[LOWER..LOWER + idx]
+ .iter()
+ .filter(|&&x| x > arr[LOWER + idx])
+ .count() as u16
+ })
+}
+
+fn set_p_coord(
+ mut c: usize,
+ perm: &mut [P; COUNT],
+ mut bag: Vec,
+) {
+ let mut n = [0; COUNT];
+ for (i, n) in n.iter_mut().enumerate() {
+ *n = c % (i + 1);
+ c /= i + 1;
+ }
+
+ for i in (LOWER..=UPPER).rev() {
+ let index = n[i - LOWER];
+ perm[i] = bag[index];
+ bag.remove(index);
+ }
+}
+
+/// Coordinate for positions of E slice edges (ignoring what the edges actually are)
+#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Copy, Clone, Hash)]
+pub struct ESliceEdgeCoord(u16);
+
+/// Coordinate for positions of U/D layer edges, assuming the cube is in and says in domino
+/// reduction.
+#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Copy, Clone, Hash)]
+pub struct DominoEPCoord(u16);
+
+/// Coordinate for positions of the E slice edges, assuming the cube is in and says in domino
+/// reduction.
+#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Copy, Clone, Hash)]
+pub struct DominoESliceCoord(u16);
+
+impl Coordinate for ESliceEdgeCoord {
+ fn from_puzzle(puzzle: &CubieCube) -> Self {
+ // https://kociemba.org/math/UDSliceCoord.htm
+ let mut r = 0;
+
+ // c is meant to be n choose (k-1) if k >= 1
+ let mut k = 0;
+ let mut c = 0u16;
+
+ for n in 0..12 {
+ if puzzle.ep[n as usize].e_slice() {
+ // every time we reach an e slice edge, we increment k, and then have to fix the
+ // value of c.
+ k += 1;
+ if k == 1 {
+ // if k was previously zero, c was 0, so just set c to be the next n value
+ c = 1;
+ } else {
+ // Otherwise, c was previously equal to n choose k-1, and we want to update it
+ // to be n+1 choose k. To do this we can use the identity
+ // n choose k = (n/k) * (n-1 choose k-1)
+ // we have to divide at the end do dodge floor division
+ debug_assert!((c * (n + 1)).is_multiple_of(k - 1));
+ c = c * (n + 1) / (k - 1);
+ }
+ } else if k > 0 {
+ r += c;
+ // In this case we want to update n choose k-1 to be n+1 choose k-1. To do this we
+ // can use the identity
+ // (n choose k) = (n/(n-k)) (n-1 choose k)
+ debug_assert!((c * (n + 1)).is_multiple_of(n + 1 - k + 1));
+ c = c * (n + 1) / (n + 1 - k + 1);
+ }
+ }
+
+ ESliceEdgeCoord(r)
+ }
+
+ fn count() -> usize {
+ 495
+ }
+
+ fn repr(self) -> usize {
+ self.0 as usize
+ }
+
+ fn from_repr(n: usize) -> Self {
+ ESliceEdgeCoord(n as u16)
+ }
+}
+
+fn binom(n: usize, k: usize) -> usize {
+ (n - k + 1..=n).product::() / (1..=k).product::()
+}
+
+impl FromCoordinate for CubieCube {
+ fn set_coord(&mut self, coord: ESliceEdgeCoord) {
+ self.ep = CubieCube::SOLVED.ep;
+ // Identify the locations of each e slice edge.
+ let mut c = coord.repr();
+ let mut poses = [0; 4];
+ let (mut n, mut k) = (11, 3);
+ let mut p = 0;
+ for i in (0..12).rev() {
+ let b = binom(n, k);
+ if b > c {
+ poses[p] = i;
+ p += 1;
+ if k == 0 {
+ break;
+ }
+ k -= 1;
+ } else {
+ c -= b;
+ }
+ n -= 1;
+ }
+ // swap e slice edges (8..=11 is the e slice edge positions)
+ for (a, b) in poses.into_iter().rev().zip(8..=11) {
+ self.ep.swap(a, b);
+ }
+ }
+}
+
+impl Coordinate for DominoEPCoord {
+ fn from_puzzle(puzzle: &CubieCube) -> Self {
+ DominoEPCoord(to_p_coord::<12, 0, 8>(&puzzle.ep.map(|n| n.into())))
+ }
+
+ fn count() -> usize {
+ 40320
+ }
+
+ fn repr(self) -> usize {
+ self.0 as usize
+ }
+
+ fn from_repr(n: usize) -> Self {
+ DominoEPCoord(n as u16)
+ }
+}
+
+impl FromCoordinate for CubieCube {
+ fn set_coord(&mut self, coord: DominoEPCoord) {
+ // UHHH i hope this is fine...
+ // self.ep = CubieCube::SOLVED.ep;
+
+ use crate::cube333::edge::Edge as E;
+ let bag = vec![E::DR, E::DB, E::DL, E::DF, E::UR, E::UB, E::UL, E::UF];
+
+ set_p_coord::<12, 0, 7, E>(coord.repr(), &mut self.ep, bag);
+ }
+}
+
+impl Coordinate for DominoESliceCoord {
+ fn from_puzzle(puzzle: &CubieCube) -> Self {
+ DominoESliceCoord(to_p_coord::<12, 8, 12>(&puzzle.ep.map(|n| n.into())))
+ }
+
+ fn count() -> usize {
+ 24
+ }
+
+ fn repr(self) -> usize {
+ self.0 as usize
+ }
+
+ fn from_repr(n: usize) -> Self {
+ DominoESliceCoord(n as u16)
+ }
+}
+
+impl FromCoordinate for CubieCube {
+ fn set_coord(&mut self, coord: DominoESliceCoord) {
+ //self.ep = CubieCube::SOLVED.ep;
+
+ use crate::cube333::edge::Edge as E;
+ let bag = vec![E::BR, E::BL, E::FL, E::FR];
+
+ set_p_coord::<12, 8, 11, E>(coord.repr(), &mut self.ep, bag);
+ }
+}
+
+/// A symmetry coordinate over a `Symmetry`. A SymCoordinate should include both what equivalence
+/// class the coordinate is in, along with the symmetry it has from the representant.
+pub trait SymCoordinate: Copy + Default + Eq {
+ type Sym: Symmetry;
+ type Raw: Coordinate;
+
+ /// The number of equivalence classes this coordinate encodes modulo symmetry.
+ fn classes() -> usize;
+
+ /// Determine if the given coordinate represents a solved state
+ fn solved(self) -> bool {
+ self.class() == 0
+ }
+
+ /// A representation of this coordinate as a usize, for use, in table lookups.
+ fn repr(self) -> (usize, Self::Sym) {
+ (self.class(), self.sym())
+ }
+
+ /// Convert the representation of a coordinate to the coordinate itself. We assume 0 with the
+ /// identity symmetry corresponds to the solved state.
+ fn from_repr(idx: usize, sym: Self::Sym) -> Self;
+
+ /// Obtain the equivalence class this coordinate represents.
+ fn class(self) -> usize;
+
+ /// Obtain the underlying symmetry of this ooordinate.
+ fn sym(self) -> Self::Sym;
+}
+
+pub struct RawSymTable
+where
+ CubieCube: FromCoordinate,
+{
+ raw_to_sym: Box<[S]>,
+ class_to_repr: Box<[S::Raw]>,
+}
+
+impl RawSymTable
+where
+ CubieCube: FromCoordinate,
+{
+ pub fn generate() -> Self {
+ let mut raw_to_sym = vec![S::default(); S::Raw::count()].into_boxed_slice();
+ let mut class_to_repr = vec![S::Raw::default(); S::classes()].into_boxed_slice();
+
+ let mut sym_idx = 0;
+
+ for raw in (0..S::Raw::count()).map(S::Raw::from_repr) {
+ // Skip entries we have already initialised (note that states symmetric to the solved
+ // state will not have solved SymCoordinate since a SymCoordinate will include its
+ // symmetry)
+ if raw_to_sym[raw.repr()] != S::default() {
+ continue;
+ }
+
+ let mut c = CubieCube::SOLVED;
+ c.set_coord(raw);
+
+ // Then we go over every coordinate symmetric to this one, and update the tables based
+ // on them.
+ for sym in S::Sym::get_all() {
+ let d = c.clone().conjugate_symmetry(sym);
+ let raw2 = S::Raw::from_puzzle(&d);
+ raw_to_sym[raw2.repr()] = S::from_repr(sym_idx, sym);
+ }
+
+ class_to_repr[sym_idx] = raw;
+ sym_idx += 1;
+ }
+
+ RawSymTable {
+ raw_to_sym,
+ class_to_repr,
+ }
+ }
+
+ pub fn raw_to_sym(&self, raw: S::Raw) -> S {
+ self.raw_to_sym[raw.repr()]
+ }
+
+ pub fn index_to_repr(&self, idx: usize) -> S::Raw {
+ self.class_to_repr[idx]
+ }
+
+ pub fn puzzle_to_sym(&self, p: &CubieCube) -> S {
+ self.raw_to_sym(S::Raw::from_puzzle(p))
+ }
+
+ #[cfg(test)]
+ pub fn sym_to_raw(&self, sym: S) -> S::Raw {
+ let repr = self.index_to_repr(sym.class());
+ let mut c = CubieCube::SOLVED;
+ c.set_coord(repr);
+ let d = c.conjugate_symmetry(sym.sym());
+ S::Raw::from_puzzle(&d)
+ }
+}
+
+#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Copy, Clone, Hash)]
+pub struct COSymCoord(u16);
+
+impl SymCoordinate for COSymCoord {
+ type Sym = HalfSymmetry;
+
+ type Raw = COCoord;
+
+ fn classes() -> usize {
+ 324
+ }
+
+ fn from_repr(idx: usize, sym: Self::Sym) -> Self {
+ COSymCoord((idx as u16) << 3 | (sym.repr() as u16))
+ }
+
+ fn class(self) -> usize {
+ (self.0 >> 3) as usize
+ }
+
+ fn sym(self) -> Self::Sym {
+ HalfSymmetry::from_repr((self.0 & 7) as usize)
+ }
+}
+
+#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Copy, Clone, Hash)]
+pub struct EOSymCoord(u16);
+
+impl SymCoordinate for EOSymCoord {
+ type Sym = HalfSymmetry;
+
+ type Raw = EOCoord;
+
+ fn classes() -> usize {
+ 336
+ }
+
+ fn from_repr(idx: usize, sym: Self::Sym) -> Self {
+ EOSymCoord((idx as u16) << 3 | (sym.repr() as u16))
+ }
+
+ fn class(self) -> usize {
+ (self.0 >> 3) as usize
+ }
+
+ fn sym(self) -> Self::Sym {
+ HalfSymmetry::from_repr((self.0 & 7) as usize)
+ }
+}
+
+#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Copy, Clone, Hash)]
+pub struct CPSymCoord(u16);
+
+impl SymCoordinate for CPSymCoord {
+ type Sym = DrSymmetry;
+
+ type Raw = CPCoord;
+
+ fn classes() -> usize {
+ 2768
+ }
+
+ fn from_repr(idx: usize, sym: Self::Sym) -> Self {
+ CPSymCoord((idx as u16) << 4 | (sym.repr() as u16))
+ }
+
+ fn class(self) -> usize {
+ (self.0 >> 4) as usize
+ }
+
+ fn sym(self) -> Self::Sym {
+ DrSymmetry::from_repr((self.0 & 15) as usize)
+ }
+}
+
+#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Copy, Clone, Hash)]
+pub struct DominoEPSymCoord(u16);
+
+impl SymCoordinate for DominoEPSymCoord {
+ type Sym = DrSymmetry;
+
+ type Raw = DominoEPCoord;
+
+ fn classes() -> usize {
+ 2768
+ }
+
+ fn from_repr(idx: usize, sym: Self::Sym) -> Self {
+ DominoEPSymCoord((idx as u16) << 4 | (sym.repr() as u16))
+ }
+
+ fn class(self) -> usize {
+ (self.0 >> 4) as usize
+ }
+
+ fn sym(self) -> Self::Sym {
+ DrSymmetry::from_repr((self.0 & 15) as usize)
+ }
+}
+
+#[cfg(test)]
+mod test {
+ use std::collections::HashSet;
+
+ use itertools::Itertools;
+
+ use super::super::move_tables::{DrMove, SubMove};
+ use super::*;
+ use crate::{
+ coord::Coordinate,
+ cube333::{CubieCube, moves::Move333},
+ moves::MoveSequence,
+ };
+
+ use proptest::collection::vec;
+ use proptest::prelude::*;
+
+ fn sets_coord + std::fmt::Debug>(mvs: MoveSequence)
+ where
+ CubieCube: FromCoordinate,
+ {
+ let coord = C::from_puzzle(&CubieCube::SOLVED.make_moves(mvs));
+ let mut d = CubieCube::SOLVED;
+ d.set_coord(coord);
+ assert_eq!(C::from_puzzle(&d), coord);
+ }
+
+ #[test]
+ fn from_coord() {
+ proptest!(|(mvs in vec(any::(), 0..20).prop_map(MoveSequence))| {
+ sets_coord::(mvs);
+ });
+ }
+
+ #[test]
+ fn from_coord_dr() {
+ proptest!(|(mvs in vec(any::(), 0..20).prop_map(|v| v.into_iter().map(SubMove::into_move).collect()).prop_map(MoveSequence))| {
+ sets_coord::(mvs.clone());
+ sets_coord::(mvs);
+ });
+ }
+
+ #[test]
+ fn e_slice_edge_uniqueness() {
+ let mut coords = HashSet::new();
+ for poses in (0..12).combinations(4) {
+ let mut cube = CubieCube::SOLVED;
+
+ for (a, b) in poses.into_iter().zip(8..12) {
+ cube.ep.swap(a, b);
+ }
+
+ let coord = ESliceEdgeCoord::from_puzzle(&cube);
+ assert!(!coords.contains(&coord));
+ coords.insert(coord);
+ }
+ assert!(coords.len() == ESliceEdgeCoord::count());
+ assert!(coords.iter().all(|c| c.repr() < ESliceEdgeCoord::count()));
+ }
+
+ #[test]
+ fn sym_table_generates() {
+ RawSymTable::::generate();
+ RawSymTable::::generate();
+ RawSymTable::::generate();
+ RawSymTable::::generate();
+ }
+
+ #[test]
+ fn sym_class_repr_in_class() {
+ let co_sym = RawSymTable::::generate();
+ let eo_sym = RawSymTable::::generate();
+ proptest!(|(mvs in vec(any::(), 0..20).prop_map(MoveSequence))| {
+ let c = CubieCube::SOLVED.make_moves(mvs);
+ let co = COCoord::from_puzzle(&c);
+ assert_eq!(co_sym.raw_to_sym(co_sym.index_to_repr(co_sym.raw_to_sym(co).class())).class(), co_sym.raw_to_sym(co).class());
+ let eo = EOCoord::from_puzzle(&c);
+ assert_eq!(eo_sym.raw_to_sym(eo_sym.index_to_repr(eo_sym.raw_to_sym(eo).class())).class(), eo_sym.raw_to_sym(eo).class());
+ });
+ let cp_sym = RawSymTable::::generate();
+ let ep_sym = RawSymTable::::generate();
+ proptest!(|(mvs in vec(any::(), 0..20).prop_map(|v| v.into_iter().map(SubMove::into_move).collect()).prop_map(MoveSequence))| {
+ let c = CubieCube::SOLVED.make_moves(mvs);
+ let cp = CPCoord::from_puzzle(&c);
+ assert_eq!(cp_sym.raw_to_sym(cp_sym.index_to_repr(cp_sym.raw_to_sym(cp).class())).class(), cp_sym.raw_to_sym(cp).class());
+ let ep = DominoEPCoord::from_puzzle(&c);
+ assert_eq!(ep_sym.raw_to_sym(ep_sym.index_to_repr(ep_sym.raw_to_sym(ep).class())).class(), ep_sym.raw_to_sym(ep).class());
+ });
+ }
+
+ fn raw_to_sym_right_inverse()
+ where
+ CubieCube: FromCoordinate,
+ S::Raw: std::fmt::Debug,
+ {
+ let table = RawSymTable::::generate();
+ for raw in (0..S::Raw::count()).map(S::Raw::from_repr) {
+ assert_eq!(raw, table.sym_to_raw(table.raw_to_sym(raw)));
+ }
+ }
+
+ #[test]
+ fn raw_to_sym_right_inverse_all() {
+ raw_to_sym_right_inverse::();
+ raw_to_sym_right_inverse::();
+ raw_to_sym_right_inverse::();
+ raw_to_sym_right_inverse::();
+ }
+}
diff --git a/src/cube333/two_phase_solver/mod.rs b/src/cube333/two_phase_solver/mod.rs
new file mode 100644
index 0000000..bc0bf69
--- /dev/null
+++ b/src/cube333/two_phase_solver/mod.rs
@@ -0,0 +1,356 @@
+//! An implementation of the two phase solver described [here](https://kociemba.org/cube.htm).
+
+mod coords;
+mod move_tables;
+mod prune;
+mod symmetry;
+
+use coords::SymCoordinate;
+use move_tables::{DrMove, SubMove};
+
+use super::{CubieCube, moves::Move333};
+use crate::coord::Coordinate;
+use crate::moves::MoveSequence;
+
+use std::rc::Rc;
+
+struct Phase1;
+struct Phase2;
+
+/// Organisation trait for each phase
+trait Phase {
+ type Cube: PhaseCube;
+ type Move: SubMove;
+ type Prune: PhasePrune;
+
+ /// Convert a cubiecube into a phase encoding cube
+ fn get_cube(sym_tables: &SymTables, cube: &CubieCube) -> Self::Cube;
+
+ /// Apply a move to a cube
+ fn make_move(mover: &Mover, cube: Self::Cube, m: Self::Move) -> Self::Cube;
+
+ /// Get the initial pruning value of a cube
+ fn init_prune(pruner: &Pruner, c: Self::Cube) -> Self::Prune;
+
+ /// Update a pruning value based on a next state
+ fn update_prune(pruner: &Pruner, p: Self::Prune, c: Self::Cube) -> Self::Prune;
+}
+
+impl Phase for Phase1 {
+ type Cube = P1Cube;
+ type Move = Move333;
+ type Prune = P1PruneState;
+
+ fn get_cube(sym_tables: &SymTables, c: &CubieCube) -> P1Cube {
+ P1Cube {
+ co: sym_tables.co.puzzle_to_sym(c),
+ eo: sym_tables.eo.puzzle_to_sym(c),
+ slice: coords::ESliceEdgeCoord::from_puzzle(c),
+ }
+ }
+
+ fn make_move(mover: &Mover, cube: P1Cube, m: Move333) -> P1Cube {
+ P1Cube {
+ co: mover.p1_co.make_move(cube.co, m),
+ eo: mover.p1_eo.make_move(cube.eo, m),
+ slice: mover.p1_slice.make_move(cube.slice, m),
+ }
+ }
+
+ fn init_prune(pruner: &Pruner, c: P1Cube) -> P1PruneState {
+ P1PruneState {
+ co_slice: pruner.p1_co.bound(c.co, c.slice),
+ eo_slice: pruner.p1_eo.bound(c.eo, c.slice),
+ }
+ }
+
+ fn update_prune(pruner: &Pruner, p: P1PruneState, c: P1Cube) -> P1PruneState {
+ P1PruneState {
+ co_slice: pruner.p1_co.update(p.co_slice, c.co, c.slice),
+ eo_slice: pruner.p1_eo.update(p.eo_slice, c.eo, c.slice),
+ }
+ }
+}
+
+impl Phase for Phase2 {
+ type Cube = P2Cube;
+ type Move = DrMove;
+ type Prune = P2PruneState;
+
+ fn get_cube(sym_tables: &SymTables, c: &CubieCube) -> P2Cube {
+ P2Cube {
+ cp: sym_tables.cp.puzzle_to_sym(c),
+ ep: sym_tables.ep.puzzle_to_sym(c),
+ slice: coords::DominoESliceCoord::from_puzzle(c),
+ }
+ }
+
+ fn make_move(mover: &Mover, cube: P2Cube, m: DrMove) -> P2Cube {
+ P2Cube {
+ cp: mover.p2_cp.make_move(cube.cp, m),
+ ep: mover.p2_ep.make_move(cube.ep, m),
+ slice: mover.p2_slice.make_move(cube.slice, m),
+ }
+ }
+
+ fn init_prune(pruner: &Pruner, c: P2Cube) -> P2PruneState {
+ P2PruneState {
+ cp_slice: pruner.p2_cp.bound(c.cp, c.slice),
+ ep_slice: pruner.p2_ep.bound(c.ep, c.slice),
+ }
+ }
+
+ fn update_prune(pruner: &Pruner, p: P2PruneState, c: P2Cube) -> P2PruneState {
+ P2PruneState {
+ cp_slice: pruner.p2_cp.update(p.cp_slice, c.cp, c.slice),
+ ep_slice: pruner.p2_ep.update(p.ep_slice, c.ep, c.slice),
+ }
+ }
+}
+
+trait PhaseCube: Copy {
+ fn is_solved(self) -> bool;
+}
+
+#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Copy, Clone, Hash)]
+struct P1Cube {
+ co: coords::COSymCoord,
+ eo: coords::EOSymCoord,
+ slice: coords::ESliceEdgeCoord,
+}
+
+#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Copy, Clone, Hash)]
+struct P2Cube {
+ cp: coords::CPSymCoord,
+ ep: coords::DominoEPSymCoord,
+ slice: coords::DominoESliceCoord,
+}
+
+impl PhaseCube for P1Cube {
+ fn is_solved(self) -> bool {
+ self.co.solved() && self.eo.solved() && self.slice.solved()
+ }
+}
+
+impl PhaseCube for P2Cube {
+ fn is_solved(self) -> bool {
+ self.cp.solved() && self.ep.solved() && self.slice.solved()
+ }
+}
+
+trait PhasePrune: Copy {
+ fn val(self) -> usize;
+}
+
+#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Copy, Clone, Hash)]
+struct P1PruneState {
+ co_slice: usize,
+ eo_slice: usize,
+}
+
+#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Copy, Clone, Hash)]
+struct P2PruneState {
+ cp_slice: usize,
+ ep_slice: usize,
+}
+
+impl PhasePrune for P1PruneState {
+ fn val(self) -> usize {
+ self.co_slice.max(self.eo_slice)
+ }
+}
+
+impl PhasePrune for P2PruneState {
+ fn val(self) -> usize {
+ self.cp_slice.max(self.ep_slice)
+ }
+}
+
+struct Mover {
+ p1_co: Rc,
+ p1_eo: Rc,
+ p1_slice: Rc,
+ p2_cp: Rc,
+ p2_ep: Rc,
+ p2_slice: Rc,
+}
+
+struct Pruner {
+ p1_co: prune::ESliceTwistPruneTable,
+ p1_eo: prune::ESliceFlipPruneTable,
+ p2_cp: prune::DominoSliceCPPruneTable,
+ p2_ep: prune::DominoSliceEPPruneTable,
+}
+
+struct SymTables {
+ co: Rc>,
+ eo: Rc>,
+ cp: Rc>,
+ ep: Rc>,
+}
+
+/// A cube solver that uses Kociemba's two phase algorithm.
+pub struct Solver {
+ mover: Mover,
+ pruner: Pruner,
+ sym_tables: SymTables,
+}
+
+impl Default for Solver {
+ fn default() -> Self {
+ Self::new()
+ }
+}
+
+enum SearchResult {
+ Found,
+ Bound(usize),
+}
+
+impl Solver {
+ /// Create a solver.
+ pub fn new() -> Self {
+ // this is so ugly...
+ let co_sym = Rc::new(coords::RawSymTable::generate());
+ let eo_sym = Rc::new(coords::RawSymTable::generate());
+ let cp_sym = Rc::new(coords::RawSymTable::generate());
+ let ep_sym = Rc::new(coords::RawSymTable::generate());
+ let p1_co = Rc::new(move_tables::SymMoveTable::generate(&co_sym));
+ let p1_eo = Rc::new(move_tables::SymMoveTable::generate(&eo_sym));
+ let p1_slice = Rc::new(move_tables::MoveTable::generate());
+ let p2_cp = Rc::new(move_tables::SymMoveTable::generate(&cp_sym));
+ let p2_ep = Rc::new(move_tables::SymMoveTable::generate(&ep_sym));
+ let p2_slice = Rc::new(move_tables::MoveTable::generate());
+
+ let mover = Mover {
+ p1_co,
+ p1_eo,
+ p1_slice,
+ p2_cp,
+ p2_ep,
+ p2_slice,
+ };
+
+ let p1_co = prune::SymRawPruningTable::generate(
+ co_sym.clone(),
+ mover.p1_co.clone(),
+ mover.p1_slice.clone(),
+ );
+ let p1_eo = prune::SymRawPruningTable::generate(
+ eo_sym.clone(),
+ mover.p1_eo.clone(),
+ mover.p1_slice.clone(),
+ );
+ let p2_cp = prune::SymRawPruningTable::generate(
+ cp_sym.clone(),
+ mover.p2_cp.clone(),
+ mover.p2_slice.clone(),
+ );
+ let p2_ep = prune::SymRawPruningTable::generate(
+ ep_sym.clone(),
+ mover.p2_ep.clone(),
+ mover.p2_slice.clone(),
+ );
+
+ let pruner = Pruner {
+ p1_co,
+ p1_eo,
+ p2_cp,
+ p2_ep,
+ };
+
+ let sym_tables = SymTables {
+ co: co_sym,
+ eo: eo_sym,
+ cp: cp_sym,
+ ep: ep_sym,
+ };
+
+ Self {
+ mover,
+ pruner,
+ sym_tables,
+ }
+ }
+
+ /// Obtain a solving sequence for the cube (such that applying the sequence solves the cube).
+ pub fn solve(&self, cube: CubieCube) -> MoveSequence {
+ let p1 = self.solve_phase::(&cube);
+ let cube = cube.make_moves(MoveSequence(p1.clone()));
+ let mut p2 = self.solve_phase::(&cube);
+
+ let mut sol = p1;
+ sol.append(&mut p2);
+ MoveSequence(sol).cancel()
+ }
+
+ fn solve_phase(&self, cube: &CubieCube) -> Vec {
+ // This is just ida*! go read about it yourself!! grrrrrr
+ let cube = P::get_cube(&self.sym_tables, cube);
+ let prune = P::init_prune(&self.pruner, cube);
+ let mut sol = Vec::new();
+
+ let mut depth = 0;
+ while depth < 20 {
+ match self.search_phase::(cube, prune, &mut sol, 0, depth) {
+ SearchResult::Found => return sol.into_iter().map(|m| m.into_move()).collect(),
+ SearchResult::Bound(d) => depth = d,
+ }
+ }
+
+ panic!("No phase solution found in 20 moves")
+ }
+
+ fn search_phase(
+ &self,
+ cube: P::Cube,
+ prune: P::Prune,
+ sol: &mut Vec,
+ cost: usize,
+ depth: usize,
+ ) -> SearchResult {
+ if cube.is_solved() {
+ return SearchResult::Found;
+ }
+ let estimate = cost + prune.val();
+ if estimate > depth {
+ return SearchResult::Bound(estimate);
+ }
+ let mut min = usize::MAX;
+ let last = sol.last().copied();
+ for &m in P::Move::MOVE_LIST.iter().filter(|&&m| {
+ last.is_none_or(|l| l.axis() != m.axis() && l.axis() != m.axis().opposite())
+ }) {
+ let cube2 = P::make_move(&self.mover, cube, m);
+ let prune2 = P::update_prune(&self.pruner, prune, cube2);
+ sol.push(m);
+
+ match self.search_phase::(cube2, prune2, sol, cost + 1, depth) {
+ SearchResult::Found => return SearchResult::Found,
+ SearchResult::Bound(depth) => min = min.min(depth),
+ }
+
+ sol.pop();
+ }
+
+ SearchResult::Bound(min)
+ }
+}
+
+#[cfg(test)]
+mod test {
+ use super::*;
+
+ use proptest::collection::vec;
+ use proptest::prelude::*;
+
+ #[test]
+ fn solve() {
+ let solver = Solver::new();
+ proptest!(|(mvs in vec(any::(), 0..20).prop_map(MoveSequence))| {
+ let cube = CubieCube::SOLVED.make_moves(mvs.clone());
+ let sol = solver.solve(cube.clone());
+ assert_eq!(cube.make_moves(sol), CubieCube::SOLVED);
+ });
+ }
+}
diff --git a/src/cube333/two_phase_solver/move_tables.rs b/src/cube333/two_phase_solver/move_tables.rs
new file mode 100644
index 0000000..880000e
--- /dev/null
+++ b/src/cube333/two_phase_solver/move_tables.rs
@@ -0,0 +1,395 @@
+//! Move tables for each coordinate type
+
+use crate::coord::{Coordinate, FromCoordinate};
+use crate::cube333::CubieCube;
+use crate::cube333::moves::{Move333, Move333Type};
+#[cfg(test)]
+use crate::moves::MoveSequence;
+use crate::moves::{Cancellation, Move};
+
+use super::coords::{
+ COSymCoord, CPSymCoord, DominoEPSymCoord, DominoESliceCoord, EOSymCoord, ESliceEdgeCoord,
+ RawSymTable, SymCoordinate,
+};
+use super::symmetry::{SymMoveConjTable, SymMultTable};
+
+use std::marker::PhantomData;
+
+#[cfg(test)]
+use proptest::strategy::Strategy;
+#[cfg(test)]
+use proptest_derive::Arbitrary;
+
+// TODO This may be generalised later, but for now it'll be specialised to just `CubieCube`
+
+/// A type that encodes a subset of the set of 3x3 moves, e.g. DR moves.
+pub trait SubMove: Move + Copy
+where
+ Self: 'static,
+{
+ /// Interpret a move as a normal move to be applied to a `CubieCube`.
+ fn into_move(self) -> Move333;
+
+ fn axis(self) -> Move333Type {
+ self.into_move().ty
+ }
+
+ /// The list of all moves that this type encodes. The length of the returned vector should be
+ /// `count()`.
+ const MOVE_LIST: &'static [Self];
+
+ /// Returns all of the states that come from applying each move to the given puzzle, along with
+ /// the given move.
+ fn successor_states(puzzle: CubieCube) -> impl Iterator- {
+ Self::MOVE_LIST
+ .iter()
+ .map(move |m| (*m, puzzle.clone().make_move(m.into_move())))
+ }
+
+ /// Get the index of this move in the move list.
+ fn index(self) -> usize;
+}
+
+/// A move table, which stores mappings of coordinate + move pairs to the coordinate that results
+/// from applying the move.
+#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
+pub struct MoveTable, const MOVES: usize> {
+ table: Box<[[C; MOVES]]>,
+ _phantom: PhantomData,
+}
+
+impl, const MOVES: usize> MoveTable {
+ /// Generate a move table. This is slightly expensive so making move tables repeatedly should
+ /// be avoided, since the resulting move table generated will always be identical.
+ pub fn generate() -> Self {
+ let mut visited = vec![false; C::count()];
+ let mut stack = vec![CubieCube::SOLVED];
+ visited[0] = true;
+
+ let mut table: Box<[[C; MOVES]]> =
+ vec![std::array::from_fn(|_| Default::default()); C::count()].into_boxed_slice();
+
+ while let Some(cur_cube) = stack.pop() {
+ let c = C::from_puzzle(&cur_cube);
+ for (mv, next) in M::successor_states(cur_cube) {
+ let c2 = C::from_puzzle(&next);
+
+ table[c.repr()][mv.index()] = c2;
+
+ if !visited[c2.repr()] {
+ visited[c2.repr()] = true;
+ stack.push(next);
+ }
+ }
+ }
+
+ debug_assert!(visited.into_iter().all(|b| b));
+
+ Self {
+ table,
+ _phantom: PhantomData,
+ }
+ }
+
+ /// Determine what coordinate comes from applying a move.
+ pub fn make_move(&self, coord: C, mv: M) -> C {
+ self.table[coord.repr()][mv.index()]
+ }
+
+ #[cfg(test)]
+ /// Determine what coordinate comes from applying a sequence of moves.
+ pub fn make_moves(&self, coord: C, alg: MoveSequence) -> C {
+ alg.0.into_iter().fold(coord, |c, m| self.make_move(c, m))
+ }
+}
+
+/// A move table working over a symmetry coordinate. See `MoveTable`.
+///
+/// Symmetry move tables only store one coordinate to move mapping per symmetry class, instead of
+/// per coordinate. This makes it more compressed than a normal raw move table. The tradeoff is
+/// that there is a very minor performance hit when computing moves as we need to adjust for
+/// symmetry differences.
+///
+/// In particular, if we have a symmetry coordinate S P S^-1, and we want to apply a move M to it,
+/// notice that S P S^-1 M = S P S^-1 M S S^-1 = S (P S^-1 M S) S^-1. Hence, if we know P M' (where
+/// M' = S^-1 M S) to be S' Q S'^-1, we can compute S P S^-1 M to be S S' Q S'^-1 S^-1.
+pub struct SymMoveTable
+where
+ CubieCube: FromCoordinate,
+{
+ table: Box<[[S; MOVES]]>,
+ sym_mult_table: SymMultTable,
+ sym_move_conj_table: SymMoveConjTable,
+ _phantom: PhantomData,
+}
+
+impl
+ SymMoveTable
+where
+ CubieCube: FromCoordinate,
+{
+ /// Generate a move table. This is slightly expensive so making move tables repeatedly should
+ /// be avoided, since the resulting move table generated will always be identical.
+ pub fn generate(sym_table: &RawSymTable
) -> Self {
+ let table: Box<[[S; MOVES]]> = (0..S::classes())
+ .map(|i| {
+ let raw = sym_table.index_to_repr(i);
+ let mut c = CubieCube::SOLVED;
+ c.set_coord(raw);
+
+ let mut t: [S; MOVES] = std::array::from_fn(|_| Default::default());
+
+ for (mv, next) in M::successor_states(c) {
+ t[mv.index()] = sym_table.raw_to_sym(S::Raw::from_puzzle(&next));
+ }
+
+ t
+ })
+ .collect::>()
+ .into_boxed_slice();
+ let sym_mult_table = SymMultTable::generate();
+ let sym_move_conj_table = SymMoveConjTable::generate();
+
+ SymMoveTable {
+ table,
+ sym_mult_table,
+ sym_move_conj_table,
+ _phantom: PhantomData,
+ }
+ }
+
+ /// Determine what coordinate comes from applying a move.
+ pub fn make_move(&self, coord: S, mv: M) -> S {
+ let (idx, sym1) = coord.repr();
+ let mv = self.sym_move_conj_table.conjugate(mv, sym1);
+ let (idx, sym2) = self.table[idx][mv.index()].repr();
+ let sym = self.sym_mult_table.multiply(sym1, sym2);
+
+ S::from_repr(idx, sym)
+ }
+
+ #[cfg(test)]
+ /// Determine what coordinate comes from applying a sequence of moves.
+ pub fn make_moves(&self, coord: S, alg: MoveSequence) -> S {
+ alg.0.into_iter().fold(coord, |c, m| self.make_move(c, m))
+ }
+}
+
+use crate::cube333::moves::MoveGenerator;
+impl SubMove for Move333 {
+ fn into_move(self) -> Move333 {
+ self
+ }
+
+ const MOVE_LIST: &'static [Move333] = crate::cube333::moves::Htm::MOVE_LIST;
+
+ fn index(self) -> usize {
+ self.into()
+ }
+}
+
+// TODO proptest DrMoves preserve phase 2
+
+/// A move in domino reduction (phase 2).
+#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
+#[cfg_attr(test, derive(Arbitrary))]
+pub enum DrMove {
+ R2,
+ L2,
+ F2,
+ B2,
+ #[cfg_attr(test, proptest(strategy = "(1..=3u8).prop_map(DrMove::U)", weight = 3))]
+ U(u8),
+ #[cfg_attr(test, proptest(strategy = "(1..=3u8).prop_map(DrMove::D)", weight = 3))]
+ D(u8),
+}
+
+impl Move for DrMove {
+ fn inverse(self) -> Self {
+ match self {
+ DrMove::U(n) => DrMove::U(4u8.wrapping_sub(n).rem_euclid(4)),
+ DrMove::D(n) => DrMove::D(4u8.wrapping_sub(n).rem_euclid(4)),
+ _ => self,
+ }
+ }
+
+ fn commutes_with(&self, b: &Self) -> bool {
+ use DrMove as M;
+ match self {
+ M::R2 | M::L2 => matches!(b, M::R2 | M::L2),
+ M::F2 | M::B2 => matches!(b, M::F2 | M::B2),
+ M::U(_) | M::D(_) => matches!(b, M::U(_) | M::D(_)),
+ }
+ }
+
+ fn cancel(self, b: Self) -> Cancellation {
+ use DrMove as M;
+ match (self, b) {
+ (M::R2, M::R2) => Cancellation::NoMove,
+ (M::L2, M::L2) => Cancellation::NoMove,
+ (M::F2, M::F2) => Cancellation::NoMove,
+ (M::U(n), M::U(m)) if (n + m).is_multiple_of(4) => Cancellation::NoMove,
+ (M::D(n), M::D(m)) if (n + m).is_multiple_of(4) => Cancellation::NoMove,
+ (M::U(n), M::U(m)) => Cancellation::OneMove(M::U((n + m) % 4)),
+ (M::D(n), M::D(m)) => Cancellation::OneMove(M::D((n + m) % 4)),
+ (M::B2, M::B2) => Cancellation::NoMove,
+ _ => Cancellation::TwoMove(self, b),
+ }
+ }
+}
+
+impl SubMove for DrMove {
+ fn into_move(self) -> Move333 {
+ use crate::mv;
+ match self {
+ DrMove::R2 => mv!(R, 2),
+ DrMove::L2 => mv!(L, 2),
+ DrMove::F2 => mv!(F, 2),
+ DrMove::B2 => mv!(B, 2),
+ DrMove::U(n) => mv!(U, n),
+ DrMove::D(n) => mv!(D, n),
+ }
+ }
+
+ fn axis(self) -> Move333Type {
+ match self {
+ DrMove::R2 => Move333Type::R,
+ DrMove::L2 => Move333Type::L,
+ DrMove::F2 => Move333Type::F,
+ DrMove::B2 => Move333Type::B,
+ DrMove::U(_) => Move333Type::U,
+ DrMove::D(_) => Move333Type::D,
+ }
+ }
+
+ const MOVE_LIST: &'static [DrMove] = &[
+ DrMove::R2,
+ DrMove::L2,
+ DrMove::F2,
+ DrMove::B2,
+ DrMove::U(1),
+ DrMove::U(2),
+ DrMove::U(3),
+ DrMove::D(1),
+ DrMove::D(2),
+ DrMove::D(3),
+ ];
+
+ fn index(self) -> usize {
+ match self {
+ DrMove::R2 => 0,
+ DrMove::L2 => 1,
+ DrMove::F2 => 2,
+ DrMove::B2 => 3,
+ // Technically this mod is unnecessary if the invariant that n is always in 1..=3
+ // holds! But that's unsatisfying
+ DrMove::U(n) => 3 + (n % 4) as usize,
+ DrMove::D(n) => 6 + (n % 4) as usize,
+ }
+ }
+}
+
+pub type COSymMoveTable = SymMoveTable;
+pub type EOSymMoveTable = SymMoveTable;
+pub type ESliceEdgeMoveTable = MoveTable;
+pub type DominoCPSymMoveTable = SymMoveTable;
+pub type DominoEPSymMoveTable = SymMoveTable;
+pub type DominoESliceMoveTable = MoveTable;
+
+#[cfg(test)]
+mod test {
+ use super::*;
+
+ use proptest::collection::vec;
+ use proptest::prelude::*;
+
+ #[test]
+ fn generates() {
+ ESliceEdgeMoveTable::generate();
+ DominoESliceMoveTable::generate();
+ COSymMoveTable::generate(&RawSymTable::generate());
+ EOSymMoveTable::generate(&RawSymTable::generate());
+ DominoCPSymMoveTable::generate(&RawSymTable::generate());
+ DominoEPSymMoveTable::generate(&RawSymTable::generate());
+ }
+
+ /* We check that the following diagram commutes
+ *
+ * CubieCube --apply_move--> CubieCube
+ * | |
+ * | |
+ * from_puzzle from_puzzle
+ * | |
+ * | |
+ * v v
+ * Coord -----apply_move---> Coord
+ *
+ * Move application should be compatable with coordinate translation.
+ */
+
+ fn diagram_commutes<
+ M: SubMove,
+ C: Coordinate + std::fmt::Debug,
+ const MOVES: usize,
+ >(
+ table: &MoveTable,
+ p: CubieCube,
+ mvs: MoveSequence,
+ ) {
+ let l = table.make_moves(C::from_puzzle(&p), mvs.clone());
+ let r = C::from_puzzle(&p.make_moves(MoveSequence(
+ mvs.0.into_iter().map(|m| m.into_move()).collect(),
+ )));
+ assert_eq!(l, r);
+ }
+
+ fn sym_diagram_commutes<
+ M: SubMove,
+ S: SymCoordinate + std::fmt::Debug,
+ const MOVES: usize,
+ const SYMS: usize,
+ >(
+ sym_table: &RawSymTable,
+ table: &SymMoveTable,
+ p: CubieCube,
+ mvs: MoveSequence,
+ ) where
+ CubieCube: FromCoordinate,
+ S::Raw: std::fmt::Debug,
+ {
+ let l = table.make_moves(sym_table.puzzle_to_sym(&p), mvs.clone());
+ let r = sym_table.puzzle_to_sym(&p.make_moves(MoveSequence(
+ mvs.0.into_iter().map(|m| m.into_move()).collect(),
+ )));
+ assert_eq!(sym_table.sym_to_raw(l), sym_table.sym_to_raw(r));
+ }
+
+ #[test]
+ fn commutes_normal() {
+ let eslice_table = ESliceEdgeMoveTable::generate();
+
+ let co_sym = RawSymTable::generate();
+ let co_sym_table = COSymMoveTable::generate(&co_sym);
+ let eo_sym = RawSymTable::generate();
+ let eo_sym_table = EOSymMoveTable::generate(&eo_sym);
+ proptest!(|(mvs in vec(any::(), 0..20).prop_map(MoveSequence))| {
+ diagram_commutes(&eslice_table, CubieCube::SOLVED, mvs.clone());
+ sym_diagram_commutes(&co_sym, &co_sym_table, CubieCube::SOLVED, mvs.clone());
+ sym_diagram_commutes(&eo_sym, &eo_sym_table, CubieCube::SOLVED, mvs.clone());
+ });
+ }
+
+ #[test]
+ fn commutes_domino() {
+ let co_sym = RawSymTable::generate();
+ let cp_table = DominoCPSymMoveTable::generate(&co_sym);
+ let eo_sym = RawSymTable::generate();
+ let ep_table = DominoEPSymMoveTable::generate(&eo_sym);
+ let eslice_table = DominoESliceMoveTable::generate();
+ proptest!(|(mvs in vec(any::(), 0..20).prop_map(MoveSequence))| {
+ sym_diagram_commutes(&co_sym, &cp_table, CubieCube::SOLVED, mvs.clone());
+ sym_diagram_commutes(&eo_sym, &ep_table, CubieCube::SOLVED, mvs.clone());
+ diagram_commutes(&eslice_table, CubieCube::SOLVED, mvs.clone());
+ });
+ }
+}
diff --git a/src/cube333/two_phase_solver/prune.rs b/src/cube333/two_phase_solver/prune.rs
new file mode 100644
index 0000000..de38193
--- /dev/null
+++ b/src/cube333/two_phase_solver/prune.rs
@@ -0,0 +1,349 @@
+//! Pruning tables for the two phase solver.
+//!
+//! Choices of pruning tables are from cs0x7f's min2phase.
+
+use super::coords::{
+ COSymCoord, CPSymCoord, DominoEPSymCoord, DominoESliceCoord, EOSymCoord, ESliceEdgeCoord,
+ RawSymTable, SymCoordinate,
+};
+use super::move_tables::{DrMove, MoveTable, SubMove, SymMoveTable};
+use super::symmetry::Symmetry;
+use crate::coord::{Coordinate, FromCoordinate};
+use crate::cube333::{CubieCube, moves::Move333};
+
+use std::marker::PhantomData;
+use std::rc::Rc;
+
+// TODO future stuff:
+// look into alternative pruning table choices
+// look into alternative information to store in pruning tables
+// look into alternative compression schemes
+
+/// A table storing results of conjugating raw coordinates by symmetries or inverses of symmetries.
+pub struct SymConjTable, const SYMS: usize> {
+ table: Box<[[R; SYMS]]>,
+ inv_table: Box<[[R; SYMS]]>,
+ _phantom1: PhantomData,
+ _phantom2: PhantomData,
+}
+
+impl, const SYMS: usize> SymConjTable
+where
+ CubieCube: FromCoordinate,
+{
+ /// Generate the table
+ pub fn generate() -> Self {
+ let mut table: Box<[[R; SYMS]]> =
+ vec![std::array::from_fn(|_| Default::default()); R::count()].into_boxed_slice();
+ let mut inv_table: Box<[[R; SYMS]]> =
+ vec![std::array::from_fn(|_| Default::default()); R::count()].into_boxed_slice();
+
+ for r1 in (0..R::count()).map(R::from_repr) {
+ let mut c = CubieCube::SOLVED;
+ c.set_coord(r1);
+ for s in S::get_all() {
+ let d = c.clone().conjugate_symmetry(s);
+ let r2 = R::from_puzzle(&d);
+ table[r1.repr()][s.repr()] = r2;
+ let d = c.clone().conjugate_inverse_symmetry(s);
+ let r2 = R::from_puzzle(&d);
+ inv_table[r1.repr()][s.repr()] = r2;
+ }
+ }
+
+ Self {
+ table,
+ inv_table,
+ _phantom1: PhantomData,
+ _phantom2: PhantomData,
+ }
+ }
+
+ /// Conjugate the given raw coordinate by the given symmetry (S R S^-1).
+ pub fn conjugate(&self, r: R, s: S) -> R {
+ self.table[r.repr()][s.repr()]
+ }
+
+ /// Conjugate the given raw coordinate by the given symmetry's inverse (S^-1 R S).
+ pub fn conjugate_inverse(&self, r: R, s: S) -> R {
+ self.inv_table[r.repr()][s.repr()]
+ }
+}
+
+/// A pruning table indexed by the class of a symmetry coordinate and a raw coordinate.
+///
+/// COUNT should be the number of symmetry coordinate classes times the number of raw coordinates
+/// divided by 4 (we store 4 entries per byte). An entry is the optimal search depth of the state
+/// modulo 3, see https://kociemba.org/math/pruning.htm for a detailed explanation. Essentially
+/// though, we can compute the whole pruning depth based on solely the pruning depth modulo 3 if we
+/// know the pruning depth of the current state we are in, when searching.
+pub struct SymRawPruningTable<
+ S: SymCoordinate,
+ R: Coordinate,
+ M: SubMove,
+ const SYMS: usize,
+ const MOVES: usize,
+> where
+ CubieCube: FromCoordinate,
+ CubieCube: FromCoordinate,
+{
+ table: Box<[u8]>,
+ conj_table: SymConjTable,
+ sym_table: Rc>,
+ sym_move_table: Rc>,
+ raw_move_table: Rc>,
+}
+
+impl, M: SubMove, const SYMS: usize, const MOVES: usize>
+ SymRawPruningTable
+where
+ CubieCube: FromCoordinate,
+ CubieCube: FromCoordinate,
+{
+ /// Generate the table
+ pub fn generate(
+ sym_table: Rc>,
+ sym_move_table: Rc>,
+ raw_move_table: Rc>,
+ ) -> Self {
+ let table = vec![0xff; S::classes() * R::count() / 4].into_boxed_slice();
+ let conj_table = SymConjTable::generate();
+ let mut table = Self {
+ table,
+ conj_table,
+ sym_table,
+ sym_move_table,
+ raw_move_table,
+ };
+
+ let conj_table_s = SymConjTable::generate();
+
+ let s = table.sym_table.puzzle_to_sym(&CubieCube::SOLVED);
+ let r = R::from_puzzle(&CubieCube::SOLVED);
+ table.set(s, r, 0, &conj_table_s);
+ let mut stack = vec![(s, r)];
+ let mut next = vec![];
+ let mut depth = 1;
+
+ while !stack.is_empty() {
+ while let Some((s, r)) = stack.pop() {
+ for &m in M::MOVE_LIST {
+ let s2 = table.sym_move_table.make_move(s, m);
+ let r2 = table.raw_move_table.make_move(r, m);
+ if table.query(s2, r2) == 3 {
+ next.push((s2, r2));
+ table.set(s2, r2, depth % 3, &conj_table_s);
+ }
+ }
+ }
+
+ stack = next;
+ next = vec![];
+ depth += 1;
+ }
+
+ table
+ }
+
+ /// Compute the index and shift into the table given a coordinate pair.
+ fn index(&self, s: S, r: R) -> (usize, usize) {
+ let r2 = self.conj_table.conjugate_inverse(r, s.sym());
+ let i = r2.repr() * S::classes() + s.class();
+ (i >> 2, (i & 3) * 2)
+ }
+
+ /// Set the depth in the search tree of this coordinate pair modulo 3.
+ fn set(&mut self, s: S, r: R, val: u8, conj_table_s: &SymConjTable) {
+ assert!(val & !3 == 0);
+
+ // Some S::Raw coordinates can be represented in multiple ways by S (there can be multiple
+ // symmetries that give an equivalent raw coordinate when conjugating some representative,
+ // think the solved state for example which could be represented by any symmetry). Because
+ // of this, there could be multiple entries into our pruning table corresponding to the
+ // same state. With just s and r, we would only update the entry corresponding to (if S is
+ // the symmetry) S^-1 r S, and so we must iterate over all symmetries and find the
+ // duplicates we need to update.
+
+ let repr_raw = self.sym_table.index_to_repr(s.class());
+ let sinv_raw = conj_table_s.conjugate(repr_raw, s.sym());
+ for sym in S::Sym::get_all() {
+ if conj_table_s.conjugate(repr_raw, sym) == sinv_raw {
+ let s2 = S::from_repr(s.class(), sym);
+ let (index, shift) = self.index(s2, r);
+
+ self.table[index] &= !(3 << shift);
+ self.table[index] |= val << shift;
+ }
+ }
+ }
+
+ /// Determine the bound of a coordinate pair modulo 3 with a lookup
+ fn query(&self, s: S, r: R) -> u8 {
+ let (index, shift) = self.index(s, r);
+
+ (self.table[index] >> shift) & 3
+ }
+
+ /// Update a prune bound given the next state (fast)
+ pub fn update(&self, cur: usize, s: S, r: R) -> usize {
+ let n = self.query(s, r) as usize;
+ let c = cur % 3;
+ let d = (n + 3 - c).rem_euclid(3);
+ match d {
+ 0 => cur,
+ 1 => cur + 1,
+ 2 => cur - 1,
+ _ => unreachable!(),
+ }
+ }
+
+ /// Compute the bound on a given coordinate pair (slow)
+ pub fn bound(&self, mut s: S, mut r: R) -> usize {
+ let mut bound = 0;
+ let solved = (
+ self.sym_table.puzzle_to_sym(&CubieCube::SOLVED).class(),
+ R::from_puzzle(&CubieCube::SOLVED),
+ );
+ while (s.class(), r) != solved {
+ let n = self.query(s, r);
+ // n - 1 but underflow
+ let goal = (n + 2).rem_euclid(3);
+ (s, r) = M::MOVE_LIST
+ .iter()
+ .map(|&m| {
+ (
+ self.sym_move_table.make_move(s, m),
+ self.raw_move_table.make_move(r, m),
+ )
+ })
+ .find(|&(s, r)| self.query(s, r) == goal)
+ .unwrap();
+
+ bound += 1;
+ }
+ bound
+ }
+}
+
+pub type ESliceTwistPruneTable = SymRawPruningTable;
+pub type ESliceFlipPruneTable = SymRawPruningTable;
+pub type DominoSliceCPPruneTable =
+ SymRawPruningTable;
+pub type DominoSliceEPPruneTable =
+ SymRawPruningTable;
+
+#[cfg(test)]
+mod test {
+ use super::super::move_tables::{DrMove, SubMove};
+ use super::super::symmetry::HalfSymmetry;
+ use super::*;
+ use crate::coord::{Coordinate, FromCoordinate};
+ use crate::cube333::coordcube::EOCoord;
+ use crate::cube333::moves::Move333;
+ use crate::moves::MoveSequence;
+
+ use proptest::collection::vec;
+ use proptest::prelude::*;
+
+ type SliceConjTable = SymConjTable;
+ type EoConjTable = SymConjTable;
+ type DominoESliceConjTable = SymConjTable;
+
+ fn diagram_commutes<
+ S: Symmetry,
+ R: Coordinate + std::fmt::Debug,
+ const COUNT: usize,
+ >(
+ table: &SymConjTable,
+ cube: CubieCube,
+ ) where
+ CubieCube: FromCoordinate,
+ {
+ for s in S::get_all() {
+ let a = table.conjugate(R::from_puzzle(&cube), s);
+ let b = R::from_puzzle(&cube.clone().conjugate_inverse_symmetry(s));
+ assert_eq!(a, b);
+ }
+ }
+
+ #[test]
+ fn conj_commutes() {
+ let slice_conj_table = SliceConjTable::generate();
+ let eo_conj_table = EoConjTable::generate();
+ let domino_eo_conj_table = DominoESliceConjTable::generate();
+ proptest!(|(mvs in vec(any::(), 0..20).prop_map(MoveSequence))| {
+ diagram_commutes(&slice_conj_table, CubieCube::SOLVED.make_moves(mvs.clone()));
+ diagram_commutes(&eo_conj_table, CubieCube::SOLVED.make_moves(mvs.clone()));
+ });
+ proptest!(|(mvs in vec(any::(), 0..20).prop_map(|v| v.into_iter().map(SubMove::into_move).collect()).prop_map(MoveSequence))| {
+ diagram_commutes(&domino_eo_conj_table, CubieCube::SOLVED.make_moves(mvs.clone()));
+ });
+ }
+
+ fn admissable_and_update_correct<
+ S: SymCoordinate,
+ R: Coordinate,
+ M: SubMove,
+ const SYMS: usize,
+ const MOVES: usize,
+ >(
+ prune_table: &SymRawPruningTable,
+ mvs: MoveSequence,
+ ) where
+ CubieCube: FromCoordinate,
+ CubieCube: FromCoordinate,
+ {
+ let s = prune_table.sym_table.puzzle_to_sym(&CubieCube::SOLVED);
+ let s = prune_table.sym_move_table.make_moves(s, mvs.clone());
+ let r = R::from_puzzle(&CubieCube::SOLVED);
+ let r = prune_table.raw_move_table.make_moves(r, mvs.clone());
+ let b = prune_table.bound(s, r);
+ assert!(b <= mvs.len());
+
+ for &m in M::MOVE_LIST {
+ let s2 = prune_table.sym_move_table.make_move(s, m);
+ let r2 = prune_table.raw_move_table.make_move(r, m);
+ let b2 = prune_table.update(b, s2, r2);
+ assert_eq!(b2, prune_table.bound(s2, r2));
+ }
+ }
+
+ #[test]
+ fn check_admissable_and_update() {
+ let co_sym_table = Rc::new(RawSymTable::generate());
+ let co_sym_move_table = Rc::new(SymMoveTable::generate(&co_sym_table));
+ let eo_sym_table = Rc::new(RawSymTable::generate());
+ let eo_sym_move_table = Rc::new(SymMoveTable::generate(&eo_sym_table));
+ let e_slice_move_table = Rc::new(MoveTable::generate());
+ let c_prune = ESliceTwistPruneTable::generate(
+ co_sym_table,
+ co_sym_move_table,
+ e_slice_move_table.clone(),
+ );
+ let e_prune =
+ ESliceFlipPruneTable::generate(eo_sym_table, eo_sym_move_table, e_slice_move_table);
+ proptest!(|(mvs in vec(any::(), 0..20).prop_map(MoveSequence))| {
+ admissable_and_update_correct(&c_prune, mvs.clone());
+ admissable_and_update_correct(&e_prune, mvs.clone());
+ });
+ let cp_sym_table = Rc::new(RawSymTable::generate());
+ let cp_sym_move_table = Rc::new(SymMoveTable::generate(&cp_sym_table));
+ let ep_sym_table = Rc::new(RawSymTable::generate());
+ let ep_sym_move_table = Rc::new(SymMoveTable::generate(&ep_sym_table));
+ let d_e_slice_move_table = Rc::new(MoveTable::generate());
+ let d_c_prune = DominoSliceCPPruneTable::generate(
+ cp_sym_table,
+ cp_sym_move_table,
+ d_e_slice_move_table.clone(),
+ );
+ let d_e_prune = DominoSliceEPPruneTable::generate(
+ ep_sym_table,
+ ep_sym_move_table,
+ d_e_slice_move_table,
+ );
+ proptest!(|(mvs in vec(any::(), 0..20).prop_map(MoveSequence))| {
+ admissable_and_update_correct(&d_c_prune, mvs.clone());
+ admissable_and_update_correct(&d_e_prune, mvs.clone());
+ });
+ }
+}
diff --git a/src/cube333/two_phase_solver/symmetry.rs b/src/cube333/two_phase_solver/symmetry.rs
new file mode 100644
index 0000000..032c4ed
--- /dev/null
+++ b/src/cube333/two_phase_solver/symmetry.rs
@@ -0,0 +1,368 @@
+//! Implements symmetries for the `CubieCube` and symmetry tables for use in move table generation.
+
+use super::move_tables::SubMove;
+use crate::cube333::{Corner as C, CornerTwist as CT, CubieCube, Edge as E, EdgeFlip as EF};
+
+use std::marker::PhantomData;
+
+pub trait Symmetry: Copy + Default + Eq {
+ /// A representation of this symmetry as a usize, for use in table lookups.
+ fn repr(self) -> usize;
+
+ /// Convert the representation of a symmetry to the symmetry itself. We assume 0 corresponds to
+ /// the identity symmetry.
+ fn from_repr(n: usize) -> Self;
+
+ /// Iterator over every symmetry in order of representation
+ fn get_all() -> impl Iterator- ;
+
+ /// Apply this symmetry to the given puzzle, written P S
+ fn apply(&self, cube: CubieCube) -> CubieCube;
+
+ /// Apply the inverse of this symmetry to the given puzzle, written P S^-1
+ fn apply_inverse(&self, cube: CubieCube) -> CubieCube;
+
+ /// Conjugate the given puzzle by this symmetry, written S P S^-1
+ fn conjugate(&self, cube: CubieCube) -> CubieCube {
+ self.apply_inverse(self.apply(CubieCube::SOLVED).multiply_cube(cube))
+ }
+
+ /// Conjugate the given puzzle by the inverse of this symmetry, written S^-1 P S^
+ fn conjugate_inverse(&self, cube: CubieCube) -> CubieCube {
+ self.apply(self.apply_inverse(CubieCube::SOLVED).multiply_cube(cube))
+ }
+}
+
+impl CubieCube {
+ /* unused
+ /// Obtain the cube given by applying some symmetry
+ pub(super) fn apply_symmetry(self, sym: S) -> CubieCube {
+ sym.apply(self)
+ }
+ */
+
+ /// Obtain the cube given by conjugating by some symmetry. We conjugate in the order S C S^-1
+ pub(super) fn conjugate_symmetry(self, sym: S) -> CubieCube {
+ sym.conjugate(self)
+ }
+
+ /// Obtain the cube given by conjugating by the inverse of some symmetry. We hence conjugate in
+ /// the order S^-1 C S
+ pub(super) fn conjugate_inverse_symmetry(self, sym: S) -> CubieCube {
+ sym.conjugate_inverse(self)
+ }
+}
+
+/// Multiplication table for a `Symmetry` group.
+pub struct SymMultTable {
+ table: [[S; COUNT]; COUNT],
+}
+
+impl SymMultTable
{
+ /// Generate the table
+ pub fn generate() -> Self {
+ use std::array::from_fn;
+
+ let cubie_syms: [_; COUNT] = from_fn(|n| S::from_repr(n).apply(CubieCube::SOLVED));
+
+ let table = from_fn(|a| {
+ from_fn(|b| {
+ let a = S::from_repr(a);
+ let b = S::from_repr(b);
+ let c = a
+ .apply(CubieCube::SOLVED)
+ .multiply_cube(b.apply(CubieCube::SOLVED));
+ S::from_repr(cubie_syms.iter().position(|d| &c == d).unwrap())
+ })
+ });
+
+ SymMultTable { table }
+ }
+
+ /// Multiply two symmetries
+ pub fn multiply(&self, a: S, b: S) -> S {
+ self.table[a.repr()][b.repr()]
+ }
+}
+
+/// A table of conjugates of moves by symmetries i.e. we identify moves corresponding to S^-1 M S.
+/// We use an inverse conjugate to the usual convention of this repository as it is what is needed
+/// for the symmetry move table.
+pub struct SymMoveConjTable {
+ table: [[M; MOVES]; SYMS],
+ _phantom: PhantomData,
+}
+
+impl
+ SymMoveConjTable
+{
+ /// Generate the table
+ pub fn generate() -> Self {
+ use std::array::from_fn;
+
+ let table = from_fn(|s| {
+ from_fn(|m| {
+ let s = S::from_repr(s);
+ let m = M::MOVE_LIST[m];
+
+ let c = s.conjugate_inverse(CubieCube::SOLVED.make_move(m.into_move()));
+
+ M::MOVE_LIST
+ .iter()
+ .filter_map(|&m2| {
+ (c.clone().make_move(m2.into_move()) == CubieCube::SOLVED)
+ .then_some(m2.inverse())
+ })
+ .next()
+ .expect("Moves should have conjugates in each symmetry")
+ })
+ });
+
+ SymMoveConjTable {
+ table,
+ _phantom: PhantomData,
+ }
+ }
+
+ /// Determine the move corresponding to S^-1 M S given M and S.
+ pub fn conjugate(&self, m: M, s: S) -> M {
+ self.table[s.repr()][m.index()]
+ }
+}
+
+/// An element of the set of symmetries of a cube that preserve domino reduction. This is generated
+/// by:
+/// - A 180 degree rotation around the F/B axis (aka F2)
+/// - A 90 degree rotation around the U/D axis (aka U4)
+/// - A reflection around the R-L slice (aka RL2)
+// We represent a symmetry as a product of these generators in a specific order:
+// - the 0th bit determines R-L reflection
+// - the 1st bit determines F/B 180 rotation
+// - the 2nd and 3rd bits determines U/D rotation
+// We multiply from top to bottom of this list. It doesn't really make sense to expose this
+// information publicly, since we could have multiplied different generators in a different order.
+// We can just think of this 4 bit number as an identifier for each symmetry.
+#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Copy, Clone, Hash)]
+pub struct DrSymmetry(u8);
+
+/// A cube that, when multiplied, applies the F2 symmetry.
+#[rustfmt::skip]
+const SYM_F2: CubieCube = CubieCube {
+ co: [CT::Oriented; 8],
+ cp: [C::DFL, C::DFR, C::DBR, C::DBL, C::UFL, C::UFR, C::UBR, C::UBL],
+ eo: [EF::Oriented; 12],
+ ep: [E::DF, E::DR, E::DB, E::DL, E::UF, E::UR, E::UB, E::UL, E::FL, E::FR, E::BR, E::BL],
+};
+
+/// A cube that, when multiplied, applies the U4 symmetry.
+#[rustfmt::skip]
+const SYM_U4: CubieCube = CubieCube {
+ co: [CT::Oriented; 8],
+ cp: [C::UBR, C::UFR, C::UFL, C::UBL, C::DBR, C::DFR, C::DFL, C::DBL],
+ eo: [EF::Oriented, EF::Oriented, EF::Oriented, EF::Oriented, EF::Oriented, EF::Oriented, EF::Oriented, EF::Oriented, EF::Flipped, EF::Flipped, EF::Flipped, EF::Flipped],
+ ep: [E::UR, E::UF, E::UL, E::UB, E::DR, E::DF, E::DL, E::DB, E::BR, E::FR, E::FL, E::BL],
+};
+
+/// A cube that, when multiplied, almost applies the RL2 symmetry, but additionally the corner
+/// orientations must be inverted (clockwise and anticlockwise swapped).
+#[rustfmt::skip]
+const SYM_RL2: CubieCube = CubieCube {
+ co: [CT::Oriented; 8],
+ cp: [C::UBR, C::UBL, C::UFL, C::UFR, C::DBR, C::DBL, C::DFL, C::DFR],
+ eo: [EF::Oriented; 12],
+ ep: [E::UB, E::UL, E::UF, E::UR, E::DB, E::DL, E::DF, E::DR, E::BR, E::BL, E::FL, E::FR],
+};
+
+impl DrSymmetry {
+ /// Returns the power of RL2 in the standard product notation of this symmetry.
+ fn rl2_count(self) -> usize {
+ (self.0 & 1) as usize
+ }
+
+ /// Returns the power of F2 in the standard product notation of this symmetry.
+ fn f2_count(self) -> usize {
+ (self.0 >> 1 & 1) as usize
+ }
+
+ /// Returns the power of U4 in the standard product notation of this symmetry.
+ fn u4_count(self) -> usize {
+ (self.0 >> 2 & 3) as usize
+ }
+
+ // lol
+ /// An array of each symmetry
+ #[rustfmt::skip]
+ pub const ARRAY: [DrSymmetry; 16] = [DrSymmetry(0), DrSymmetry(1), DrSymmetry(2), DrSymmetry(3), DrSymmetry(4), DrSymmetry(5), DrSymmetry(6), DrSymmetry(7), DrSymmetry(8), DrSymmetry(9), DrSymmetry(10), DrSymmetry(11), DrSymmetry(12), DrSymmetry(13), DrSymmetry(14), DrSymmetry(15)];
+}
+
+impl Symmetry for DrSymmetry {
+ fn repr(self) -> usize {
+ self.0 as usize
+ }
+
+ fn from_repr(n: usize) -> Self {
+ DrSymmetry(n as u8)
+ }
+
+ fn get_all() -> impl Iterator- {
+ Self::ARRAY.into_iter()
+ }
+
+ fn apply(&self, mut cube: CubieCube) -> CubieCube {
+ for _ in 0..self.rl2_count() {
+ cube = cube.multiply_cube(SYM_RL2);
+ cube.co = cube.co.map(|c| c.inverse());
+ }
+
+ for _ in 0..self.f2_count() {
+ cube = cube.multiply_cube(SYM_F2);
+ }
+
+ for _ in 0..self.u4_count() {
+ cube = cube.multiply_cube(SYM_U4);
+ }
+
+ cube
+ }
+
+ fn apply_inverse(&self, mut cube: CubieCube) -> CubieCube {
+ for _ in 0..((4 - self.u4_count()) % 4) {
+ cube = cube.multiply_cube(SYM_U4);
+ }
+
+ for _ in 0..self.f2_count() {
+ cube = cube.multiply_cube(SYM_F2);
+ }
+
+ for _ in 0..self.rl2_count() {
+ cube = cube.multiply_cube(SYM_RL2);
+ cube.co = cube.co.map(|c| c.inverse());
+ }
+
+ cube
+ }
+}
+
+/// An element of the set of symmetries of a cube that preserve domino reduction. This is generated
+/// by:
+/// - A 180 degree rotation around the F/B axis (aka F2)
+/// - A 180 degree rotation around the U/D axis (aka U2)
+/// - A reflection around the R-L slice (aka RL2)
+// 0s bit is R-L
+// 1s bit F/B
+// 2s bit U/D
+#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Copy, Clone, Hash)]
+pub struct HalfSymmetry(u8);
+
+impl HalfSymmetry {
+ /// Returns the power of RL2 in the standard product notation of this symmetry.
+ fn rl2_count(self) -> usize {
+ (self.0 & 1) as usize
+ }
+
+ /// Returns the power of F2 in the standard product notation of this symmetry.
+ fn f2_count(self) -> usize {
+ (self.0 >> 1 & 1) as usize
+ }
+
+ /// Returns the power of U2 in the standard product notation of this symmetry.
+ fn u2_count(self) -> usize {
+ (self.0 >> 2 & 1) as usize
+ }
+
+ // lol
+ /// An array of each symmetry
+ #[rustfmt::skip]
+ pub const ARRAY: [HalfSymmetry; 8] = [HalfSymmetry(0), HalfSymmetry(1), HalfSymmetry(2), HalfSymmetry(3), HalfSymmetry(4), HalfSymmetry(5), HalfSymmetry(6), HalfSymmetry(7)];
+}
+
+impl Symmetry for HalfSymmetry {
+ fn repr(self) -> usize {
+ self.0 as usize
+ }
+
+ fn from_repr(n: usize) -> Self {
+ HalfSymmetry(n as u8)
+ }
+
+ fn get_all() -> impl Iterator
- {
+ Self::ARRAY.into_iter()
+ }
+
+ fn apply(&self, mut cube: CubieCube) -> CubieCube {
+ for _ in 0..self.rl2_count() {
+ cube = cube.multiply_cube(SYM_RL2);
+ cube.co = cube.co.map(|c| c.inverse());
+ }
+
+ for _ in 0..self.f2_count() {
+ cube = cube.multiply_cube(SYM_F2);
+ }
+
+ for _ in 0..self.u2_count() {
+ cube = cube.multiply_cube(SYM_U4.multiply_cube(SYM_U4));
+ }
+
+ cube
+ }
+
+ fn apply_inverse(&self, mut cube: CubieCube) -> CubieCube {
+ for _ in 0..self.u2_count() {
+ cube = cube.multiply_cube(SYM_U4.multiply_cube(SYM_U4));
+ }
+
+ for _ in 0..self.f2_count() {
+ cube = cube.multiply_cube(SYM_F2);
+ }
+
+ for _ in 0..self.rl2_count() {
+ cube = cube.multiply_cube(SYM_RL2);
+ cube.co = cube.co.map(|c| c.inverse());
+ }
+
+ cube
+ }
+}
+
+#[cfg(test)]
+mod test {
+ use super::*;
+
+ use crate::cube333::moves::Move333;
+ use crate::moves::MoveSequence;
+
+ use proptest::collection::vec;
+ use proptest::prelude::*;
+
+ fn check_multiplication_correct() {
+ let table = SymMultTable::
::generate();
+ proptest!(|(mvs in vec(any::(), 0..20).prop_map(MoveSequence))| {
+ let cube = CubieCube::SOLVED.make_moves(mvs);
+ for sym1 in S::get_all() {
+ for sym2 in S::get_all() {
+ assert_eq!(
+ table.multiply(sym1, sym2).apply(cube.clone()),
+ sym2.apply(sym1.apply(cube.clone())),
+ "{sym1:?} {sym2:?} {:?}", table.multiply(sym1, sym2),
+ );
+ assert_eq!(
+ table.multiply(sym1, sym2).apply_inverse(cube.clone()),
+ sym1.apply_inverse(sym2.apply_inverse(cube.clone())),
+ "{sym1:?} {sym2:?} {:?}", table.multiply(sym1, sym2),
+ );
+ assert_eq!(
+ table.multiply(sym1, sym2).conjugate(cube.clone()),
+ sym1.conjugate(sym2.conjugate(cube.clone())),
+ "{sym1:?} {sym2:?} {:?}", table.multiply(sym1, sym2),
+ );
+ }
+ }
+ });
+ }
+
+ #[test]
+ fn multiplication_correct() {
+ check_multiplication_correct::();
+ check_multiplication_correct::();
+ }
+}
diff --git a/src/error.rs b/src/error.rs
index 9933011..4346631 100644
--- a/src/error.rs
+++ b/src/error.rs
@@ -9,4 +9,3 @@ pub enum TryFromIntToEnumError {
#[error("attempted to convert integer into enum value, but integer was out of bounds")]
OutOfBounds,
}
-
diff --git a/src/lib.rs b/src/lib.rs
index 265d196..26ebbec 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -3,6 +3,7 @@
#![deny(missing_docs)]
+pub mod coord;
pub mod cube333;
pub mod error;
pub mod moves;
diff --git a/src/moves/mod.rs b/src/moves/mod.rs
index 60a99c6..5a26ab3 100644
--- a/src/moves/mod.rs
+++ b/src/moves/mod.rs
@@ -26,12 +26,10 @@ pub enum Cancellation {
/// are encoded in the `commutes_with` method and order relations are encoded in the `cancel`
/// method. These relations are all that are assumed for the general `MoveSequence::cancel`, so any
/// additional relations will not be used for optimal cancellation.
-pub trait Move: Eq + Clone {
+pub trait Move: Eq + Clone + Sized {
/// Take the inverse of a move. These inverses must satisfy the invertibility conditions of
/// a group, i.e. that `X X^{-1} = X^{-1} X = e` where `e` is the empty sequence.
- fn inverse(self) -> Self
- where
- Self: Sized;
+ fn inverse(self) -> Self;
/// Returns whether the two moves commute, i.e. can be swapped when adjacent. It is required
/// that this property is transitive.
@@ -57,9 +55,7 @@ pub trait Move: Eq + Clone {
/// assert!(mv!(R, 1).cancel(mv!(R, 3)) == Cancellation::NoMove);
/// # }
/// ```
- fn cancel(self, b: Self) -> Cancellation
- where
- Self: Sized;
+ fn cancel(self, b: Self) -> Cancellation;
}
/// A sequence of moves (also known as an algorithm) for some specific type of move.