Skip to content

Instantly share code, notes, and snippets.

@s5bug
Last active December 28, 2025 00:35
Show Gist options
  • Select an option

  • Save s5bug/53d416aa7c35f8687b1780e5be667138 to your computer and use it in GitHub Desktop.

Select an option

Save s5bug/53d416aa7c35f8687b1780e5be667138 to your computer and use it in GitHub Desktop.
Movement around a grid wrapped on cube faces using quaternions
//> using scala 3.7.4
//> using dep org.typelevel::spire:0.18.0
import spire.math.{Algebraic, Quaternion}
enum Face extends java.lang.Enum[Face] {
case Front
case Back
case Left
case Right
case Top
case Bottom
}
final case class Qube(size: Int, orientation: Quaternion[Algebraic], x: Int, y: Int) {
def moveUp: Qube =
if this.y <= 0 then
this.copy(orientation = this.orientation * Qube.upWrap, y = this.size - 1)
else
this.copy(y = this.y - 1)
def moveLeft: Qube =
if this.x <= 0 then
this.copy(orientation = this.orientation * Qube.leftWrap, x = this.size - 1)
else
this.copy(x = this.x - 1)
def moveDown: Qube =
if this.y >= this.size - 1 then
this.copy(orientation = this.orientation * Qube.downWrap, y = 0)
else
this.copy(y = this.y + 1)
def moveRight: Qube =
if this.x >= this.size - 1 then
this.copy(orientation = this.orientation * Qube.rightWrap, x = 0)
else
this.copy(x = this.x + 1)
def face: Face = {
val pointer = this.orientation * Qube.LeftVector * this.orientation.reciprocal
pointer match {
case Qube.LeftVector => Face.Left
case Qube.RightVector => Face.Right
case Qube.UpVector => Face.Top
case Qube.DownVector => Face.Bottom
case Qube.BackVector => Face.Back
case Qube.FrontVector => Face.Front
case notAFace => throw new IllegalStateException("unknown orientation: " + this.orientation)
}
}
def normalizedPosition: (Int, Int) = {
val r = this.orientation.r
val i = this.orientation.i
if r != Algebraic.Zero && i == Algebraic.Zero then {
// 0deg case
(x, y)
} else if r == i then {
// 90deg case
((this.size - 1) - y, x)
} else if r == Algebraic.Zero && i != Algebraic.Zero then {
// 180deg case
((this.size - 1) - x, (this.size - 1) - y)
} else if r == -i then {
// 270deg case
(y, (this.size - 1) - x)
} else {
throw new IllegalStateException("unknown orientation: " + this.orientation)
}
}
}
object Qube {
private final val oneOverSqrtTwo = Algebraic(1) / Algebraic(2).sqrt
private final val LeftVector: Quaternion[Algebraic] =
Quaternion.i[Algebraic]
private final val UpVector: Quaternion[Algebraic] =
Quaternion.j[Algebraic]
private final val FrontVector: Quaternion[Algebraic] =
Quaternion.k[Algebraic]
private final val RightVector: Quaternion[Algebraic] =
-LeftVector
private final val DownVector: Quaternion[Algebraic] =
-UpVector
private final val BackVector: Quaternion[Algebraic] =
-FrontVector
private final val leftWrap: Quaternion[Algebraic] =
Quaternion(oneOverSqrtTwo, Algebraic.Zero, oneOverSqrtTwo, Algebraic.Zero)
private final val upWrap: Quaternion[Algebraic] =
Quaternion(oneOverSqrtTwo, Algebraic.Zero, Algebraic.Zero, oneOverSqrtTwo)
private final val rightWrap: Quaternion[Algebraic] =
Quaternion(oneOverSqrtTwo, Algebraic.Zero, -oneOverSqrtTwo, Algebraic.Zero)
private final val downWrap: Quaternion[Algebraic] =
Quaternion(oneOverSqrtTwo, Algebraic.Zero, Algebraic.Zero, -oneOverSqrtTwo)
def leftBackTop(size: Int): Qube =
Qube(size, Quaternion.one, 0, 0)
}
val q0 = Qube.leftBackTop(2)
q0
q0.face // => Left
q0.normalizedPosition // => (0, 0)
val q1 = q0.moveRight
q1
q1.face // => Left
q1.normalizedPosition // => (1, 0)
val q2 = q1.moveRight
q2
q2.face // => Front
q2.normalizedPosition // => (0, 0)
val q3 = q2.moveUp
q3
q3.face // => Top
q3.normalizedPosition // => (1, 1)
val q4 = q3.moveUp
q4
q4.face // => Top
q4.normalizedPosition // => (0, 1)
val q5 = q4.moveUp
q5
q5.face // => Back
q5.normalizedPosition // => (1, 0)
val q6 = q5.moveLeft
q6
q6.face // => Left
q6.normalizedPosition // => (0, 0)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment