I implemented this Rubik's Cube game in Python, using Canvas and Context2d for user interaction. This was mainly an exercise in "DRY" principles, which were quite difficult to follow: the code really wanted to have long repeating sections spelling out the geometry of the cube or the types of different moves, which were suppressed at some loss to readability.
import numpy as np
import json
from scipy.linalg import logm, expm
import itertools
def rigid_perms(D, prefix=[]):
 return (
 sum(
 (
 rigid_perms(D, prefix + [i])
 for i in range(len(D))
 if (
 D[prefix + [i]] @ D[i] == D[: len(prefix) + 1] @ D[len(prefix)]
 ).all()
 ),
 [],
 )
 if len(prefix) < len(D)
 else [np.eye(len(D))[prefix]]
 )
def np2js(arr):
 real_arr = np.block([[arr.real, arr.imag], [-arr.imag, arr.real]])
 return json.dumps(real_arr.tolist())
sticker_coords = np.array(
 [
 s
 for s in itertools.product(range(-2, 3), repeat=3)
 if (np.abs(s) == 2).sum() == 1
 ]
)
sticker_colors = 127 + 127 * np.round(sticker_coords / 2)
global_perms = rigid_perms(sticker_coords)
slice_perms = rigid_perms(sticker_coords[:21])
move = np.eye(len(sticker_coords))
move[:21, :21] = slice_perms[3]
all_moves = []
for perm in global_perms:
 new_move = perm @ move @ perm.T
 if any( (new_move == m).all() for m in all_moves ):
 continue
 all_moves.append(new_move)
 all_moves.append(new_move.T)
view = np.linalg.qr(np.random.randn(3, 3))[0]
with open("output.html", "w") as static_site:
 static_site.write(
 f"""
 <!DOCTYPE html>
 <html>
 <body>
 <canvas id="canvas" height=500 width=500></canvas>
 <script>
 const ctx = document.getElementById("canvas").getContext('2d');
 let mul = (A, B) => A.map((row, i) => B[0].map((_, j) =>
 row.reduce((acc, _, n) => acc + A[i][n] * B[n][j], 0)))
 var state = {np2js(np.eye(len(sticker_coords)))}
 const coords = {np2js(sticker_coords @ view * 70 + 255)}
 var moves = [state]
 document.addEventListener("keypress", (event) => {{
 """
 )
 for i, generator in enumerate(all_moves[::2]):
 static_site.write(
 f"""
 if (event.key == {i}) {{
 moves = (new Array(10).fill( {np2js(expm(.1 * logm(generator)))})).concat( moves);
 }}
 """
 )
 static_site.write(
 """
 });
 step = () => {
 if (!moves.length) {
 requestAnimationFrame(step)
 return;
 }
 state = mul(state, moves.pop());
 const locations = mul(state, coords);
 ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
 drawlist = [
 """
 )
 for i, color in enumerate(sticker_colors):
 static_site.write(
 f"""
 [...locations[{i}], 'rgb({color[0]} {color[1]} {color[2]})'], 
 """
 )
 static_site.write(
 """
 ].sort((a, b) => a[0]-b[0])
 for (var elem of drawlist){
 ctx.beginPath()
 ctx.arc(elem[1], elem[2], 40, 0, 2 * Math.PI)
 ctx.fillStyle=elem[6]
 ctx.fill()
 ctx.stroke()
 }
 requestAnimationFrame(step);
 }
 requestAnimationFrame(step);
 </script>
 </body>
 </html>
 """
 )
- 
 1\$\begingroup\$ I tried running it in the browser. The number keys move the rubics cube in some way, it would be nice if that is actually displayed in the document. The movements are quite slow and illegal rotations are allowed.I would expect the rubics cube to move until completion (or to nearest valid state) \$\endgroup\$AccidentalTaylorExpansion– AccidentalTaylorExpansion2025年08月22日 08:06:43 +00:00Commented Aug 22 at 8:06
3 Answers 3
Documentation
The PEP 8 style guide recommends adding docstrings for functions and the main code.
It would be helpful to add a docstring at the top of the code to summarize:
- The purpose of the code: expand upon what you mean by "Rubik's Cube game"
- Its output: how the output file is expected to be used
For example:
"""
Rubik's Cube game
What the code does...
Output file "output.html" ...
"""
For the functions, again, the docstrings should describe the input and return types
as well as what the function does and how it does it. For rigid_perms,
it would be helpful to mention that it is recursive.
Also consider using type hints for the functions to make the code more self-documenting.
Naming
The function names are a bit cryptic. rigid_perms could be rigid_permutations (if that is what "perms" stands for).
np2js could be numpy2javascript.
Magic numbers
You could either add comments or create named constants for the numbers 127 and 21.
Function is complicated
That mul function is quite busy:
let mul = (A, B) => A.map((row, i) => B[0].map((_, j) => row.reduce((acc, _, n) => acc + A[i][n] * B[n][j], 0)))
It seems a bit convoluted for one to "grok"...
Converted to anonymous functions that would be:
const mul = function (A, B) {
 return A.map(function (row, i) {
 return B[0].map(function (_, j) {
 return row.reduce(function (acc, _, n) {
 return acc + A[i][n] * B[n][j]
 }, 0)
 })
 })
}
Are you certain that is what is needed? If so that is fine, otherwise there may be a potential to simplify it.
Destructuring assignment can simplify code
One can use Destructuring to assign the x and y values passed to the arc() method:
 for (var elem of drawlist){
 ctx.beginPath()
 ctx.arc(elem[1], elem[2], 40, 0, 2 * Math.PI)
 ctx.fillStyle=elem[6]
Can be simplified to:
 for (const [x, y,,,,,style] of drawlist){
 ctx.beginPath()
 ctx.arc(x, y, 40, 0, 2 * Math.PI)
 ctx.fillStyle = style
- 
 \$\begingroup\$ JS arrays are 0-indexed, so your last destructuring example is missing a leading comma:) \$\endgroup\$STerliakov– STerliakov2025年08月24日 11:21:44 +00:00Commented Aug 24 at 11:21
Your JS variables are declared using a mix of const (ctx, coords, locations), let (mul), var (state, moves, elem) and global (step).
var is a thing of the past, and functions like mul and step should use either const or, preferably, function.
Aside from that, the embedded code is not very well formatted. Some HTML attributes have quotes, some don't. Some JS statements have semicolons, some don't.
You must log in to answer this question.
Explore related questions
See similar questions with these tags.