cube-model 1.0.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- cube_model-1.0.3/PKG-INFO +45 -0
- cube_model-1.0.3/README.md +30 -0
- cube_model-1.0.3/pyproject.toml +28 -0
- cube_model-1.0.3/src/cube_model/__init__.py +86 -0
- cube_model-1.0.3/src/cube_model/action.py +189 -0
- cube_model-1.0.3/src/cube_model/model.py +99 -0
- cube_model-1.0.3/src/cube_model/move.py +167 -0
- cube_model-1.0.3/src/cube_model/navigation.py +155 -0
- cube_model-1.0.3/src/cube_model/new_cube.py +299 -0
- cube_model-1.0.3/src/cube_model/py.typed +0 -0
- cube_model-1.0.3/src/cube_model/reachable.py +194 -0
- cube_model-1.0.3/src/cube_model/rotate.py +104 -0
- cube_model-1.0.3/src/cube_model/utils.py +58 -0
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: cube-model
|
|
3
|
+
Version: 1.0.3
|
|
4
|
+
Summary: Orientation-independent cube model in strictly typed Python
|
|
5
|
+
Author: Yitzchak Gale
|
|
6
|
+
Author-email: gale@sefer.org
|
|
7
|
+
Requires-Python: >=3.13,<4.0
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
11
|
+
Project-URL: Homepage, https://github.com/ygale/cube-model
|
|
12
|
+
Project-URL: Repository, https://github.com/ygale/cube-model
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
|
|
15
|
+
# cube-model
|
|
16
|
+
|
|
17
|
+
An orientation-independent cube model in strictly typed Python.
|
|
18
|
+
|
|
19
|
+
## Highlights
|
|
20
|
+
|
|
21
|
+
- Strict typing (`mypy --strict`)
|
|
22
|
+
- Orientation-independent cube state
|
|
23
|
+
- `Color` and `Side` are enums
|
|
24
|
+
- `CornerSticker` objects form circular linked lists of size 3
|
|
25
|
+
- `EdgeSticker` objects form circular linked lists of size 2
|
|
26
|
+
- `Cube.next_edge` and `Cube.next_corner` encode clockwise sticker order on faces
|
|
27
|
+
|
|
28
|
+
## Install
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
pip install cube-model
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Example
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
from cube_model import Color, Move, Multiplicity, Side, solved, move
|
|
38
|
+
|
|
39
|
+
cube = solved()
|
|
40
|
+
assert cube.front_color is Color.GREEN
|
|
41
|
+
|
|
42
|
+
move(Move(Side.FRONT, Multiplicity.CW), cube)
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# cube-model
|
|
2
|
+
|
|
3
|
+
An orientation-independent cube model in strictly typed Python.
|
|
4
|
+
|
|
5
|
+
## Highlights
|
|
6
|
+
|
|
7
|
+
- Strict typing (`mypy --strict`)
|
|
8
|
+
- Orientation-independent cube state
|
|
9
|
+
- `Color` and `Side` are enums
|
|
10
|
+
- `CornerSticker` objects form circular linked lists of size 3
|
|
11
|
+
- `EdgeSticker` objects form circular linked lists of size 2
|
|
12
|
+
- `Cube.next_edge` and `Cube.next_corner` encode clockwise sticker order on faces
|
|
13
|
+
|
|
14
|
+
## Install
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
pip install cube-model
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Example
|
|
21
|
+
|
|
22
|
+
```python
|
|
23
|
+
from cube_model import Color, Move, Multiplicity, Side, solved, move
|
|
24
|
+
|
|
25
|
+
cube = solved()
|
|
26
|
+
assert cube.front_color is Color.GREEN
|
|
27
|
+
|
|
28
|
+
move(Move(Side.FRONT, Multiplicity.CW), cube)
|
|
29
|
+
```
|
|
30
|
+
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
[tool.poetry]
|
|
2
|
+
name = "cube-model"
|
|
3
|
+
version = "1.0.3"
|
|
4
|
+
description = "Orientation-independent cube model in strictly typed Python"
|
|
5
|
+
authors = ["Yitzchak Gale <gale@sefer.org>"]
|
|
6
|
+
readme = "README.md"
|
|
7
|
+
packages = [{ include = "cube_model", from = "src" }]
|
|
8
|
+
include = [{ path = "src/cube_model/py.typed", format = ["sdist", "wheel"] }]
|
|
9
|
+
homepage = "https://github.com/ygale/cube-model"
|
|
10
|
+
repository = "https://github.com/ygale/cube-model"
|
|
11
|
+
|
|
12
|
+
[tool.poetry.dependencies]
|
|
13
|
+
python = ">=3.13,<4.0"
|
|
14
|
+
|
|
15
|
+
[tool.poetry.group.dev.dependencies]
|
|
16
|
+
pytest = ">=8.0"
|
|
17
|
+
|
|
18
|
+
[tool.pytest.ini_options]
|
|
19
|
+
testpaths = ["tests"]
|
|
20
|
+
pythonpath = ["src", "tests"]
|
|
21
|
+
|
|
22
|
+
[build-system]
|
|
23
|
+
requires = ["poetry-core>=1.9.0"]
|
|
24
|
+
build-backend = "poetry.core.masonry.api"
|
|
25
|
+
|
|
26
|
+
[tool.mypy]
|
|
27
|
+
mypy_path = ["src", "tests"]
|
|
28
|
+
explicit_package_bases = true
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
from .new_cube import new_cube, shuffled, solved
|
|
2
|
+
from .model import (
|
|
3
|
+
Color,
|
|
4
|
+
Side,
|
|
5
|
+
Sticker,
|
|
6
|
+
CornerSticker,
|
|
7
|
+
EdgeSticker,
|
|
8
|
+
Cube,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
from .navigation import (
|
|
12
|
+
Nav,
|
|
13
|
+
nav,
|
|
14
|
+
nav_cc,
|
|
15
|
+
parse_navs,
|
|
16
|
+
HOME_TO_SIDE,
|
|
17
|
+
corner_on_side,
|
|
18
|
+
side_corners,
|
|
19
|
+
sticker_side,
|
|
20
|
+
side_color,
|
|
21
|
+
color_side,
|
|
22
|
+
all_colors,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
from .move import (
|
|
26
|
+
Multiplicity,
|
|
27
|
+
Move,
|
|
28
|
+
move,
|
|
29
|
+
moved,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
from .utils import (
|
|
33
|
+
permutation_cycles,
|
|
34
|
+
even_permutation,
|
|
35
|
+
rand_elt,
|
|
36
|
+
to_radix,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
from .reachable import (
|
|
40
|
+
locations_ok,
|
|
41
|
+
edge_flips_ok,
|
|
42
|
+
corner_rotations_ok,
|
|
43
|
+
reachable,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
from .rotate import (
|
|
47
|
+
rotate,
|
|
48
|
+
rotated,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
__all__ = [
|
|
52
|
+
'Color',
|
|
53
|
+
'Side',
|
|
54
|
+
'Sticker',
|
|
55
|
+
'CornerSticker',
|
|
56
|
+
'EdgeSticker',
|
|
57
|
+
'Cube',
|
|
58
|
+
'new_cube',
|
|
59
|
+
'shuffled',
|
|
60
|
+
'solved',
|
|
61
|
+
'Nav',
|
|
62
|
+
'nav',
|
|
63
|
+
'nav_cc',
|
|
64
|
+
'parse_navs',
|
|
65
|
+
'HOME_TO_SIDE',
|
|
66
|
+
'corner_on_side',
|
|
67
|
+
'side_corners',
|
|
68
|
+
'sticker_side',
|
|
69
|
+
'side_color',
|
|
70
|
+
'color_side',
|
|
71
|
+
'all_colors',
|
|
72
|
+
'Multiplicity',
|
|
73
|
+
'Move',
|
|
74
|
+
'move',
|
|
75
|
+
'moved',
|
|
76
|
+
'permutation_cycles',
|
|
77
|
+
'even_permutation',
|
|
78
|
+
'rand_elt',
|
|
79
|
+
'to_radix',
|
|
80
|
+
'locations_ok',
|
|
81
|
+
'edge_flips_ok',
|
|
82
|
+
'corner_rotations_ok',
|
|
83
|
+
'reachable',
|
|
84
|
+
'rotate',
|
|
85
|
+
'rotated',
|
|
86
|
+
]
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
'''Standard cube move notation: actions, parsing, and dispatch.
|
|
2
|
+
|
|
3
|
+
Action is the union Move | Rotation | WideMove | SliceMove.
|
|
4
|
+
Rotation, WideMove, and SliceMove are frozen dataclasses wrapping a
|
|
5
|
+
Move, distinct concrete types at runtime while serving as newtypes of
|
|
6
|
+
Move at the type-checker level.
|
|
7
|
+
|
|
8
|
+
act(action, cube) applies an action in place.
|
|
9
|
+
acted(action, cube) returns a new cube with the action applied.
|
|
10
|
+
|
|
11
|
+
Decompositions:
|
|
12
|
+
WideMove(m) — rotate(m, cube); move(Move(opp_side[m.face], m.mult), cube)
|
|
13
|
+
SliceMove(m) — rotate(m, cube); move(Move(opp_side[m.face], m.mult), cube);
|
|
14
|
+
move(Move(m.face, invert[m.mult]), cube)
|
|
15
|
+
|
|
16
|
+
parse_actions(s, ci=False) parses standard cube move notation.
|
|
17
|
+
Tokens may be written with or without spaces between them. Each token
|
|
18
|
+
is a base letter followed by an optional modifier: nothing (CW),
|
|
19
|
+
apostrophe (CCW), or 2 (TWO).
|
|
20
|
+
|
|
21
|
+
Standard notation:
|
|
22
|
+
Face moves: U D F B L R
|
|
23
|
+
Rotations: x y z
|
|
24
|
+
Wide moves: u d f b l r or Uw Dw Fw Bw Lw Rw
|
|
25
|
+
Slice moves: M E S
|
|
26
|
+
|
|
27
|
+
When ci=True, all letters are folded to uppercase. Bare lowercase
|
|
28
|
+
letters parse as face moves. Wide moves require the w or W suffix.
|
|
29
|
+
'''
|
|
30
|
+
|
|
31
|
+
from __future__ import annotations
|
|
32
|
+
|
|
33
|
+
from collections.abc import Iterator
|
|
34
|
+
from dataclasses import dataclass
|
|
35
|
+
|
|
36
|
+
from .model import Cube, Side, opp_side, shallow_copy
|
|
37
|
+
from .move import Move, Multiplicity, invert, move
|
|
38
|
+
from .rotate import rotate
|
|
39
|
+
|
|
40
|
+
@dataclass(frozen=True)
|
|
41
|
+
class Rotation:
|
|
42
|
+
'''A rigid cube rotation (x, y, z); newtype of Move.'''
|
|
43
|
+
move: Move
|
|
44
|
+
|
|
45
|
+
@dataclass(frozen=True)
|
|
46
|
+
class WideMove:
|
|
47
|
+
'''A wide two-layer move; newtype of Move.'''
|
|
48
|
+
move: Move
|
|
49
|
+
|
|
50
|
+
@dataclass(frozen=True)
|
|
51
|
+
class SliceMove:
|
|
52
|
+
'''A middle-layer slice move (M, E, S); newtype of Move.'''
|
|
53
|
+
move: Move
|
|
54
|
+
|
|
55
|
+
# Any action representable in standard cube notation.
|
|
56
|
+
type Action = Move | Rotation | WideMove | SliceMove
|
|
57
|
+
|
|
58
|
+
# An iterator of Action values.
|
|
59
|
+
type Actions = Iterator[Action]
|
|
60
|
+
|
|
61
|
+
_MULT_SUFFIX: dict[str, Multiplicity] = {
|
|
62
|
+
'': Multiplicity.CW,
|
|
63
|
+
"'": Multiplicity.CCW,
|
|
64
|
+
'2': Multiplicity.TWO,
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
_FACE_SIDE: dict[str, Side] = {
|
|
68
|
+
'U': Side.TOP,
|
|
69
|
+
'D': Side.BOTTOM,
|
|
70
|
+
'F': Side.FRONT,
|
|
71
|
+
'B': Side.BACK,
|
|
72
|
+
'L': Side.LEFT,
|
|
73
|
+
'R': Side.RIGHT,
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
_ROTATION_SIDE: dict[str, Side] = {
|
|
77
|
+
'x': Side.RIGHT,
|
|
78
|
+
'y': Side.TOP,
|
|
79
|
+
'z': Side.FRONT,
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
_WIDE_SIDE: dict[str, Side] = {
|
|
83
|
+
'u': Side.TOP,
|
|
84
|
+
'd': Side.BOTTOM,
|
|
85
|
+
'f': Side.FRONT,
|
|
86
|
+
'b': Side.BACK,
|
|
87
|
+
'l': Side.LEFT,
|
|
88
|
+
'r': Side.RIGHT,
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
_SLICE_SIDE: dict[str, Side] = {
|
|
92
|
+
'M': Side.LEFT,
|
|
93
|
+
'E': Side.BOTTOM,
|
|
94
|
+
'S': Side.FRONT,
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
class ParseError(ValueError):
|
|
98
|
+
'''Raised when a move token cannot be parsed.'''
|
|
99
|
+
|
|
100
|
+
def _parse_mult(s: str, pos: int) -> tuple[Multiplicity, int]:
|
|
101
|
+
'''Parse a multiplicity modifier at pos, returning (mult, new_pos).'''
|
|
102
|
+
if pos < len(s) and s[pos] == "'":
|
|
103
|
+
return Multiplicity.CCW, pos + 1
|
|
104
|
+
if pos < len(s) and s[pos] == '2':
|
|
105
|
+
return Multiplicity.TWO, pos + 1
|
|
106
|
+
return Multiplicity.CW, pos
|
|
107
|
+
|
|
108
|
+
def _parse_one(s: str, pos: int, ci: bool) -> tuple[Action, int]:
|
|
109
|
+
'''Parse one token from s starting at pos.
|
|
110
|
+
|
|
111
|
+
Returns (action, new_pos). Raises ParseError on invalid input.
|
|
112
|
+
'''
|
|
113
|
+
if pos >= len(s):
|
|
114
|
+
raise ParseError(f'unexpected end of input at position {pos}')
|
|
115
|
+
base: str = s[pos]
|
|
116
|
+
pos += 1
|
|
117
|
+
|
|
118
|
+
# w/W suffix: wide move. Valid for any face letter, either case.
|
|
119
|
+
if pos < len(s) and s[pos] in ('w', 'W'):
|
|
120
|
+
pos += 1
|
|
121
|
+
mult, pos = _parse_mult(s, pos)
|
|
122
|
+
upper: str = base.upper()
|
|
123
|
+
if upper not in _FACE_SIDE:
|
|
124
|
+
raise ParseError(
|
|
125
|
+
f'unknown wide-move face {base!r}'
|
|
126
|
+
)
|
|
127
|
+
return WideMove(Move(_FACE_SIDE[upper], mult)), pos
|
|
128
|
+
|
|
129
|
+
if ci:
|
|
130
|
+
upper = base.upper()
|
|
131
|
+
mult, pos = _parse_mult(s, pos)
|
|
132
|
+
if upper in _FACE_SIDE:
|
|
133
|
+
return Move(_FACE_SIDE[upper], mult), pos
|
|
134
|
+
if upper in ('X', 'Y', 'Z'):
|
|
135
|
+
return Rotation(Move(_ROTATION_SIDE[base.lower()], mult)), pos
|
|
136
|
+
if upper in ('M', 'E', 'S'):
|
|
137
|
+
return SliceMove(Move(_SLICE_SIDE[upper], mult)), pos
|
|
138
|
+
raise ParseError(f'unknown move letter {base!r}')
|
|
139
|
+
|
|
140
|
+
mult, pos = _parse_mult(s, pos)
|
|
141
|
+
if base in _FACE_SIDE:
|
|
142
|
+
return Move(_FACE_SIDE[base], mult), pos
|
|
143
|
+
if base in _ROTATION_SIDE:
|
|
144
|
+
return Rotation(Move(_ROTATION_SIDE[base], mult)), pos
|
|
145
|
+
if base in _WIDE_SIDE:
|
|
146
|
+
return WideMove(Move(_WIDE_SIDE[base], mult)), pos
|
|
147
|
+
if base in _SLICE_SIDE:
|
|
148
|
+
return SliceMove(Move(_SLICE_SIDE[base], mult)), pos
|
|
149
|
+
raise ParseError(f'unknown move letter {base!r}')
|
|
150
|
+
|
|
151
|
+
def parse_actions(s: str, ci: bool = False) -> list[Action]:
|
|
152
|
+
'''Parse standard cube move notation into a list of actions.
|
|
153
|
+
|
|
154
|
+
Tokens may be separated by optional whitespace. Each token is a
|
|
155
|
+
base letter followed by an optional modifier: nothing (CW),
|
|
156
|
+
apostrophe (CCW), or 2 (TWO).
|
|
157
|
+
|
|
158
|
+
Raises ParseError on unrecognised letters or modifiers.
|
|
159
|
+
'''
|
|
160
|
+
actions: list[Action] = []
|
|
161
|
+
pos: int = 0
|
|
162
|
+
while pos < len(s):
|
|
163
|
+
if s[pos].isspace():
|
|
164
|
+
pos += 1
|
|
165
|
+
continue
|
|
166
|
+
action, pos = _parse_one(s, pos, ci)
|
|
167
|
+
actions.append(action)
|
|
168
|
+
return actions
|
|
169
|
+
|
|
170
|
+
def act(action: Action, cube: Cube) -> None:
|
|
171
|
+
'''Apply an action to a cube in place.'''
|
|
172
|
+
match action:
|
|
173
|
+
case Rotation(move=m):
|
|
174
|
+
rotate(m, cube)
|
|
175
|
+
case WideMove(move=m):
|
|
176
|
+
rotate(m, cube)
|
|
177
|
+
move(Move(opp_side[m.face], m.mult), cube)
|
|
178
|
+
case SliceMove(move=m):
|
|
179
|
+
rotate(m, cube)
|
|
180
|
+
move(Move(opp_side[m.face], m.mult), cube)
|
|
181
|
+
move(Move(m.face, invert[m.mult]), cube)
|
|
182
|
+
case Move() as m:
|
|
183
|
+
move(m, cube)
|
|
184
|
+
|
|
185
|
+
def acted(action: Action, cube: Cube) -> Cube:
|
|
186
|
+
'''Return a new cube with an action applied, leaving the original unchanged.'''
|
|
187
|
+
new: Cube = shallow_copy(cube)
|
|
188
|
+
act(action, new)
|
|
189
|
+
return new
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
'''Core cube model: colors, sides, stickers, and the Cube dataclass.'''
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from abc import ABC
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
from enum import Enum, auto
|
|
8
|
+
|
|
9
|
+
class Color(Enum):
|
|
10
|
+
'''Enumeration of the six cube colors.'''
|
|
11
|
+
WHITE = auto()
|
|
12
|
+
YELLOW = auto()
|
|
13
|
+
RED = auto()
|
|
14
|
+
ORANGE = auto()
|
|
15
|
+
BLUE = auto()
|
|
16
|
+
GREEN = auto()
|
|
17
|
+
|
|
18
|
+
class Side(Enum):
|
|
19
|
+
'''Enumeration of the six cube sides.'''
|
|
20
|
+
FRONT = auto()
|
|
21
|
+
BACK = auto()
|
|
22
|
+
LEFT = auto()
|
|
23
|
+
RIGHT = auto()
|
|
24
|
+
TOP = auto()
|
|
25
|
+
BOTTOM = auto()
|
|
26
|
+
|
|
27
|
+
@dataclass(eq=False)
|
|
28
|
+
class Sticker(ABC):
|
|
29
|
+
'''Abstract sticker with a color and cyclic partner.'''
|
|
30
|
+
color: Color
|
|
31
|
+
other: Sticker = field(init=False)
|
|
32
|
+
_hash: int = field(init=False, repr=False, compare=False)
|
|
33
|
+
|
|
34
|
+
def __post_init__(self) -> None:
|
|
35
|
+
'''Initialize as a self-loop before wiring.'''
|
|
36
|
+
self._rewire(self)
|
|
37
|
+
|
|
38
|
+
def _rewire(self, other: Sticker) -> None:
|
|
39
|
+
'''Set partner and recompute hash.'''
|
|
40
|
+
self.other = other
|
|
41
|
+
self._hash = hash((self.color, other.color))
|
|
42
|
+
|
|
43
|
+
def __eq__(self, other: object) -> bool:
|
|
44
|
+
'''Equality based on color pair.'''
|
|
45
|
+
if not isinstance(other, Sticker):
|
|
46
|
+
return NotImplemented
|
|
47
|
+
return (
|
|
48
|
+
self.color == other.color
|
|
49
|
+
and self.other.color == other.other.color
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
def __hash__(self) -> int:
|
|
53
|
+
'''Hash based on this sticker's color and its partner's color.'''
|
|
54
|
+
return self._hash
|
|
55
|
+
|
|
56
|
+
@dataclass(eq=False)
|
|
57
|
+
class CornerSticker(Sticker):
|
|
58
|
+
'''Corner sticker linked in a 3-cycle.'''
|
|
59
|
+
other: CornerSticker = field(init=False)
|
|
60
|
+
|
|
61
|
+
@dataclass(eq=False)
|
|
62
|
+
class EdgeSticker(Sticker):
|
|
63
|
+
'''Edge sticker linked in a 2-cycle.'''
|
|
64
|
+
other: EdgeSticker = field(init=False)
|
|
65
|
+
|
|
66
|
+
@dataclass
|
|
67
|
+
class Cube:
|
|
68
|
+
'''Cube defined by one corner and cyclic adjacency maps.'''
|
|
69
|
+
home: CornerSticker
|
|
70
|
+
front_color: Color
|
|
71
|
+
top_color: Color
|
|
72
|
+
next_edge: dict[CornerSticker, EdgeSticker]
|
|
73
|
+
next_corner: dict[EdgeSticker, CornerSticker]
|
|
74
|
+
|
|
75
|
+
# Opposite face for each side.
|
|
76
|
+
opp_side: dict[Side, Side] = {
|
|
77
|
+
Side.LEFT: Side.RIGHT,
|
|
78
|
+
Side.RIGHT: Side.LEFT,
|
|
79
|
+
Side.BOTTOM: Side.TOP,
|
|
80
|
+
Side.TOP: Side.BOTTOM,
|
|
81
|
+
Side.BACK: Side.FRONT,
|
|
82
|
+
Side.FRONT: Side.BACK,
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
def shallow_copy(cube: Cube) -> Cube:
|
|
86
|
+
'''Return a new Cube sharing all sticker objects but with new dicts.
|
|
87
|
+
|
|
88
|
+
Moves only reassign dict values (which sticker a key maps to); they
|
|
89
|
+
never mutate sticker objects themselves. A shallow copy therefore
|
|
90
|
+
gives full independence between the two cubes without allocating new
|
|
91
|
+
sticker objects.
|
|
92
|
+
'''
|
|
93
|
+
return Cube(
|
|
94
|
+
home=cube.home,
|
|
95
|
+
front_color=cube.front_color,
|
|
96
|
+
top_color=cube.top_color,
|
|
97
|
+
next_edge=dict(cube.next_edge),
|
|
98
|
+
next_corner=dict(cube.next_corner),
|
|
99
|
+
)
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
'''Face moves implemented via local sticker cycles.'''
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from enum import Enum, auto
|
|
7
|
+
|
|
8
|
+
from .model import CornerSticker, Cube, EdgeSticker, Side, shallow_copy
|
|
9
|
+
from .navigation import (
|
|
10
|
+
Nav,
|
|
11
|
+
nav_cc,
|
|
12
|
+
corner_on_side,
|
|
13
|
+
side_corners,
|
|
14
|
+
parse_navs,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
class Multiplicity(Enum):
|
|
18
|
+
'''Move multiplicity.'''
|
|
19
|
+
CW = auto()
|
|
20
|
+
CCW = auto()
|
|
21
|
+
TWO = auto()
|
|
22
|
+
|
|
23
|
+
# Inverse multiplicity: CW<->CCW, TWO is self-inverse.
|
|
24
|
+
invert: dict[Multiplicity, Multiplicity] = {
|
|
25
|
+
Multiplicity.CW: Multiplicity.CCW,
|
|
26
|
+
Multiplicity.CCW: Multiplicity.CW,
|
|
27
|
+
Multiplicity.TWO: Multiplicity.TWO,
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
@dataclass(frozen=True)
|
|
31
|
+
class Move:
|
|
32
|
+
'''A face move: a side and a multiplicity.'''
|
|
33
|
+
face: Side
|
|
34
|
+
mult: Multiplicity
|
|
35
|
+
|
|
36
|
+
restore_home: dict[tuple[Side, Multiplicity], list[Nav]] = {
|
|
37
|
+
(Side.FRONT, Multiplicity.CW): parse_navs('ONNO'),
|
|
38
|
+
(Side.FRONT, Multiplicity.CCW): parse_navs('NN'),
|
|
39
|
+
(Side.FRONT, Multiplicity.TWO): parse_navs('NNNN'),
|
|
40
|
+
|
|
41
|
+
(Side.LEFT, Multiplicity.CW): parse_navs('OONN'),
|
|
42
|
+
(Side.LEFT, Multiplicity.CCW): parse_navs('OONNOO'),
|
|
43
|
+
(Side.LEFT, Multiplicity.TWO): parse_navs('ONNNNOO'),
|
|
44
|
+
|
|
45
|
+
(Side.TOP, Multiplicity.CW): parse_navs('NNOO'),
|
|
46
|
+
(Side.TOP, Multiplicity.CCW): parse_navs('OONNO'),
|
|
47
|
+
(Side.TOP, Multiplicity.TWO): parse_navs('OONNNNO'),
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
def _move_corner(
|
|
51
|
+
cube: Cube, corner: CornerSticker, m: Multiplicity
|
|
52
|
+
) -> None:
|
|
53
|
+
'''Rotate the face of a corner with given multiplicity.
|
|
54
|
+
|
|
55
|
+
Note: caller may need to restore cube.home afterwards.
|
|
56
|
+
'''
|
|
57
|
+
corners: list[CornerSticker] = side_corners(cube, corner)
|
|
58
|
+
|
|
59
|
+
corners_oo: list[CornerSticker] = [
|
|
60
|
+
c.other.other for c in corners
|
|
61
|
+
]
|
|
62
|
+
|
|
63
|
+
edges_no: list[EdgeSticker] = [
|
|
64
|
+
cube.next_edge[c].other for c in corners_oo
|
|
65
|
+
]
|
|
66
|
+
|
|
67
|
+
match m:
|
|
68
|
+
case Multiplicity.CW:
|
|
69
|
+
(
|
|
70
|
+
cube.next_edge[corners_oo[0]],
|
|
71
|
+
cube.next_edge[corners_oo[1]],
|
|
72
|
+
cube.next_edge[corners_oo[2]],
|
|
73
|
+
cube.next_edge[corners_oo[3]],
|
|
74
|
+
) = (
|
|
75
|
+
cube.next_edge[corners_oo[1]],
|
|
76
|
+
cube.next_edge[corners_oo[2]],
|
|
77
|
+
cube.next_edge[corners_oo[3]],
|
|
78
|
+
cube.next_edge[corners_oo[0]],
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
(
|
|
82
|
+
cube.next_corner[edges_no[0]],
|
|
83
|
+
cube.next_corner[edges_no[1]],
|
|
84
|
+
cube.next_corner[edges_no[2]],
|
|
85
|
+
cube.next_corner[edges_no[3]],
|
|
86
|
+
) = (
|
|
87
|
+
cube.next_corner[edges_no[3]],
|
|
88
|
+
cube.next_corner[edges_no[0]],
|
|
89
|
+
cube.next_corner[edges_no[1]],
|
|
90
|
+
cube.next_corner[edges_no[2]],
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
case Multiplicity.CCW:
|
|
94
|
+
(
|
|
95
|
+
cube.next_edge[corners_oo[0]],
|
|
96
|
+
cube.next_edge[corners_oo[1]],
|
|
97
|
+
cube.next_edge[corners_oo[2]],
|
|
98
|
+
cube.next_edge[corners_oo[3]],
|
|
99
|
+
) = (
|
|
100
|
+
cube.next_edge[corners_oo[3]],
|
|
101
|
+
cube.next_edge[corners_oo[0]],
|
|
102
|
+
cube.next_edge[corners_oo[1]],
|
|
103
|
+
cube.next_edge[corners_oo[2]],
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
(
|
|
107
|
+
cube.next_corner[edges_no[0]],
|
|
108
|
+
cube.next_corner[edges_no[1]],
|
|
109
|
+
cube.next_corner[edges_no[2]],
|
|
110
|
+
cube.next_corner[edges_no[3]],
|
|
111
|
+
) = (
|
|
112
|
+
cube.next_corner[edges_no[1]],
|
|
113
|
+
cube.next_corner[edges_no[2]],
|
|
114
|
+
cube.next_corner[edges_no[3]],
|
|
115
|
+
cube.next_corner[edges_no[0]],
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
case Multiplicity.TWO:
|
|
119
|
+
(
|
|
120
|
+
cube.next_edge[corners_oo[0]],
|
|
121
|
+
cube.next_edge[corners_oo[2]],
|
|
122
|
+
) = (
|
|
123
|
+
cube.next_edge[corners_oo[2]],
|
|
124
|
+
cube.next_edge[corners_oo[0]],
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
(
|
|
128
|
+
cube.next_edge[corners_oo[1]],
|
|
129
|
+
cube.next_edge[corners_oo[3]],
|
|
130
|
+
) = (
|
|
131
|
+
cube.next_edge[corners_oo[3]],
|
|
132
|
+
cube.next_edge[corners_oo[1]],
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
(
|
|
136
|
+
cube.next_corner[edges_no[0]],
|
|
137
|
+
cube.next_corner[edges_no[2]],
|
|
138
|
+
) = (
|
|
139
|
+
cube.next_corner[edges_no[2]],
|
|
140
|
+
cube.next_corner[edges_no[0]],
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
(
|
|
144
|
+
cube.next_corner[edges_no[1]],
|
|
145
|
+
cube.next_corner[edges_no[3]],
|
|
146
|
+
) = (
|
|
147
|
+
cube.next_corner[edges_no[3]],
|
|
148
|
+
cube.next_corner[edges_no[1]],
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
def _move_home(cube: Cube, side: Side, m: Multiplicity) -> None:
|
|
152
|
+
'''Restore home pointer after a face move.'''
|
|
153
|
+
key: tuple[Side, Multiplicity] = (side, m)
|
|
154
|
+
if key in restore_home:
|
|
155
|
+
cube.home = nav_cc(restore_home[key], cube, cube.home)
|
|
156
|
+
|
|
157
|
+
def move(m: Move, cube: Cube) -> None:
|
|
158
|
+
'''Rotate a face with given multiplicity.'''
|
|
159
|
+
corner: CornerSticker = corner_on_side(cube, m.face)
|
|
160
|
+
_move_corner(cube, corner, m.mult)
|
|
161
|
+
_move_home(cube, m.face, m.mult)
|
|
162
|
+
|
|
163
|
+
def moved(m: Move, cube: Cube) -> Cube:
|
|
164
|
+
'''Return a new cube after rotating a face.'''
|
|
165
|
+
new: Cube = shallow_copy(cube)
|
|
166
|
+
move(m, new)
|
|
167
|
+
return new
|