Quantum Cube is a timer app for speed cubers. Users create sessions and record their solve times, and track their statistics over time. The solves are stored in the cloud and are accessible on Quantum Cube's Android, iOS, and web apps. The simple, elegant design gets out of the user's way, so they can test their skills and prepare for competitions.

1,811,707
Backend Node.js, Express, Redis, PostgreSQL
Web Frontend React, Redux, Sass
Mobile Frontend Flutter, Bloc, Isar DB
Puzzles Supported 2x2x2, 3x3x3, 4x4x4, 5x5x5, 6x6x6, 7x7x7, Pyraminx, Megaminx, Skewb, Square-1, Clock
Solves Stored in the Cloud Over 1.8 Million

Features

Cloud Storage for Solve Times

Quantum Cube's source of truth is a cloud-hosted PostgreSQL database. It uses Redis for server-side session storage and lightweight caching. An Express.js JSON API provides a consistent interface for both web and mobile clients.

React Single-Page Application

On the web, QC's React SPA provides a smooth, responsive user experience. Cubing sessions can be long or short, with solve data and statistics updating continuously in real time. A redux store serves nicely to manage application state and keep the UI in sync with the backend.

Flutter Mobile App

Quantum Cube makes solve data avilable on any device, anywhere in the world. With the Flutter framework, a single codebase compiles to native code for nearly any device or platform. The mobile app also supports offline mode, so cubers can take their solves on the go. The Flutter Bloc state management library and Isar local database help to make data access appear nearly instantaneous.

Engineering Challenges

Offline mode -- a distributed data problem

The Challenge:

Imagine beating your personal best time, only to find out that your phone is offline and the solve data is lost. Unacceptable! Quantum Cube anticipates that users may have limited or no internet access. How can an app tolerate unreliable in networks while preserving data integrity?

The Solution:

To ensure data integrity and availability, Quantum Cube uses a distributed data system. A copy of the user's data is kept in local device storage, then asynchronously replicated in the cloud. The process generally works like this:

  1. Every new solve is created offline first.
  2. The client then attempts to POST the new solve to the server. If successful, the API responds with the new solve object, and this is added to the local database.
  3. Network timeouts and failed connections throw the app into "UserOffline" state. In this state, solves can be created locally, but not modified or deleted.
  4. While in "UserOffline" state, the device periodically tries to reconnect (polling)
  5. Once connection is established, the device re-authenticates with the server. It pulls the latest data down and pushes up any 'offline' solves.

For local storage, QC uses an Isar NoSQL database, which supports multiple platforms and is written in Rust. This approach comes nice features -- advanced querying, type safety, and the ability to subscribe to streams of data changes.

One common problem with data replication is how to handle conflicting updates and deletes. Quantum Cube works around this by following a simple rule: offline devices may create or modify offline data, but can never modify online data. This is suitable for our use-case, since cubers rarely need to edit their solves after they are created.

Cube scrambles and solutions -- search algorithms in Dart

The Challenge:

A core feature of Quantum Cube is generating scramble instructions for the user to solve. Scrambles tell the user how to mix up the cube before starting a solve. Scramble instructions are presented in 'scramble notation', which may look something like this:

U2 L2 D2 U R U' L2 D U L' R' F R U' L2 F D' U2 L F'
For more info on scramble notation, see this in-depth explanation

If the scrambles are not random enough, users may complain that the puzzles are too easy, or don't accurately simulate a competition. On the other hand, if the scramble is too random, it may contain invalid or non-sensical moves. For example, the sequence

L R L' R2
may be appear to be valid, but it results in the same state as simply doing:
R'

The Solution:

Because Dart is a relatively new language, it lacks the cubing-related libraries available in the JavaScript ecosystem. For Quantum Cube Flutter, it was necessary to implement scramble generators from scratch in Dart. Luckily, there are many excellent resources out there on how to model Rubik's cubes, such as this one.

In practice there are two approaches to generating scrambles.

1. A List of Random Moves
  1. Generate a list of random numeric values with a fixed length. The length will depend on the puzzle type.
  2. Map each number to a move in the scramble notation. It may be useful to use a lookup table for this.
  3. Validate the moves in the sequence to prevent duplication, reversals, or orthoganal moves (e.g., U D or B' F2)

This approach is naive, but can work well for small, structured puzzles such as 'Clock' or 'Pyraminx'.

2. A Cube Solver Algorithm
  1. Model the puzzle and the valid moves in a data structure. For example, a 2D array representing the faces and stickers of a cube.
  2. Generate a random starting state. It's not important what the scramble for this state is, but it must be a valid state for the puzzle.
  3. Use a depth-first search algorithm -- recursively testing sequences of moves until an optimal solution is found.

For more complex puzzles, such as the 3x3x3, the second approach is more suitable. It provides consistent randomness, while guaranteeing a valid set of moves. However, the DFS can quickly become time and memory constrained with complex puzzles. A common approach is to break the solution into "phases", where each phase solves a subset of the puzzle. Kociemba's 2-phase algorithm is a famous example.