cube-model 1.0.3__py3-none-any.whl
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/__init__.py +86 -0
- cube_model/action.py +189 -0
- cube_model/model.py +99 -0
- cube_model/move.py +167 -0
- cube_model/navigation.py +155 -0
- cube_model/new_cube.py +299 -0
- cube_model/py.typed +0 -0
- cube_model/reachable.py +194 -0
- cube_model/rotate.py +104 -0
- cube_model/utils.py +58 -0
- cube_model-1.0.3.dist-info/METADATA +45 -0
- cube_model-1.0.3.dist-info/RECORD +13 -0
- cube_model-1.0.3.dist-info/WHEEL +4 -0
cube_model/__init__.py
ADDED
|
@@ -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
|
+
]
|
cube_model/action.py
ADDED
|
@@ -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
|
cube_model/model.py
ADDED
|
@@ -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
|
+
)
|
cube_model/move.py
ADDED
|
@@ -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
|
cube_model/navigation.py
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
'''Navigation algebra over cube stickers.'''
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Iterable
|
|
6
|
+
from enum import Enum, auto
|
|
7
|
+
from typing import cast
|
|
8
|
+
|
|
9
|
+
from .model import Color, CornerSticker, Cube, EdgeSticker, Side, Sticker
|
|
10
|
+
from .new_cube import solved
|
|
11
|
+
|
|
12
|
+
class Nav(Enum):
|
|
13
|
+
'''Navigation steps: next or other.'''
|
|
14
|
+
NEXT = auto()
|
|
15
|
+
OTHER = auto()
|
|
16
|
+
|
|
17
|
+
# A sequence of Nav steps, accepted as an argument to nav().
|
|
18
|
+
type NavPath = Iterable[Nav]
|
|
19
|
+
|
|
20
|
+
def nav(nav_path: NavPath | str, cube: Cube, sticker: Sticker) -> Sticker:
|
|
21
|
+
'''Apply a sequence of navigation steps.'''
|
|
22
|
+
steps: NavPath
|
|
23
|
+
if isinstance(nav_path, str):
|
|
24
|
+
steps = parse_navs(nav_path)
|
|
25
|
+
else:
|
|
26
|
+
steps = nav_path
|
|
27
|
+
|
|
28
|
+
current: Sticker = sticker
|
|
29
|
+
is_edge: bool = isinstance(current, EdgeSticker)
|
|
30
|
+
step: Nav
|
|
31
|
+
for step in steps:
|
|
32
|
+
match step:
|
|
33
|
+
case Nav.NEXT:
|
|
34
|
+
if is_edge:
|
|
35
|
+
current = cube.next_corner[cast(EdgeSticker, current)]
|
|
36
|
+
else:
|
|
37
|
+
current = cube.next_edge[cast(CornerSticker, current)]
|
|
38
|
+
is_edge = not is_edge
|
|
39
|
+
case Nav.OTHER:
|
|
40
|
+
current = current.other
|
|
41
|
+
return current
|
|
42
|
+
|
|
43
|
+
def nav_cc(
|
|
44
|
+
nav_path: NavPath | str, cube: Cube, sticker: CornerSticker
|
|
45
|
+
) -> CornerSticker:
|
|
46
|
+
'''Navigate corner to corner under even NEXT parity.'''
|
|
47
|
+
return cast(CornerSticker, nav(nav_path, cube, sticker))
|
|
48
|
+
|
|
49
|
+
def parse_navs(text: str) -> list[Nav]:
|
|
50
|
+
'''Parse a string of N and O into navigation steps.'''
|
|
51
|
+
result: list[Nav] = []
|
|
52
|
+
ch: str
|
|
53
|
+
for ch in text:
|
|
54
|
+
match ch:
|
|
55
|
+
case 'N':
|
|
56
|
+
result.append(Nav.NEXT)
|
|
57
|
+
case 'O':
|
|
58
|
+
result.append(Nav.OTHER)
|
|
59
|
+
case _:
|
|
60
|
+
raise ValueError(f'invalid nav character: {ch!r}')
|
|
61
|
+
return result
|
|
62
|
+
|
|
63
|
+
HOME_TO_SIDE: dict[Side, list[Nav]] = {
|
|
64
|
+
Side.FRONT: parse_navs(''),
|
|
65
|
+
Side.TOP: parse_navs('OO'),
|
|
66
|
+
Side.RIGHT: parse_navs('NNOO'),
|
|
67
|
+
Side.LEFT: parse_navs('O'),
|
|
68
|
+
Side.BOTTOM: parse_navs('ONNOO'),
|
|
69
|
+
Side.BACK: parse_navs('OONNOO'),
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
def corner_on_side(cube: Cube, side: Side) -> CornerSticker:
|
|
73
|
+
'''Return a representative corner sticker for a side.'''
|
|
74
|
+
return nav_cc(HOME_TO_SIDE[side], cube, cube.home)
|
|
75
|
+
|
|
76
|
+
def side_corners(
|
|
77
|
+
cube: Cube, start: CornerSticker
|
|
78
|
+
) -> list[CornerSticker]:
|
|
79
|
+
'''Return the four corners on the same side.'''
|
|
80
|
+
result: list[CornerSticker] = [start]
|
|
81
|
+
current: CornerSticker = start
|
|
82
|
+
for _ in range(3):
|
|
83
|
+
edge: EdgeSticker = cube.next_edge[current]
|
|
84
|
+
current = cube.next_corner[edge]
|
|
85
|
+
result.append(current)
|
|
86
|
+
return result
|
|
87
|
+
|
|
88
|
+
def sticker_side(cube: Cube, sticker: Sticker) -> Side:
|
|
89
|
+
'''Return the side containing the given sticker.'''
|
|
90
|
+
corner: CornerSticker
|
|
91
|
+
if isinstance(sticker, EdgeSticker):
|
|
92
|
+
corner = cube.next_corner[sticker]
|
|
93
|
+
else:
|
|
94
|
+
corner = cast(CornerSticker, sticker)
|
|
95
|
+
corners: list[CornerSticker] = side_corners(cube, corner)
|
|
96
|
+
side: Side
|
|
97
|
+
path: list[Nav]
|
|
98
|
+
for side, path in HOME_TO_SIDE.items():
|
|
99
|
+
home_corner: CornerSticker = nav_cc(path, cube, cube.home)
|
|
100
|
+
if home_corner in corners:
|
|
101
|
+
return side
|
|
102
|
+
raise ValueError('sticker not found on any side')
|
|
103
|
+
|
|
104
|
+
# --- precompute orientation tables ---
|
|
105
|
+
|
|
106
|
+
_temp: Cube = solved()
|
|
107
|
+
|
|
108
|
+
side_colors: dict[tuple[Color, Color, Side], Color] = {}
|
|
109
|
+
|
|
110
|
+
corner: CornerSticker
|
|
111
|
+
for corner in _temp.next_edge.keys():
|
|
112
|
+
s: CornerSticker = corner
|
|
113
|
+
for _ in range(3):
|
|
114
|
+
side: Side
|
|
115
|
+
path: list[Nav]
|
|
116
|
+
for side, path in HOME_TO_SIDE.items():
|
|
117
|
+
target: CornerSticker = nav_cc(path, _temp, s)
|
|
118
|
+
side_colors[(s.color, s.other.other.color, side)] = target.color
|
|
119
|
+
s = s.other
|
|
120
|
+
|
|
121
|
+
color_sides: dict[tuple[Color, Color, Color], Side] = {
|
|
122
|
+
(a, b, c): side
|
|
123
|
+
for (a, b, side), c in side_colors.items()
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
del _temp
|
|
127
|
+
|
|
128
|
+
def side_color(cube: Cube, side: Side) -> Color:
|
|
129
|
+
'''Return the color of a side.'''
|
|
130
|
+
return side_colors[(cube.front_color, cube.top_color, side)]
|
|
131
|
+
|
|
132
|
+
def color_side(cube: Cube, color: Color) -> Side:
|
|
133
|
+
'''Return the side corresponding to a color.'''
|
|
134
|
+
return color_sides[(cube.front_color, cube.top_color, color)]
|
|
135
|
+
|
|
136
|
+
def all_colors(cube: Cube) -> dict[Side, list[Color]]:
|
|
137
|
+
'''Return the eight outer sticker colors on each side in
|
|
138
|
+
clockwise order, beginning with corner that bounds on the
|
|
139
|
+
following other two sides:
|
|
140
|
+
front: left, top
|
|
141
|
+
top: front, left
|
|
142
|
+
right: front, top
|
|
143
|
+
left: top, front
|
|
144
|
+
bottom; left, front
|
|
145
|
+
back: top, left'''
|
|
146
|
+
result: dict[Side, list[Color]] = {}
|
|
147
|
+
side: Side
|
|
148
|
+
for side in Side:
|
|
149
|
+
st: Sticker = corner_on_side(cube, side)
|
|
150
|
+
colors: list[Color] = []
|
|
151
|
+
for _ in range(8):
|
|
152
|
+
colors.append(st.color)
|
|
153
|
+
st = nav([Nav.NEXT], cube, st)
|
|
154
|
+
result[side] = colors
|
|
155
|
+
return result
|