Skip to content

First Puzzle

Now that we know all the puzzle jargon, let's build a shallow-cut face-turning cube (that's the standard 3x3x3 Rubik's cube) in Hyperspeedcube. First I'll show you the code and then we'll dissect each part.

3x3x3.lua
puzzles:add('3x3x3', {
  name = "3x3x3",
  ndim = 3,
  build = function(self)
    local sym = cd'bc3'

    -- Carve the base shape
    for _, v in sym:orbit(sym.oox.unit) do
      self:carve(plane(v))
    end

    -- Add twist axes and slice puzzle
    for _, v in sym:orbit(sym.oox.unit) do
      self.axes:add(v, {1/3, -1/3})
    end

    -- Add twists
    for _, axis in ipairs(self.axes) do
      self.twists:add(axis, rot{fix = axis.vector, angle = tau/4})
    end
  end,
})

Boilerplate

I've tried my best to minimize the amount of boilerplate required to define a puzzle, but there's still some.

Boilerplate
puzzles:add('3x3x3', {
  name = "3x3x3",
  ndim = 3,
  build = function(self)
    ...
  end,
})

The first string is the puzzle ID. This should have only letters, numbers, and underscores.

The next string is the puzzle name. In this case, it's the same as the puzzle ID, but it isn't always. For a puzzle like Eitan's Star, the name should be "Eitan's Star" but the ID would have to be 'eitans_star' since it can't contain spaces or apostrophes.

Why the inconsistent quotation marks?

Lua doesn't actually care whether you use single quotes 'like this' or double quotes "like this", except that you can only put literal quotes inside a string with the opposite quotation type, 'like "this"' or "like 'this'". I like to use single quotes for non-user-facing strings, like the puzzle ID (since single quotes are slightly easier to type), and double quotes for user-facing strings (because they might contain apostrophes). Plus it gives a hint to anyone reading the code about the semantics of the string: is it user-facing, or just a string ID for use within code?

Then we have the number of dimensions, which should be pretty self-explanatory.

Lastly, we have the build function, which runs when our puzzle is constructed. It takes a single argument, the puzzle, which we call p.

Symmetry

Carving the base shape

At the beginning of the build function, our puzzle has a single piece that takes up all of 3D space.1 Think of it like a really big block of marble that we can carve() our shape from.

We could carve out each face individually ...

Please don't do this
self:carve(vec(1, 0, 0))
self:carve(vec(-1, 0, 0))
self:carve(vec(0, 1, 0))
self:carve(vec(0, -1, 0))
self:carve(vec(0, 0, 1))
self:carve(vec(0, 0, -1))

But since the puzzle is symmetric, we can use symmtery to simplify this. By starting with one face and iterating over its orbit, we can generate the other five.

We start by generating the group \(BC_3\) (which you may recognize from the last section as the symmetry group of a cube) using the global cd() constructor.

Carving a cube using a loop
local sym = cd'bc3'

for _, v in sym:orbit

Adding axes

Adding twists


  1. Ok technically it's just a very large primordial cube that takes up a lot of 3D space, not all of it. The current iteration of the shape-cutting algorithm only knows how to handle finite shapes.