10
\$\begingroup\$

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>
    """
    )
\$\endgroup\$
1
  • 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\$ Commented Aug 22 at 8:06

3 Answers 3

8
\$\begingroup\$

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.

\$\endgroup\$
8
\$\begingroup\$

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
\$\endgroup\$
1
  • \$\begingroup\$ JS arrays are 0-indexed, so your last destructuring example is missing a leading comma:) \$\endgroup\$ Commented Aug 24 at 11:21
7
\$\begingroup\$

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.

\$\endgroup\$

You must log in to answer this question.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.