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 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
@@ -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