Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions .flake8

This file was deleted.

29 changes: 0 additions & 29 deletions .github/workflows/ci.yml

This file was deleted.

30 changes: 30 additions & 0 deletions .github/workflows/test.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
name: Tests

on: [push]

jobs:
lint-and-test:
runs-on: "ubuntu-latest"
strategy:
max-parallel: 4
matrix:
python-version: ["3.12", "3.13"]

steps:
- uses: actions/checkout@v3
- name: Install just
uses: extractions/setup-just@v2
- name: Install the latest version of uv
uses: astral-sh/setup-uv@v3
with:
version: "latest"
- name: Cache generated tables
uses: actions/cache@v4
with:
path: src/twophase/tables/.cache
key: tables-v1-${{ runner.os }}-${{ runner.arch }}-${{ matrix.python-version }}-${{ hashFiles('src/twophase/constants.py', 'src/twophase/pieces.py', 'src/twophase/tables/**/*.py') }}
- name: Lint code
if: matrix.python-version == '3.13'
run: just lint
- name: Run tests
run: just python_version=${{ matrix.python-version }} test
14 changes: 6 additions & 8 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
# python artifacts
dist
__pycache__
*.so
*.pyc
*.egg-info
.ipynb_checkpoints

# build and test artifacts
.nox
dist

# move and pruning tables
tables.json
# legacy, code no longer generates this
tables.pkl
# cached tables
*.bin
76 changes: 54 additions & 22 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,30 +1,53 @@
# cube-solver
# twophase

A pure Python implementation of Herbert Kociemba's two-phase algorithm for solving the Rubik's Cube
A pure-Python implementation of Herbert Kociemba's two-phase algorithm for
solving a Rubik's Cube.

## Installation

Requires Python 3. Install with
Requires Python 3.12 or later. Add it to your `uv` project directly from GitHub:

```sh
pip install git+https://github.com/tcbegley/cube-solver.git
uv add git+https://github.com/tcbegley/cube-solver.git
```

Note that depending on how your system is configured, you may need to replace `pip` with `pip3` in the above command to install for Python 3.
Without uv, install it with pip instead:

```sh
pip install git+https://github.com/tcbegley/cube-solver.git
```

## Usage

To solve a cube, just import the `solve` method and pass a cube string.
Pass `solve` a 54-character string that describes the cube's stickers:

```python
from twophase import solve

solve("<cube_string>")
cube = "UUUUUUUUUBBBRRRRRRRRRFFFFFFDDDDDDDDDFFFLLLLLLLLLBBBBBB"
print(solve(cube)) # U'
```

Where the cube string is a 54 character string, consisting of the characters U, R, F, D, L, B (corresponding to the Upper, Right, Front, Down, Left and Back faces). Each character corresponds to one of the 54 stickers on the cube:
> [!IMPORTANT]
> The first solve generates the coordinate and pruning tables used by the
> search. This can take several minutes. The tables are cached for later runs;
> pass `cache_path` to `solve` or `solve_progressively` to store them elsewhere.

The result is a space-separated sequence in standard cube notation (`U`, `R2`,
`F'`, and so on). `solve` returns `None` when it cannot find a solution within
its limits; use `max_length` and `timeout` to adjust the default 20-move and
10-second limits.

```python
solution = solve(cube, max_length=22, timeout=30)
```

```plaintext
### Cube strings

A cube string contains the sticker colours `U`, `R`, `F`, `D`, `L`, and `B` in
this order: U1–U9, R1–R9, F1–F9, D1–D9, L1–L9, B1–B9.

```text
|------------|
|-U1--U2--U3-|
|------------|
Expand All @@ -46,24 +69,33 @@ Where the cube string is a 54 character string, consisting of the characters U,
|------------|
```

and should be specified in the order U1-U9, R1-R9, F1-F9, D1-D9, L1-L9, B1-B9.
For example, a solved cube is:

For example, a completely solved cube is represented by the string `"UUUUUUUUURRRRRRRRRFFFFFFFFFDDDDDDDDDLLLLLLLLLBBBBBBBBB"`.
```text
UUUUUUUUURRRRRRRRRFFFFFFFFFDDDDDDDDDLLLLLLLLLBBBBBBBBB
```

`solve` will return a solution unless timeout has been reached (default is 10 seconds). Typically it will find a solution very quickly unless you set a low upper bound on the number of moves allowed. Note that the first time you run `solve`, it will precompute move tables needed for the solution which might take ~1 minute. Subsequent runs will be much faster.
### Finding shorter solutions

If you want to keep searching for better solutions, use the `solve_best` or
`solve_best_generator` functions. `solve_best` reduces `max_length` each time a
solution is found and continues searching for a better solution. All solutions
found are returned in a list at the end. `solve_best_generator` creates a
generator that yields solutions as they are found.
`solve_progressively` yields increasingly shorter solutions until it reaches
the time limit:

```python
from twophase import solve_best, solve_best_generator
from twophase import solve_progressively

for solution in solve_progressively(cube, timeout=30):
print(solution)
```

### Lower-level interface

# returns a list of solutions
solve_best("<cube_string>")
For applications that need the cube model or moves rather than a formatted
string, use `parse`, `Solver`, and `format_moves`:

```python
from twophase import Solver, format_moves, parse

# creates a generator that yields solutions as they are found
solve_best_generator("<cube_string>")
moves = Solver().solve(parse(cube))
if moves is not None:
print(format_moves(moves))
```
13 changes: 13 additions & 0 deletions justfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
python_version := "3.12"

lint:
uv run ruff check
uv run ruff format --check
uv run ty check

format:
uv run ruff check --fix
uv run ruff format

test:
uv run --python {{ python_version }} pytest tests/
21 changes: 0 additions & 21 deletions noxfile.py

This file was deleted.

7 changes: 0 additions & 7 deletions poetry.lock

This file was deleted.

42 changes: 25 additions & 17 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,23 +1,31 @@
[build-system]
requires = [ "poetry>=0.12",]
build-backend = "poetry.masonry.api"
requires = ["uv_build>=0.12.7,<0.13.0"]
build-backend = "uv_build"

[tool.poetry]
[project]
name = "twophase"
version = "0.1.0"
description = "Rubik's cube solver implementing Kociemba's two-phase algorithm."
authors = [ "tcbegley <tomcbegley@gmail.com>",]
license = "MIT"
classifiers = [ "Development Status :: 3 - Alpha", "License :: OSI Approved :: MIT License",]
version = "0.2.0"
description = "An educational implementation of Kociemba's two-phase cube-solving algorithm."
readme = "README.md"
requires-python = ">=3.12"
dependencies = ["tqdm>=4.66.5"]

[tool.poetry.dependencies]
python = "^3.6"
[dependency-groups]
dev = [
"pytest>=8.4.0",
"ruff>=0.11.13",
"ty>=0.0.21",
]

[tool.black]
line-length = 79
target-version = [ "py36" ]
[tool.ruff]
target-version = "py312"
line-length = 88

[tool.isort]
include_trailing_comma = true
line_length = 79
multi_line_output = 3
[tool.ruff.format]
skip-magic-trailing-comma = true

[tool.ruff.lint.isort]
split-on-trailing-comma = false

[tool.ruff.lint]
select = ["F", "E", "W", "I001"]
29 changes: 29 additions & 0 deletions src/twophase/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
"""An educational implementation of Kociemba's two-phase cube solver."""

from importlib.metadata import PackageNotFoundError, version

from twophase.cubestring import parse
from twophase.interface import solve, solve_progressively
from twophase.pieces import Color, Corner, Edge, Facelet, Move, format_moves
from twophase.solve.solver import Solver
from twophase.tables.cubiecube import CubieCube

try:
__version__ = version("twophase")
except PackageNotFoundError:
__version__ = "0.2.0+fallback"

__all__ = [
"Color",
"Corner",
"CubieCube",
"Edge",
"Facelet",
"Move",
"Solver",
"__version__",
"format_moves",
"parse",
"solve",
"solve_progressively",
]
51 changes: 51 additions & 0 deletions src/twophase/constants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import math

from twophase.pieces import Move

TWIST_MAX = 3**7
FLIP_MAX = 2**11
UD_SLICE_MAX = math.comb(12, 4)
EDGE4_MAX = 24
EDGE8_MAX = 40_320
CORNER_MAX = 40_320

MOVES = 18
SYMMETRIES = 48
SYMMETRIES_D4H = 16
FLIP_UD_SLICE_CLASSES = 64430
CORNER_CLASSES = 2768

PHASE_1_MOVES = [
Move.U1,
Move.U2,
Move.U3,
Move.R1,
Move.R2,
Move.R3,
Move.F1,
Move.F2,
Move.F3,
Move.D1,
Move.D2,
Move.D3,
Move.L1,
Move.L2,
Move.L3,
Move.B1,
Move.B2,
Move.B3,
]
PHASE_2_PRUNE_UNFILLED_DEPTH = 11

PHASE_2_MOVES = [
Move.U1,
Move.U2,
Move.U3,
Move.R2,
Move.F2,
Move.D1,
Move.D2,
Move.D3,
Move.L2,
Move.B2,
]
Loading
Loading