squanlib 1.0.0 → 1.1.0
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.
- package/README.md +50 -1
- package/dist/squanlib.umd.js +1805 -0
- package/dist/squanlib.umd.min.js +2 -0
- package/package.json +19 -5
- package/squanlib.js +837 -91
|
@@ -0,0 +1,1805 @@
|
|
|
1
|
+
/*! squanlib | GPL-3.0-or-later | https://github.com/Mattttttttttttttttttttttttttttttt/squan-lib */
|
|
2
|
+
(function (global, factory) {
|
|
3
|
+
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
|
|
4
|
+
typeof define === 'function' && define.amd ? define(factory) :
|
|
5
|
+
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.squanlib = factory());
|
|
6
|
+
})(this, (function () { 'use strict';
|
|
7
|
+
|
|
8
|
+
// =============================================================================
|
|
9
|
+
// Unified Squan toolkit
|
|
10
|
+
// =============================================================================
|
|
11
|
+
|
|
12
|
+
class SquanLib {
|
|
13
|
+
|
|
14
|
+
// =========================================================================
|
|
15
|
+
// SECTION 1: DATA TABLES
|
|
16
|
+
// =========================================================================
|
|
17
|
+
|
|
18
|
+
// -------------------------------------------------------------------------
|
|
19
|
+
// karnToWCA
|
|
20
|
+
// Keys are padded with spaces on both sides so a simple global replace on a
|
|
21
|
+
// space-delimited string cannot match partial tokens.
|
|
22
|
+
// -------------------------------------------------------------------------
|
|
23
|
+
static karnToWCA = {
|
|
24
|
+
"U4": "U U' U U'", "U4'": "U' U U' U",
|
|
25
|
+
"D4": "D D' D D'", "D4'": "D' D D' D",
|
|
26
|
+
"u4": "u u' u u'", "u4'": "u' u u' u",
|
|
27
|
+
"d4": "d d' d d'", "d4'": "d' d d' d",
|
|
28
|
+
|
|
29
|
+
"U3": "U U' U", "U3'": "U' U U'",
|
|
30
|
+
"D3": "D D' D", "D3'": "D' D D'",
|
|
31
|
+
"u3": "u u' u", "u3'": "u' u u'",
|
|
32
|
+
"d3": "d d' d", "d3'": "d' d d'",
|
|
33
|
+
"F3": "F F' F", "F3'": "F' F F'",
|
|
34
|
+
"f3": "f f' f", "f3'": "f' f f'",
|
|
35
|
+
|
|
36
|
+
"W": "U U'", "W'": "U' U",
|
|
37
|
+
"B": "D D'", "B'": "D' D",
|
|
38
|
+
"w": "u u'", "w'": "u' u",
|
|
39
|
+
"b": "d d'", "b'": "d' d",
|
|
40
|
+
"F2": "F F'", "F2'": "F' F",
|
|
41
|
+
"f2": "f f'", "f2'": "f' f",
|
|
42
|
+
"UU": "U U", "UU'": "U' U'",
|
|
43
|
+
"DD": "D D", "DD'": "D' D'",
|
|
44
|
+
"T2": "T T'", "T2'": "T' T",
|
|
45
|
+
"t2": "t t'", "t2'": "t' t",
|
|
46
|
+
"E2": "E E'", "E2'": "E' E",
|
|
47
|
+
"ɇ": "U D", "ɇ'": "U' D'",
|
|
48
|
+
"Ɇ": "U D'", "Ɇ'": "U' D",
|
|
49
|
+
|
|
50
|
+
"U2": "6,0", "U2'": "6,0",
|
|
51
|
+
"D2": "0,6",
|
|
52
|
+
"U2D": "6,3", "U2D'": "6,-3",
|
|
53
|
+
"U2'D": "6,3", "U2'D'": "6,-3",
|
|
54
|
+
"U2D2": "6,6",
|
|
55
|
+
"UD2": "3,6", "U'D2": "-3,6",
|
|
56
|
+
|
|
57
|
+
"U": "3,0", "U'": "-3,0",
|
|
58
|
+
"D": "0,3", "D'": "0,-3",
|
|
59
|
+
"E": "3,-3", "E'": "-3,3",
|
|
60
|
+
"e": "3,3", "e'": "-3,-3",
|
|
61
|
+
"u": "2,-1", "u'": "-2,1",
|
|
62
|
+
"d": "-1,2", "d'": "1,-2",
|
|
63
|
+
"F": "4,1", "F'": "-4,-1",
|
|
64
|
+
"f": "1,4", "f'": "-1,-4",
|
|
65
|
+
"T": "2,-4", "T'": "-2,4",
|
|
66
|
+
"t": "4,-2", "t'": "-4,2",
|
|
67
|
+
"m": "2,2", "m'": "-2,-2",
|
|
68
|
+
"M": "1,1", "M'": "-1,-1",
|
|
69
|
+
"u2": "5,-1", "u2'": "-5,1",
|
|
70
|
+
"d2": "-1,5", "d2'": "1,-5",
|
|
71
|
+
"K": "5,2", "K'": "-5,-2",
|
|
72
|
+
"k": "2,5", "k'": "-2,-5",
|
|
73
|
+
"A": "1,0", "A'": "-1,0",
|
|
74
|
+
"G": "5,-4", "G'": "-5,4",
|
|
75
|
+
"g": "4,-5", "g'": "-4,5",
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
// -------------------------------------------------------------------------
|
|
79
|
+
// shorthandToKarn
|
|
80
|
+
// "move10" means top misalign, "move1-1" means double misalign, etc.
|
|
81
|
+
// Some shorthands are alignment-independent (bjj, fjj, nn, …).
|
|
82
|
+
// -------------------------------------------------------------------------
|
|
83
|
+
static shorthandToKarn = {
|
|
84
|
+
// ── alignment-independent ─────────────────────────────────────────────
|
|
85
|
+
"bjj": "U' e D'", "fjj": "U e' D",
|
|
86
|
+
"e2bjj": "U' e' U'", "e2fjj": "U e U",
|
|
87
|
+
"nn": "E E'",
|
|
88
|
+
"jn": "D4'", "nj": "U4",
|
|
89
|
+
"jj": "U e' D", "bjj+e2": "U' e' U'",
|
|
90
|
+
"-nn": "E' E",
|
|
91
|
+
"-jn": "D4", "-nj": "D4'",
|
|
92
|
+
// ── alignment-dependent ───────────────────────────────────────────────
|
|
93
|
+
"bpj10": "d m' U", "bpj0-1": "u' m D'",
|
|
94
|
+
"fpj10": "u m' D", "fpj0-1": "d' m U'",
|
|
95
|
+
"aa10": "u m' u T'", "aa0-1": "U m' U t'",
|
|
96
|
+
"fadj10": "D M' d'", "dadj10": "D M' d'",
|
|
97
|
+
"fadj0-1": "U' M u", "u'adj0-1": "U' M u",
|
|
98
|
+
"badj10": "U M' u'", "uadj10": "U M' u'",
|
|
99
|
+
"badj0-1": "D' M d", "d'adj0-1": "D' M d",
|
|
100
|
+
"bb10": "T u' e U'", "bb0-1": "t d e' D",
|
|
101
|
+
"fdd10": "D e' d t", "fdd0-1": "U' e u' T",
|
|
102
|
+
"bdd10": "U e' u T'", "bdd0-1": "D' e d' t'",
|
|
103
|
+
"ff10": "d m' d M E", "ff0-1": "u' m U' M T",
|
|
104
|
+
"fv10": "d4", "fv0-1": "d4'",
|
|
105
|
+
"vf10": "u4", "vf0-1": "u4'",
|
|
106
|
+
"y2fv10": "u d' u -5,4",
|
|
107
|
+
"jf10": "w D' u T'", "jf0-1": "w' D u' T",
|
|
108
|
+
"fj10": "b U' d t", "fj0-1": "b' U d' t'",
|
|
109
|
+
"jr00": "e' w e", "jr10": "e' b e",
|
|
110
|
+
"jr0-1": "e' w' e", "jr1-1": "e' b' e",
|
|
111
|
+
"rj00": "e b' e'", "rj10": "e w e'",
|
|
112
|
+
"rj0-1": "e b' e'", "rj1-1": "e w e'",
|
|
113
|
+
"jv10": "b D d d2'", "jv0-1": "b' D' d' d2",
|
|
114
|
+
"vj10": "w U u u2'", "vj0-1": "w' U' u' u2",
|
|
115
|
+
"kk10": "u m' U E'", "kk0-1": "U m' u E'",
|
|
116
|
+
"opp10": "u2 u2'", "opp0-1": "u2' u2",
|
|
117
|
+
"pn10": "T T'", "pn0-1": "t t'",
|
|
118
|
+
"px10": "f' d3' f'", "px0-1": "f d3 f",
|
|
119
|
+
"xp10": "F' u3' F'", "xp0-1": "F u3 F",
|
|
120
|
+
"tt10": "d m' F' u2'",
|
|
121
|
+
"fss10": "u M D' E'", "fss0-1": "D' M u E'",
|
|
122
|
+
"bss10": "D M' u' E", "bss0-1": "U' M d E",
|
|
123
|
+
"vv10": "u M u m' E'",
|
|
124
|
+
"zz10": "u M t' M D'", "zz0-1": "D' M t' M u",
|
|
125
|
+
// random things
|
|
126
|
+
"30adj10": "U M' u'", "-30adj0-1": "U' M u",
|
|
127
|
+
"03adj10": "D M' d'",
|
|
128
|
+
"obopp00": "1,0/M' F M' F M'/0,1",
|
|
129
|
+
"oaopp1-1": "0,1/M' u' M' u' M'/0,1",
|
|
130
|
+
"but00": "", "also00": "", "done!00": "0,0",
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
static alignmentIndependent = new Set([
|
|
134
|
+
'bjj', 'fjj', 'nn', 'jn', 'nj', 'e2bjj', 'e2fjj',
|
|
135
|
+
'jj', 'bjj+e2', '-nn', '-jn', '-nj',
|
|
136
|
+
]);
|
|
137
|
+
|
|
138
|
+
// -------------------------------------------------------------------------
|
|
139
|
+
// wcaToBaseKarn: maps single WCA move → base karn.
|
|
140
|
+
// -------------------------------------------------------------------------
|
|
141
|
+
static wcaToBaseKarn = {
|
|
142
|
+
// ── compound numeric → single karn ────────────────────────────────────
|
|
143
|
+
"6,0": "U2",
|
|
144
|
+
"6,3": "U2D", "6,-3": "U2D'", "6,6": "U2D2",
|
|
145
|
+
"0,6": "D2",
|
|
146
|
+
"3,6": "UD2", "-3,6": "U'D2",
|
|
147
|
+
// ── single numeric → single karn ──────────────────────────────────────
|
|
148
|
+
"3,0": "U", "-3,0": "U'",
|
|
149
|
+
"0,3": "D", "0,-3": "D'",
|
|
150
|
+
"3,-3": "E", "-3,3": "E'",
|
|
151
|
+
"3,3": "e", "-3,-3": "e'",
|
|
152
|
+
"2,-1": "u", "-2,1": "u'",
|
|
153
|
+
"-1,2": "d", "1,-2": "d'",
|
|
154
|
+
"4,1": "F", "-4,-1": "F'",
|
|
155
|
+
"1,4": "f", "-1,-4": "f'",
|
|
156
|
+
"2,-4": "T", "-2,4": "T'",
|
|
157
|
+
"4,-2": "t", "-4,2": "t'",
|
|
158
|
+
"2,2": "m", "-2,-2": "m'",
|
|
159
|
+
"1,1": "M", "-1,-1": "M'",
|
|
160
|
+
"5,-1": "u2", "-5,1": "u2'",
|
|
161
|
+
"-1,5": "d2", "1,-5": "d2'",
|
|
162
|
+
"5,2": "K", "-5,-2": "K'",
|
|
163
|
+
"2,5": "k", "-2,-5": "k'",
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
// -------------------------------------------------------------------------
|
|
167
|
+
// baseKarnToHighKarn: longest first, base karn → high karn
|
|
168
|
+
// -------------------------------------------------------------------------
|
|
169
|
+
static baseKarnToHighKarn = {
|
|
170
|
+
"U U' U U'": "U4", "U' U U' U": "U4'",
|
|
171
|
+
"D D' D D'": "D4", "D' D D' D": "D4'",
|
|
172
|
+
"u u' u u'": "u4", "u' u u' u": "u4'",
|
|
173
|
+
"d d' d d'": "d4", "d' d d' d": "d4'",
|
|
174
|
+
|
|
175
|
+
"U U' U": "U3", "U' U U'": "U3'",
|
|
176
|
+
"D D' D": "D3", "D' D D'": "D3'",
|
|
177
|
+
"u u' u": "u3", "u' u u'": "u3'",
|
|
178
|
+
"d d' d": "d3", "d' d d'": "d3'",
|
|
179
|
+
"F F' F": "F3", "F' F F'": "F3'",
|
|
180
|
+
"f f' f": "f3", "f' f f'": "f3'",
|
|
181
|
+
|
|
182
|
+
"U U'": "W", "U' U": "W'",
|
|
183
|
+
"D D'": "B", "D' D": "B'",
|
|
184
|
+
"u u'": "w", "u' u": "w'",
|
|
185
|
+
"d d'": "b", "d' d": "b'",
|
|
186
|
+
"F F'": "F2", "F' F": "F2'",
|
|
187
|
+
"f f'": "f2", "f' f": "f2'",
|
|
188
|
+
"U U": "UU", "U' U'": "UU'",
|
|
189
|
+
"D D": "DD", "D' D'": "DD'",
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* A_MOVES: legal moves available for top misalign
|
|
194
|
+
* a_MOVES: legal moves available for bottom misalign
|
|
195
|
+
*/
|
|
196
|
+
static A_MOVES = [
|
|
197
|
+
[3, 0], [-3, 0], [0, 3], [0, -3], [3, 3],
|
|
198
|
+
[2, -1], [-1, 2], [-4, -1], [-1, -4], [2, -4], [2, 2], [-1, -1], [5, -1],
|
|
199
|
+
];
|
|
200
|
+
static a_MOVES = [
|
|
201
|
+
[3, 0], [-3, 0], [0, 3], [0, -3], [3, 3],
|
|
202
|
+
[-2, 1], [1, -2], [4, 1], [1, 4], [-2, 4], [-2, -2], [1, 1], [-5, 1],
|
|
203
|
+
];
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* OPTIM: table for optimizable moves
|
|
207
|
+
*/
|
|
208
|
+
static OPTIM = {
|
|
209
|
+
// special case
|
|
210
|
+
"/0,0/": "",
|
|
211
|
+
"/3,3/3,3/": "-3,-3/-3,-3",
|
|
212
|
+
"/-3,-3/-3,-3/": "3,3/3,3",
|
|
213
|
+
"/2,2/-2,-2/": "2,2/-2,-2",
|
|
214
|
+
"/-2,-2/2,2/": "-2,-2/2,2",
|
|
215
|
+
"/1,1/-1,-1/": "1,1/-1,-1",
|
|
216
|
+
"/-1,-1/1,1/": "-1,-1/1,1",
|
|
217
|
+
"/2,-4/-2,4/2,-4/": "2,-4/-2,4/2,-4",
|
|
218
|
+
"/-2,4/2,-4/-2,4/": "-2,4/2,-4/-2,4",
|
|
219
|
+
"/5,-1/-5,1/5,-1/": "5,-1/-5,1/5,-1",
|
|
220
|
+
"/-5,1/5,-1/-5,1/": "-5,1/5,-1/-5,1",
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* CLOSEST_MAP: maps a -5~6 turn to the its closest 3n move
|
|
225
|
+
*/
|
|
226
|
+
static CLOSEST_MAP = new Map([
|
|
227
|
+
[-5, -6], [-4, -3], [-3, -3], [-2, -3], [-1, 0], [0, 0],
|
|
228
|
+
[1, 0], [2, 3], [3, 3], [4, 3], [5, 6], [6, 6],
|
|
229
|
+
]);
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* MOVE_VALUES: lookup table for the ergonomic rating of each individual move.
|
|
233
|
+
* A = 10, a = 0-1
|
|
234
|
+
* / = upslice, \ = downslice
|
|
235
|
+
*/
|
|
236
|
+
static MOVE_VALUES = new Map([
|
|
237
|
+
// aligned top, upslice
|
|
238
|
+
['A/0,3', 16], ['A/0,6', 1], ['A/0,-3', 18],
|
|
239
|
+
['A/3,0', 16], ['A/3,3', 12], ['A/3,6', 0], ['A/3,-3', 13],
|
|
240
|
+
['A/6,0', 12], ['A/6,3', 11], ['A/6,6', 2], ['A/6,-3', 12],
|
|
241
|
+
['A/-3,0', 9], ['A/-3,3', 13], ['A/-3,6', 4], ['A/-3,-3', 12],
|
|
242
|
+
// aligned top, downslice
|
|
243
|
+
['A\\0,3', 17], ['A\\0,6', 1], ['A\\0,-3', 8],
|
|
244
|
+
['A\\3,0', 6], ['A\\3,3', 14], ['A\\3,6', 1], ['A\\3,-3', 12],
|
|
245
|
+
['A\\6,0', 14], ['A\\6,3', 11], ['A\\6,6', 5], ['A\\6,-3', 8],
|
|
246
|
+
['A\\-3,0', 11], ['A\\-3,3', 14], ['A\\-3,6', 6], ['A\\-3,-3', 9],
|
|
247
|
+
// unaligned top, upslice
|
|
248
|
+
['a/0,3', 5], ['a/0,6', 5], ['a/0,-3', 12],
|
|
249
|
+
['a/3,0', 17], ['a/3,3', 10], ['a/3,6', 5], ['a/3,-3', 7],
|
|
250
|
+
['a/6,0', 4], ['a/6,3', 2], ['a/6,6', 0], ['a/6,-3', 3],
|
|
251
|
+
['a/-3,0', 18], ['a/-3,3', 12], ['a/-3,6', 7], ['a/-3,-3', 11],
|
|
252
|
+
// unaligned top, downslice
|
|
253
|
+
['a\\0,3', 5], ['a\\0,6', 5], ['a\\0,-3', 5],
|
|
254
|
+
['a\\3,0', 16], ['a\\3,3', 11], ['a\\3,6', 4], ['a\\3,-3', 6],
|
|
255
|
+
['a\\6,0', 4], ['a\\6,3', 2], ['a\\6,6', 0], ['a\\6,-3', 1],
|
|
256
|
+
['a\\-3,0', 15], ['a\\-3,3', 10], ['a\\-3,6', 2], ['a\\-3,-3', 5],
|
|
257
|
+
// fractional (non-multiple-of-3) moves: alignment prefix omitted
|
|
258
|
+
['/1,-2', 4], ['\\1,-2', 17], ['/-1,2', 15], ['\\-1,2', 14],
|
|
259
|
+
['/1,-5', 3], ['\\1,-5', 1], ['/-1,5', 8], ['\\-1,5', 3],
|
|
260
|
+
['/1,4', 7], ['\\1,4', 14], ['/-1,-4', 12], ['\\-1,-4', 9],
|
|
261
|
+
['/1,1', 11], ['\\1,1', 20], ['/-1,-1', 20], ['\\-1,-1', 10],
|
|
262
|
+
['/2,-1', 20], ['\\2,-1', 12], ['/-2,1', 14], ['\\-2,1', 18],
|
|
263
|
+
['/2,2', 12], ['\\2,2', 13], ['/-2,-2', 14], ['\\-2,-2', 8],
|
|
264
|
+
['/2,5', 5], ['\\2,5', 3], ['/-2,-5', 4], ['\\-2,-5', 3],
|
|
265
|
+
['/2,-4', 14], ['\\2,-4', 6], ['/-2,4', 13], ['\\-2,4', 13],
|
|
266
|
+
['/4,4', 5], ['\\4,4', 12], ['/-4,-4', 12], ['\\-4,-4', 4],
|
|
267
|
+
['/4,1', 6], ['\\4,1', 13], ['/-4,-1', 16], ['\\-4,-1', 6],
|
|
268
|
+
['/4,-2', 12], ['\\4,-2', 9], ['/-4,2', 16], ['\\-4,2', 13],
|
|
269
|
+
['/4,-5', 2], ['\\4,-5', 5], ['/-4,5', 13], ['\\-4,5', 3],
|
|
270
|
+
['/5,5', 1], ['\\5,5', 4], ['/-5,-5', 2], ['\\-5,-5', 0],
|
|
271
|
+
['/5,2', 6], ['\\5,2', 10], ['/-5,-2', 12], ['\\-5,-2', 13],
|
|
272
|
+
['/5,-1', 11], ['\\5,-1', 7], ['/-5,1', 14], ['\\-5,1', 15],
|
|
273
|
+
['/5,-4', 2], ['\\5,-4', 2], ['/-5,4', 12], ['\\-5,4', 14],
|
|
274
|
+
]);
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* GOOD_FINISHES: moves that are acceptable as the last move
|
|
278
|
+
*/
|
|
279
|
+
static GOOD_FINISHES = new Set([
|
|
280
|
+
"11", "-1-1", "22", "-2-2", "2-1", "-21", "1-2", "-12",
|
|
281
|
+
"30", "-30", "03", "0-3", "33", "3-3", "-3-3", "-33",
|
|
282
|
+
"41", "-4-1", "14", "-1-4", "2-4", "-24", "4-2", "-42",
|
|
283
|
+
"5-1", "-51", "-45", "-54", "63",
|
|
284
|
+
]);
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* OBLToEnglish: maps CSP-style hex into OBL names
|
|
288
|
+
* (format is 24-character string, both corner first, STARTING FROM TOP RIGHT OF SLICE
|
|
289
|
+
* meaning: 0-1 is the solved position. **the order is like CSP tracing.**)
|
|
290
|
+
*/
|
|
291
|
+
static OBLToEnglish = {
|
|
292
|
+
'BBbBBbBBbBBb': 'solved',
|
|
293
|
+
'BBwWWwWWwWWw': '1c',
|
|
294
|
+
'BBwBBwWWwWWw': 'cadj',
|
|
295
|
+
'BBwWWwBBwWWw': 'copp',
|
|
296
|
+
'BBwBBwBBwWWw': '3c',
|
|
297
|
+
'BBwBBwBBwBBw': '4e',
|
|
298
|
+
'WWbWWbWWbWWw': '3e',
|
|
299
|
+
'WWbWWwWWbWWw': 'line',
|
|
300
|
+
'WWbWWbWWwWWw': 'L',
|
|
301
|
+
'WWbWWwWWwWWw': '1e',
|
|
302
|
+
'WWbBBwWWwWWw': 'left pair', 'BBbWWwWWwWWw': 'right pair',
|
|
303
|
+
'BBwWWwWWbWWw': 'left arrow', 'BBwWWbWWwWWw': 'right arrow',
|
|
304
|
+
'WWbBBbWWwWWw': 'gem',
|
|
305
|
+
'WWwWWbWWbBBw': 'left knight', 'BBbWWbWWwWWw': 'right knight',
|
|
306
|
+
'WWwWWbWWwBBb': 'left axe', 'BBwWWbWWwWWb': 'right axe',
|
|
307
|
+
'BBwWWbWWbWWw': 'squid',
|
|
308
|
+
'WWwWWbBBbWWb': 'left thumb', 'WWbBBbWWwWWb': 'right thumb',
|
|
309
|
+
'WWwBBbWWbWWb': 'left bunny', 'WWbWWbBBwWWb': 'right bunny',
|
|
310
|
+
'BBbBBwWWwWWw': 'shell',
|
|
311
|
+
'BBwWWwWWbBBw': 'left bird', 'BBwBBbWWwWWw': 'right bird',
|
|
312
|
+
'BBwWWbWWwBBw': 'hazard',
|
|
313
|
+
'BBbBBbWWwWWw': 'left kite', 'WWwWWbBBbBBw': 'right kite',
|
|
314
|
+
'BBwBBwWWbWWb': 'left cut', 'BBwBBbWWbWWw': 'right cut',
|
|
315
|
+
'BBbBBwWWbWWw': 'black T', 'WWwWWbBBwBBb': 'white T',
|
|
316
|
+
'WWbBBwWWbBBw': 'left N', 'WWwBBbWWwBBb': 'right N',
|
|
317
|
+
'WWbBBbWWwBBw': 'black tie', 'BBwWWwBBbWWb': 'white tie',
|
|
318
|
+
'BBbWWwBBwWWw': 'left yoshi', 'WWwBBwWWbBBw': 'right yoshi'
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
static OBLToState = Object.fromEntries(
|
|
322
|
+
Object.entries(this.OBLToEnglish).map(([key, value]) => [value, key])
|
|
323
|
+
);
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* NAMING: matt's OBL naming
|
|
327
|
+
*/
|
|
328
|
+
static NAMING = {
|
|
329
|
+
"solved": "O", "1c": "D", "cadj": "J", "copp": "V", "3c": "M", "4e": "Q",
|
|
330
|
+
"3e": "W", "line": "F", "L": "L", "1e": "E", "left pair": "Pw", "right pair": "Pc",
|
|
331
|
+
"left arrow": "Aw", "right arrow": "Ac", "gem": "G", "left knight": "Hw",
|
|
332
|
+
"right knight": "Hc", "left axe": "Xc", "right axe": "Xw", "squid": "S",
|
|
333
|
+
"left thumb": "THw", "right thumb": "THc", "left bunny": "Uc", "right bunny": "Uw",
|
|
334
|
+
"shell": "SH", "left bird": "Bc", "right bird": "Bw", "hazard": "Z",
|
|
335
|
+
"left kite": "Kc", "right kite": "Kw", "left cut": "Cw", "right cut": "Cc",
|
|
336
|
+
"black T": "Tu", "white T": "Td", "left N": "Nw", "right N": "Nc",
|
|
337
|
+
"black tie": "Iu", "white tie": "Id", "left yoshi": "Yc", "right yoshi": "Yw"
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* OBL_ANGLES: the starting angle, logged in PATTERNS
|
|
342
|
+
*/
|
|
343
|
+
static OBL_ANGLES = {
|
|
344
|
+
"solved": "-", "1c": "UR", "cadj": "R",
|
|
345
|
+
"copp": "/", "3c": "DR", "4e": "-",
|
|
346
|
+
"3e": "D", "line": "—", "L": "DR",
|
|
347
|
+
"1e": "R", "left pair": "DR", "right pair": "UR",
|
|
348
|
+
"left arrow": "UR", "right arrow": "UR",
|
|
349
|
+
"gem": "DR", "left knight": "L", "right knight": "R",
|
|
350
|
+
"left axe": "L", "right axe": "R", "squid": "UR",
|
|
351
|
+
"left thumb": "DL", "right thumb": "DR",
|
|
352
|
+
"left bunny": "DR", "right bunny": "DL",
|
|
353
|
+
"shell": "R", "left bird": "R", "right bird": "U",
|
|
354
|
+
"hazard": "U", "left kite": "R", "right kite": "L",
|
|
355
|
+
"left cut": "R", "right cut": "R",
|
|
356
|
+
"black T": "R", "white T": "R",
|
|
357
|
+
"left N": "\\", "right N": "\\",
|
|
358
|
+
"black tie": "DR", "white tie": "DR",
|
|
359
|
+
"left yoshi": "UL", "right yoshi": "UR"
|
|
360
|
+
};
|
|
361
|
+
|
|
362
|
+
static nextAngle = {
|
|
363
|
+
// Single character angles
|
|
364
|
+
"-": "-", "/": "\\", "\\": "/", "—": "|", "|": "—",
|
|
365
|
+
"U": "R", "R": "D", "D": "L", "L": "U",
|
|
366
|
+
// Double character angles
|
|
367
|
+
"UR": "DR", "DR": "DL", "DL": "UL", "UL": "UR"
|
|
368
|
+
};
|
|
369
|
+
|
|
370
|
+
static get HALF_L() { return 6; }
|
|
371
|
+
static get LAYERL() { return 12; }
|
|
372
|
+
static get THREE_FOUR_L() { return 18; }
|
|
373
|
+
static get CUBEL() { return 24; }
|
|
374
|
+
static get SOLVED() { return "bBBbBBbBBbBBwWWwWWwWWwWW"; }
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* POSSIBLE_OBL: every OBL case as [specifier, U, D].
|
|
378
|
+
*/
|
|
379
|
+
static POSSIBLE_OBL = [
|
|
380
|
+
['', 'solved', 'solved'],
|
|
381
|
+
['', '1c', '1c'], ['', 'cadj', 'cadj'], ['', 'cadj', 'copp'], ['', 'copp', 'copp'],
|
|
382
|
+
['', '3c', '3c'], ['', '4e', '4e'], ['', '3e', '3e'], ['', 'line', 'line'],
|
|
383
|
+
['', 'L', 'line'], ['', 'L', 'L'], ['', '1e', '1e'],
|
|
384
|
+
['good', 'pair', 'pair'], ['bad', 'pair', 'pair'],
|
|
385
|
+
['good', 'arrow', 'pair'], ['bad', 'arrow', 'pair'],
|
|
386
|
+
['good', 'arrow', 'arrow'], ['bad', 'arrow', 'arrow'],
|
|
387
|
+
['', 'gem', 'gem'], ['', 'gem', 'knight'], ['', 'gem', 'axe'], ['', 'gem', 'squid'],
|
|
388
|
+
['good', 'knight', 'knight'], ['bad', 'knight', 'knight'],
|
|
389
|
+
['good', 'knight', 'axe'], ['bad', 'knight', 'axe'],
|
|
390
|
+
['same', 'axe', 'axe'], ['diff', 'axe', 'axe'],
|
|
391
|
+
['', 'squid', 'knight'], ['', 'squid', 'axe'], ['', 'squid', 'squid'],
|
|
392
|
+
['good', 'thumb', 'thumb'], ['bad', 'thumb', 'thumb'],
|
|
393
|
+
['good', 'thumb', 'bunny'], ['bad', 'thumb', 'bunny'],
|
|
394
|
+
['good', 'bunny', 'bunny'], ['bad', 'bunny', 'bunny'],
|
|
395
|
+
['', 'shell', 'shell'], ['', 'shell', 'bird'], ['', 'shell', 'hazard'],
|
|
396
|
+
['', 'yoshi', 'shell'],
|
|
397
|
+
['good', 'bird', 'bird'], ['bad', 'bird', 'bird'],
|
|
398
|
+
['', 'bird', 'hazard'], ['', 'hazard', 'hazard'],
|
|
399
|
+
['good', 'yoshi', 'bird'], ['bad', 'yoshi', 'bird'],
|
|
400
|
+
['', 'yoshi', 'hazard'], ['same', 'yoshi', 'yoshi'], ['diff', 'yoshi', 'yoshi'],
|
|
401
|
+
['good', 'kite', 'kite'], ['bad', 'kite', 'kite'],
|
|
402
|
+
['good', 'kite', 'cut'], ['bad', 'kite', 'cut'],
|
|
403
|
+
['', 'kite', 'T'], ['good', 'kite', 'N'], ['bad', 'kite', 'N'], ['', 'kite', 'tie'],
|
|
404
|
+
['', 'cut', 'T'], ['good', 'cut', 'N'], ['bad', 'cut', 'N'], ['', 'cut', 'tie'],
|
|
405
|
+
['good', 'cut', 'cut'], ['bad', 'cut', 'cut'],
|
|
406
|
+
['good', 'T', 'T'], ['bad', 'T', 'T'], ['', 'T', 'N'],
|
|
407
|
+
['good', 'T', 'tie'], ['bad', 'T', 'tie'],
|
|
408
|
+
['good', 'N', 'N'], ['bad', 'N', 'N'], ['', 'tie', 'N'],
|
|
409
|
+
['good', 'tie', 'tie'], ['bad', 'tie', 'tie']
|
|
410
|
+
];
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* OBL_LEN: optimal slicecount of each OBL
|
|
414
|
+
*/
|
|
415
|
+
static OBL_LEN = {
|
|
416
|
+
"solved/solved": 0, "1c/1c": 5, "cadj/cadj": 4, "cadj/copp": 5, "copp/copp": 2,
|
|
417
|
+
"3c/3c": 5, "4e/4e": 4, "3e/3e": 5, "line/line": 2, "L/line": 5, "L/L": 4, "1e/1e": 5,
|
|
418
|
+
"good pair/pair": 2, "bad pair/pair": 4, "good arrow/pair": 3, "bad arrow/pair": 4,
|
|
419
|
+
"good arrow/arrow": 3, "bad arrow/arrow": 4, "gem/gem": 4, "gem/knight": 4,
|
|
420
|
+
"gem/axe": 3, "gem/squid": 4, "good knight/knight": 4, "bad knight/knight": 5,
|
|
421
|
+
"good knight/axe": 3, "bad knight/axe": 4, "same axe/axe": 5, "diff axe/axe": 5,
|
|
422
|
+
"squid/knight": 4, "squid/axe": 4, "squid/squid": 5, "good thumb/thumb": 2,
|
|
423
|
+
"bad thumb/thumb": 5, "good thumb/bunny": 4, "bad thumb/bunny": 4,
|
|
424
|
+
"good bunny/bunny": 3, "bad bunny/bunny": 5, "shell/shell": 4, "shell/bird": 4,
|
|
425
|
+
"shell/hazard": 4, "yoshi/shell": 3, "good bird/bird": 4, "bad bird/bird": 5,
|
|
426
|
+
"bird/hazard": 4, "hazard/hazard": 5, "good yoshi/bird": 3, "bad yoshi/bird": 4,
|
|
427
|
+
"yoshi/hazard": 4, "same yoshi/yoshi": 5, "diff yoshi/yoshi": 5,
|
|
428
|
+
"good kite/kite": 1, "bad kite/kite": 5, "good kite/cut": 3, "bad kite/cut": 6,
|
|
429
|
+
"kite/T": 4, "good kite/N": 3, "bad kite/N": 4, "kite/tie": 4, "cut/T": 4,
|
|
430
|
+
"good cut/N": 4, "bad cut/N": 5, "cut/tie": 4, "good cut/cut": 3, "bad cut/cut": 6,
|
|
431
|
+
"good T/T": 3, "bad T/T": 4, "T/N": 5, "good T/tie": 3, "bad T/tie": 4,
|
|
432
|
+
"good N/N": 2, "bad N/N": 4, "tie/N": 5, "good tie/tie": 3, "bad tie/tie": 4
|
|
433
|
+
};
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* OBL_TRANSLATION: map nonspecific OBL to specific OBLs without layer flips
|
|
437
|
+
*/
|
|
438
|
+
static OBL_TRANSLATION = {
|
|
439
|
+
'solved/solved': ['solved/solved'],
|
|
440
|
+
'1c/1c': ['1c/1c'],
|
|
441
|
+
'cadj/cadj': ['cadj/cadj'],
|
|
442
|
+
'cadj/copp': ['cadj/copp'],
|
|
443
|
+
'copp/copp': ['copp/copp'],
|
|
444
|
+
'3c/3c': ['3c/3c'],
|
|
445
|
+
'4e/4e': ['4e/4e'],
|
|
446
|
+
'3e/3e': ['3e/3e'],
|
|
447
|
+
'line/line': ['line/line'],
|
|
448
|
+
'L/line': ['L/line'],
|
|
449
|
+
'L/L': ['L/L'],
|
|
450
|
+
'1e/1e': ['1e/1e'],
|
|
451
|
+
'good pair/pair': ['left pair/left pair', 'right pair/right pair'],
|
|
452
|
+
'bad pair/pair': ['left pair/right pair'],
|
|
453
|
+
'good arrow/pair': ['left arrow/right pair', 'right arrow/left pair'],
|
|
454
|
+
'bad arrow/pair': ['left arrow/left pair', 'right arrow/right pair'],
|
|
455
|
+
'good arrow/arrow': ['left arrow/left arrow', 'right arrow/right arrow'],
|
|
456
|
+
'bad arrow/arrow': ['left arrow/right arrow'],
|
|
457
|
+
'gem/gem': ['gem/gem'],
|
|
458
|
+
'gem/knight': ['gem/left knight', 'gem/right knight'],
|
|
459
|
+
'gem/axe': ['gem/left axe', 'gem/right axe'],
|
|
460
|
+
'gem/squid': ['gem/squid'],
|
|
461
|
+
'good knight/knight': ['left knight/right knight'],
|
|
462
|
+
'bad knight/knight': ['left knight/left knight', 'right knight/right knight'],
|
|
463
|
+
'good knight/axe': ['left knight/left axe', 'right knight/right axe'],
|
|
464
|
+
'bad knight/axe': ['left knight/right axe', 'right knight/left axe'],
|
|
465
|
+
'same axe/axe': ['left axe/left axe', 'right axe/right axe'],
|
|
466
|
+
'diff axe/axe': ['left axe/right axe'],
|
|
467
|
+
'squid/knight': ['squid/left knight', 'squid/right knight'],
|
|
468
|
+
'squid/axe': ['squid/left axe', 'squid/right axe'],
|
|
469
|
+
'squid/squid': ['squid/squid'],
|
|
470
|
+
'good thumb/thumb': ['left thumb/left thumb', 'right thumb/right thumb'],
|
|
471
|
+
'bad thumb/thumb': ['left thumb/right thumb'],
|
|
472
|
+
'good thumb/bunny': ['left thumb/right bunny', 'right thumb/left bunny'],
|
|
473
|
+
'bad thumb/bunny': ['left thumb/left bunny', 'right thumb/right bunny'],
|
|
474
|
+
'good bunny/bunny': ['left bunny/left bunny', 'right bunny/right bunny'],
|
|
475
|
+
'bad bunny/bunny': ['left bunny/right bunny'],
|
|
476
|
+
'shell/shell': ['shell/shell'],
|
|
477
|
+
'shell/bird': ['shell/left bird', 'shell/right bird'],
|
|
478
|
+
'shell/hazard': ['shell/hazard'],
|
|
479
|
+
'yoshi/shell': ['left yoshi/shell', 'right yoshi/shell'],
|
|
480
|
+
'good bird/bird': ['left bird/right bird'],
|
|
481
|
+
'bad bird/bird': ['left bird/left bird', 'right bird/right bird'],
|
|
482
|
+
'bird/hazard': ['left bird/hazard', 'right bird/hazard'],
|
|
483
|
+
'hazard/hazard': ['hazard/hazard'],
|
|
484
|
+
'good yoshi/bird': ['left yoshi/left bird', 'right yoshi/right bird'],
|
|
485
|
+
'bad yoshi/bird': ['left yoshi/right bird', 'right yoshi/left bird'],
|
|
486
|
+
'yoshi/hazard': ['left yoshi/hazard', 'right yoshi/hazard'],
|
|
487
|
+
'same yoshi/yoshi': ['left yoshi/left yoshi', 'right yoshi/right yoshi'],
|
|
488
|
+
'diff yoshi/yoshi': ['left yoshi/right yoshi'],
|
|
489
|
+
'good kite/kite': ['left kite/left kite', 'right kite/right kite'],
|
|
490
|
+
'bad kite/kite': ['left kite/right kite'],
|
|
491
|
+
'good kite/cut': ['left kite/left cut', 'right kite/right cut'],
|
|
492
|
+
'bad kite/cut': ['left kite/right cut', 'right kite/left cut'],
|
|
493
|
+
'kite/T': ['left kite/black T', 'left kite/white T', 'right kite/black T', 'right kite/white T'],
|
|
494
|
+
'good kite/N': ['left kite/right N', 'right kite/left N'],
|
|
495
|
+
'bad kite/N': ['left kite/left N', 'right kite/right N'],
|
|
496
|
+
'kite/tie': ['left kite/black tie', 'left kite/white tie', 'right kite/black tie', 'right kite/white tie'],
|
|
497
|
+
'cut/T': ['left cut/black T', 'left cut/white T', 'right cut/black T', 'right cut/white T'],
|
|
498
|
+
'good cut/N': ['left cut/left N', 'right cut/right N'],
|
|
499
|
+
'bad cut/N': ['left cut/right N', 'right cut/left N'],
|
|
500
|
+
'cut/tie': ['left cut/black tie', 'left cut/white tie', 'right cut/black tie', 'right cut/white tie'],
|
|
501
|
+
'good cut/cut': ['left cut/left cut', 'right cut/right cut'],
|
|
502
|
+
'bad cut/cut': ['left cut/right cut'],
|
|
503
|
+
'good T/T': ['black T/black T', 'white T/white T'],
|
|
504
|
+
'bad T/T': ['black T/white T'],
|
|
505
|
+
'T/N': ['black T/left N', 'black T/right N', 'white T/left N', 'white T/right N'],
|
|
506
|
+
'good T/tie': ['black T/black tie', 'white T/white tie'],
|
|
507
|
+
'bad T/tie': ['black T/white tie', 'white T/black tie'],
|
|
508
|
+
'good N/N': ['left N/left N', 'right N/right N'],
|
|
509
|
+
'bad N/N': ['left N/right N'],
|
|
510
|
+
'tie/N': ['black tie/left N', 'black tie/right N', 'white tie/left N', 'white tie/right N'],
|
|
511
|
+
'good tie/tie': ['black tie/black tie', 'white tie/white tie'],
|
|
512
|
+
'bad tie/tie': ['black tie/white tie']
|
|
513
|
+
};
|
|
514
|
+
|
|
515
|
+
/**
|
|
516
|
+
* CORNERS: possible OBLP corner memo
|
|
517
|
+
*/
|
|
518
|
+
static CORNERS = [[''], ['1', '3', '5', '7'], ['13', '15', '17', '35', '37', '57'], ['135', '137', '157', '357'], ['1357']];
|
|
519
|
+
|
|
520
|
+
/**
|
|
521
|
+
* EDGES: possible OBLP edge memo
|
|
522
|
+
*/
|
|
523
|
+
static EDGES = [[''], ['2', '4', '6', '8'], ['24', '26', '28', '46', '48', '68'], ['246', '248', '268', '468'], ['2468']];
|
|
524
|
+
|
|
525
|
+
/**
|
|
526
|
+
* TOTAL_CORNERS: flat list of CORNERS
|
|
527
|
+
*/
|
|
528
|
+
static TOTAL_CORNERS = ['', '1', '3', '5', '7', '13', '15', '17', '35', '37', '57', '135', '137', '157', '357', '1357'];
|
|
529
|
+
|
|
530
|
+
/**
|
|
531
|
+
* TOTAL_EDGES: flat list of EDGES
|
|
532
|
+
*/
|
|
533
|
+
static TOTAL_EDGES = ['', '2', '4', '6', '8', '24', '26', '28', '46', '48', '68', '246', '248', '268', '468', '2468'];
|
|
534
|
+
|
|
535
|
+
/**
|
|
536
|
+
* @param {object} [tempReplacements]: initial manual unkarnifications.
|
|
537
|
+
*/
|
|
538
|
+
constructor(tempReplacements = { "meow :3": "meow :3" }) {
|
|
539
|
+
// place to put manual unkarnifications
|
|
540
|
+
this.tempReplacements = { ...tempReplacements };
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
/**
|
|
544
|
+
* setTempReplacements: replace the entire tempReplacements map.
|
|
545
|
+
*
|
|
546
|
+
* @param {Object<string,string>} replacements the new key→value pairs
|
|
547
|
+
* @returns {this}
|
|
548
|
+
*/
|
|
549
|
+
setTempReplacements(replacements) {
|
|
550
|
+
this.tempReplacements = { ...replacements };
|
|
551
|
+
return this;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
/**
|
|
555
|
+
* addTempReplacements: merge key→value pairs into tempReplacements.
|
|
556
|
+
*
|
|
557
|
+
* @param {Object<string,string>} replacements: pairs to add (overwrites collisions)
|
|
558
|
+
* @returns {this}
|
|
559
|
+
*/
|
|
560
|
+
addTempReplacements(replacements) {
|
|
561
|
+
Object.assign(this.tempReplacements, replacements);
|
|
562
|
+
return this;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
|
|
566
|
+
// =========================================================================
|
|
567
|
+
// SECTION 2: CORE UTILITIES
|
|
568
|
+
// =========================================================================
|
|
569
|
+
|
|
570
|
+
/**
|
|
571
|
+
* dictReplace: repeatedly applies every key→value substitution in `dict`
|
|
572
|
+
* to `str` until the string stabilizes.
|
|
573
|
+
*
|
|
574
|
+
* @param {string} str the string to be replaced
|
|
575
|
+
* @param {object} dict the dictionary
|
|
576
|
+
* @returns {string} the fully replaced string
|
|
577
|
+
*/
|
|
578
|
+
dictReplace(str, dict) {
|
|
579
|
+
const pattern = new RegExp(
|
|
580
|
+
Object.keys(dict).map(k => k.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|'),
|
|
581
|
+
'g'
|
|
582
|
+
);
|
|
583
|
+
let prev;
|
|
584
|
+
do { prev = str; str = str.replace(pattern, m => dict[m]); } while (str !== prev);
|
|
585
|
+
return str;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
/**
|
|
589
|
+
* addCommas: e.g. "2-1" → "2,-1"
|
|
590
|
+
*
|
|
591
|
+
* length 1 → "N,0"
|
|
592
|
+
* length 2 → starts with '-'? "-N,0" : "A,B"
|
|
593
|
+
* length 3 → starts with '-'? "-A,B" : "A,BC" (where BC is the second part)
|
|
594
|
+
* length 4 → "AB,CD"
|
|
595
|
+
* anything else that is not all-digits/minus → pass through unchanged
|
|
596
|
+
*
|
|
597
|
+
* @param {string} alg the scramble, any separator (no additional spaces). can have commas already.
|
|
598
|
+
* @returns {string} the scramble, with commas added
|
|
599
|
+
*/
|
|
600
|
+
addCommas(alg) {
|
|
601
|
+
return alg.split(/[/\\| ]/).map(move => {
|
|
602
|
+
if (!move || isNaN(Number(move.replaceAll('-', ''))) || move.includes(","))
|
|
603
|
+
return move;
|
|
604
|
+
switch (move.length) {
|
|
605
|
+
case 1: return move + ',0';
|
|
606
|
+
case 2: return move.charAt(0) === '-' ? move + ',0'
|
|
607
|
+
: move[0] + ',' + move[1];
|
|
608
|
+
case 3: return move.charAt(0) === '-' ? move.slice(0, 2) + ',' + move[2]
|
|
609
|
+
: move[0] + ',' + move.slice(1);
|
|
610
|
+
case 4: return move.slice(0, 2) + ',' + move.slice(2);
|
|
611
|
+
default: throw new Error(`"${move}" is not a valid karn numeric move`);
|
|
612
|
+
}
|
|
613
|
+
}).join(' ');
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
/**
|
|
617
|
+
* isKarn: returns true if the string uses any letters
|
|
618
|
+
*
|
|
619
|
+
* @param {string} str the alg
|
|
620
|
+
* @returns {boolean} whether the alg contains letters
|
|
621
|
+
*/
|
|
622
|
+
isKarn(str) {
|
|
623
|
+
return /[a-zA-Z]/.test(str);
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
/**
|
|
627
|
+
* getAlignmentMove: turns topA and bottomA into a starting move
|
|
628
|
+
*
|
|
629
|
+
* @param {boolean} topA top misalign?
|
|
630
|
+
* @param {boolean} bottomA bottom misalign?
|
|
631
|
+
* @returns {string} e.g. "10", "1-1"
|
|
632
|
+
*/
|
|
633
|
+
getAlignmentMove(topA, bottomA) {
|
|
634
|
+
return (topA ? '1' : '0') + (bottomA ? '-1' : '0');
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
/**
|
|
638
|
+
* getAlignment: turns a starting/ending move into topA and bottomA
|
|
639
|
+
*
|
|
640
|
+
* @param {string} m the move
|
|
641
|
+
* @returns {topA: top misalign?, bottomA: bottom misalign?}}
|
|
642
|
+
*/
|
|
643
|
+
getAlignment(m) {
|
|
644
|
+
if (!m) return { topA: false, bottomA: false } // this is just a 00 move
|
|
645
|
+
m = this.addCommas(m);
|
|
646
|
+
if (!m.includes(",")) throw new Error("getAlignment: move is weird: " + m);
|
|
647
|
+
const [u, d] = m.split(",");
|
|
648
|
+
return { topA: u !== "0", bottomA: d !== "0" }
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
|
|
652
|
+
// =========================================================================
|
|
653
|
+
// SECTION 3: UNKARNIFY PIPELINE
|
|
654
|
+
// =========================================================================
|
|
655
|
+
|
|
656
|
+
/**
|
|
657
|
+
* unkarnifyHelp: does the actual unkarnifying
|
|
658
|
+
*
|
|
659
|
+
* @param {string} alg the alg
|
|
660
|
+
* @returns {string} surface-level unkarnified alg
|
|
661
|
+
*/
|
|
662
|
+
unkarnifyHelp(alg) {
|
|
663
|
+
// trim and replace random ass characters
|
|
664
|
+
alg = alg.trim().replaceAll(/[()]/g, "");
|
|
665
|
+
// " / " → "/"
|
|
666
|
+
alg = alg.replaceAll(/ ([\/\\\|]) /g, "$1");
|
|
667
|
+
if (/[\/\\\|]{2,}/.test(alg)) throw new Error("unkarnifyHelp: Two slices in a row.");
|
|
668
|
+
|
|
669
|
+
if (!this.isKarn(alg)) return alg; // not karn at all
|
|
670
|
+
|
|
671
|
+
// these can be "", if the alg starts/ends with a slice
|
|
672
|
+
let firstMove, lastMove;
|
|
673
|
+
if (!/[/\\| ]/.test(alg)) firstMove = lastMove = alg; // only one move
|
|
674
|
+
else {
|
|
675
|
+
firstMove = alg.match(/^([^/\\| ]*)[/\\| ]/)?.[1];
|
|
676
|
+
lastMove = alg.match(/[/\\| ]([^/\\| ]*)$/)?.[1];
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
// only tests if it literally starts with a slice
|
|
680
|
+
let startsSlice = ["/", "\\", "|"].includes(alg.charAt(0));
|
|
681
|
+
// grab the literal starting slice, or just use a /
|
|
682
|
+
let startingSlice = startsSlice ? alg.charAt(0) :
|
|
683
|
+
firstMove in SquanLib.karnToWCA ? "/" : "";
|
|
684
|
+
// same
|
|
685
|
+
let endingSlice = "/" === alg.at(-1) ? "/" :
|
|
686
|
+
lastMove in SquanLib.karnToWCA ? "/" : "";
|
|
687
|
+
|
|
688
|
+
// replace all possible slices with spaces now that we have slice start
|
|
689
|
+
alg = alg.replaceAll(/[/\\| ]+/g, ' ');
|
|
690
|
+
alg = this.addCommas(alg);
|
|
691
|
+
// now go through scramble move by move
|
|
692
|
+
let s = alg.split(" ").filter(Boolean);
|
|
693
|
+
for (let i = 0; i < s.length; i++)
|
|
694
|
+
if (s[i] in SquanLib.karnToWCA) s[i] = SquanLib.karnToWCA[s[i]].split(" ");
|
|
695
|
+
|
|
696
|
+
// high karns gone. now flatten
|
|
697
|
+
s = s.flat();
|
|
698
|
+
for (let i = 0; i < s.length; i++)
|
|
699
|
+
if (s[i] in SquanLib.karnToWCA) s[i] = SquanLib.karnToWCA[s[i]];
|
|
700
|
+
|
|
701
|
+
alg = startingSlice + s.join("/") + endingSlice;
|
|
702
|
+
// sanity replacements
|
|
703
|
+
alg = alg.replaceAll(/ +/g, "");
|
|
704
|
+
if (/[\/\\\|]{2,}/.test(alg)) throw new Error("unkarnifyHelp: Two slices in a row post-replacements.");
|
|
705
|
+
|
|
706
|
+
return alg;
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
/**
|
|
710
|
+
* unkarnify: master karn → WCA
|
|
711
|
+
* basically unkarnifyHelp + replaceShorthand with bling blings
|
|
712
|
+
*
|
|
713
|
+
* @param {string} alg the alg to be unkarnified
|
|
714
|
+
* @returns {string} unkarnified alg, duh
|
|
715
|
+
*/
|
|
716
|
+
unkarnify(alg) {
|
|
717
|
+
// overrides
|
|
718
|
+
if (alg in this.tempReplacements) return this.tempReplacements[alg];
|
|
719
|
+
|
|
720
|
+
// p scrambles
|
|
721
|
+
let isPScramble = /^p[ /\\|]/.test(alg);
|
|
722
|
+
let startingSlice;
|
|
723
|
+
if (isPScramble) {
|
|
724
|
+
startingSlice = alg.charAt(1) === " " ? "/" : alg.charAt(1);
|
|
725
|
+
alg = alg.slice(2, -3);
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
// legacy character substitutions
|
|
729
|
+
alg = alg
|
|
730
|
+
.replaceAll('&', '-1')
|
|
731
|
+
.replaceAll('^', '-2')
|
|
732
|
+
.replaceAll('9', '-3')
|
|
733
|
+
.replaceAll('8', '-4')
|
|
734
|
+
.replaceAll('7', '-5');
|
|
735
|
+
|
|
736
|
+
// expand move groups, e.g. "(U U')3" → "U U' U U' U U'"
|
|
737
|
+
for (const group of alg.matchAll(/(\(.*?\))(\d+)/g)) {
|
|
738
|
+
const inner = group[1].replaceAll(/[()]/g, '');
|
|
739
|
+
const count = parseInt(group[2], 10);
|
|
740
|
+
alg = alg.replace(group[0], Array(count).fill(inner).join(' '));
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
// the core defer
|
|
744
|
+
let final = this.replaceShorthands(this.unkarnifyHelp(alg));
|
|
745
|
+
|
|
746
|
+
// handle p scramble
|
|
747
|
+
if (isPScramble) {
|
|
748
|
+
if (["/", "\\", "|"].includes(final.charAt(0))) final = final.slice(1);
|
|
749
|
+
final = 'p' + startingSlice + final + "/p'";
|
|
750
|
+
}
|
|
751
|
+
final = final.replaceAll(/\/+/g, '/');
|
|
752
|
+
|
|
753
|
+
return final;
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
/**
|
|
757
|
+
* replaceShorthands: replace shorthands (bjj, fv, kk, …) in an alg,
|
|
758
|
+
* tracking alignment state to choose the correct shorthand.
|
|
759
|
+
*
|
|
760
|
+
* @param {string} alg the alg
|
|
761
|
+
* @returns {string} the alg with shorthands replaced... guys jsdoc is sometimes dumb
|
|
762
|
+
*/
|
|
763
|
+
replaceShorthands(alg) {
|
|
764
|
+
const moves = alg.split(/[\/\\\|]/);
|
|
765
|
+
|
|
766
|
+
// early out: no shorthands
|
|
767
|
+
const allKnown = moves.every(m =>
|
|
768
|
+
!m || !this.isKarn(m) || (' ' + m + ' ' in SquanLib.karnToWCA)
|
|
769
|
+
);
|
|
770
|
+
if (allKnown) return this.unkarnifyHelp(alg);
|
|
771
|
+
|
|
772
|
+
let topA = false, bottomA = false;
|
|
773
|
+
|
|
774
|
+
for (const move of moves) {
|
|
775
|
+
if (!move) continue;
|
|
776
|
+
|
|
777
|
+
if (move.includes(',')) {
|
|
778
|
+
// Numeric turn: update alignment tracker.
|
|
779
|
+
const [u, d] = move.split(',');
|
|
780
|
+
if (parseInt(u, 10) % 3 !== 0) topA = !topA;
|
|
781
|
+
if (parseInt(d, 10) % 3 !== 0) bottomA = !bottomA;
|
|
782
|
+
} else {
|
|
783
|
+
// shorthand
|
|
784
|
+
const key = SquanLib.alignmentIndependent.has(move.toLowerCase())
|
|
785
|
+
? move.toLowerCase()
|
|
786
|
+
: move.toLowerCase() + this.getAlignmentMove(topA, bottomA);
|
|
787
|
+
|
|
788
|
+
const replacement = SquanLib.shorthandToKarn[key];
|
|
789
|
+
if (replacement === undefined)
|
|
790
|
+
throw new Error(`replaceShorthands: "${move}" with alignment ${this.getAlignmentMove(topA, bottomA)} is not defined.`);
|
|
791
|
+
|
|
792
|
+
alg = alg.replace(move, replacement);
|
|
793
|
+
|
|
794
|
+
// Update alignment based on what the replacement expands to.
|
|
795
|
+
for (const sub of this.unkarnifyHelp(replacement).split('/')) {
|
|
796
|
+
if (!sub) continue;
|
|
797
|
+
const [u, d] = sub.split(',');
|
|
798
|
+
if (parseInt(u, 10) % 3 !== 0) topA = !topA;
|
|
799
|
+
if (parseInt(d, 10) % 3 !== 0) bottomA = !bottomA;
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
// unkarnify the shorthands that were replaced into the alg
|
|
805
|
+
return this.unkarnifyHelp(alg);
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
|
|
809
|
+
// =========================================================================
|
|
810
|
+
// SECTION 4: SCRAMBLE / ALG UTILITIES
|
|
811
|
+
// =========================================================================
|
|
812
|
+
|
|
813
|
+
/**
|
|
814
|
+
* parseScramble: tokenizes a WCA squan scramble
|
|
815
|
+
*
|
|
816
|
+
* @param {string} alg the alg
|
|
817
|
+
* @returns {{ type: string, top?: number, bottom?: number }[]} after parsing
|
|
818
|
+
*/
|
|
819
|
+
parseScramble(alg) {
|
|
820
|
+
const moves = [];
|
|
821
|
+
const parts = alg.replace(/[\/\\\|]/g, ' / ').trim().split(/\s+/).filter(Boolean);
|
|
822
|
+
for (let part of parts) {
|
|
823
|
+
if (part.startsWith("p"))
|
|
824
|
+
moves.push({ type: 'turn', top: 0, bottom: 0 });
|
|
825
|
+
else if (part === '/') {
|
|
826
|
+
moves.push({ type: 'twist' });
|
|
827
|
+
} else {
|
|
828
|
+
part = this.addCommas(part.replace(/[()]/g, ''));
|
|
829
|
+
if (!part.includes(",")) throw new Error(`parseScramble: move: ${part} is weird.`)
|
|
830
|
+
const [top, bottom] = part.split(',').map(n => parseInt(n.trim(), 10));
|
|
831
|
+
if (!isNaN(top) && !isNaN(bottom))
|
|
832
|
+
moves.push({ type: 'turn', top, bottom });
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
return moves;
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
/**
|
|
839
|
+
* twist: does a slice on a hex string
|
|
840
|
+
*
|
|
841
|
+
* @param {string} tlHex top layer hex, from UFL clockwise
|
|
842
|
+
* @param {string} blHex bottom layer hex, from DF clockwise
|
|
843
|
+
*/
|
|
844
|
+
twist(tlHex, blHex) {
|
|
845
|
+
return {
|
|
846
|
+
tlHex: tlHex.slice(0, 6) + blHex.slice(0, 6),
|
|
847
|
+
blHex: tlHex.slice(6) + blHex.slice(6),
|
|
848
|
+
};
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
/**
|
|
852
|
+
* doSlice: does a slice on a CSP-style OBL cube. throws error if unsliceable.
|
|
853
|
+
*
|
|
854
|
+
* @param {string} cube the CSP-style "BbWw" cube
|
|
855
|
+
* @returns {string} the cube post-slice, if sliceable
|
|
856
|
+
*/
|
|
857
|
+
doSlice(cube) {
|
|
858
|
+
const [UR, UL, DR, DL] = [
|
|
859
|
+
cube.slice(0, SquanLib.HALF_L),
|
|
860
|
+
cube.slice(SquanLib.HALF_L, SquanLib.LAYERL),
|
|
861
|
+
cube.slice(SquanLib.LAYERL, SquanLib.THREE_FOUR_L),
|
|
862
|
+
cube.slice(SquanLib.THREE_FOUR_L, SquanLib.CUBEL)
|
|
863
|
+
];
|
|
864
|
+
const isUP = (char) => char === char.toUpperCase();
|
|
865
|
+
const canSlice = (halfLayer) => (
|
|
866
|
+
!isUP(halfLayer.at(0)) || isUP(halfLayer.at(1)) &&
|
|
867
|
+
!isUP(halfLayer.at(-1)) || isUP(halfLayer.at(-2))
|
|
868
|
+
);
|
|
869
|
+
if (!([UR, UL, DR, DL].map(canSlice).every(Boolean)))
|
|
870
|
+
throw new Error("doSlice: unsliceable position encountered");
|
|
871
|
+
return DR + UL + UR + DL;
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
/**
|
|
875
|
+
* algToHex: get the hex state that the alg generates
|
|
876
|
+
*
|
|
877
|
+
* @param {string} alg an alg. karn is accepted.
|
|
878
|
+
* @returns {tlHex: string, blHex: string} the hex
|
|
879
|
+
*/
|
|
880
|
+
algToHex(alg) {
|
|
881
|
+
let tlHex = '011233455677';
|
|
882
|
+
let blHex = '998bbaddcffe';
|
|
883
|
+
for (const move of this.parseScramble(this.unkarnify(alg))) {
|
|
884
|
+
if (move.type === 'twist') {
|
|
885
|
+
({ tlHex, blHex } = this.twist(tlHex, blHex));
|
|
886
|
+
} else {
|
|
887
|
+
tlHex = this.shift(tlHex, -move.top);
|
|
888
|
+
blHex = this.shift(blHex, -move.bottom);
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
return { tlHex, blHex };
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
/**
|
|
895
|
+
* doMoves: does moves on a cube
|
|
896
|
+
*
|
|
897
|
+
* @param {string} ms the moves. karn accepted.
|
|
898
|
+
* @param {string} s the starting CSP-styled cube. leave to use the solved OBL state
|
|
899
|
+
* @returns {string} the cube post-moves
|
|
900
|
+
*/
|
|
901
|
+
doMoves(ms, s) {
|
|
902
|
+
if (s === undefined) s = SquanLib.SOLVED;
|
|
903
|
+
for (const tok of this.unkarnify(ms).split('/')) {
|
|
904
|
+
const m = tok.trim();
|
|
905
|
+
if (m !== '') {
|
|
906
|
+
const [u, d] = m.split(',').map(Number);
|
|
907
|
+
s = this.moveCube(s, u, d);
|
|
908
|
+
}
|
|
909
|
+
s = this.doSlice(s);
|
|
910
|
+
}
|
|
911
|
+
return this.doSlice(s);
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
/**
|
|
915
|
+
* moveCube: does a move on a CSP-style cube
|
|
916
|
+
*
|
|
917
|
+
* @param {string} cube the CSP-style cube
|
|
918
|
+
* @param {number} u the U move
|
|
919
|
+
* @param {number} d the D move
|
|
920
|
+
* @returns {string} the cube post-move
|
|
921
|
+
*/
|
|
922
|
+
moveCube(cube, u, d) {
|
|
923
|
+
return this.shift(cube.slice(0, SquanLib.LAYERL), u) +
|
|
924
|
+
this.shift(cube.slice(SquanLib.LAYERL), d);
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
/**
|
|
928
|
+
* invertScramble: reverses a scramble
|
|
929
|
+
*
|
|
930
|
+
* @param {string} alg the alg. karn is accepted.
|
|
931
|
+
* @returns {string} the reversed alg
|
|
932
|
+
*/
|
|
933
|
+
invertScramble(alg) {
|
|
934
|
+
if (!alg) return alg;
|
|
935
|
+
return this.unkarnify(alg).trim().split('/').reverse().map(part => {
|
|
936
|
+
part = part.trim();
|
|
937
|
+
const src = part.includes('(')
|
|
938
|
+
? part.match(/\(([^)]+)\)/)?.[1]
|
|
939
|
+
: part.includes(',') ? part : null;
|
|
940
|
+
if (!src) return part;
|
|
941
|
+
const inverted = src.split(',').map(v => {
|
|
942
|
+
const n = parseInt(v.trim(), 10);
|
|
943
|
+
return isNaN(n) ? v.trim() : String(-n);
|
|
944
|
+
}).join(',');
|
|
945
|
+
return part.includes('(') ? `(${inverted})` : inverted;
|
|
946
|
+
}).join('/');
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
/**
|
|
950
|
+
* isPBL: check if a hex is PBL
|
|
951
|
+
*
|
|
952
|
+
* @param {string} hex
|
|
953
|
+
* @returns {boolean} whether it's a PBL
|
|
954
|
+
*/
|
|
955
|
+
isPBL(hex) {
|
|
956
|
+
let { tlHex, blHex } = hex;
|
|
957
|
+
let tA = [...tlHex]; let bA = [...blHex];
|
|
958
|
+
if (tA[1] !== tA[2] ||
|
|
959
|
+
tA[4] !== tA[5] ||
|
|
960
|
+
tA[7] !== tA[8] ||
|
|
961
|
+
tA[10] !== tA[11]
|
|
962
|
+
) return false;
|
|
963
|
+
if (parseInt(tA[0], 10) % 2 !== 0 ||
|
|
964
|
+
parseInt(tA[1], 10) % 2 !== 1 ||
|
|
965
|
+
parseInt(tA[3], 10) % 2 !== 0 ||
|
|
966
|
+
parseInt(tA[4], 10) % 2 !== 1 ||
|
|
967
|
+
parseInt(tA[6], 10) % 2 !== 0 ||
|
|
968
|
+
parseInt(tA[7], 10) % 2 !== 1 ||
|
|
969
|
+
parseInt(tA[9], 10) % 2 !== 0 ||
|
|
970
|
+
parseInt(tA[10], 10) % 2 !== 1
|
|
971
|
+
) return false;
|
|
972
|
+
if (!tA.includes('0') ||
|
|
973
|
+
!tA.includes('1') ||
|
|
974
|
+
!tA.includes('2') ||
|
|
975
|
+
!tA.includes('3') ||
|
|
976
|
+
!tA.includes('4') ||
|
|
977
|
+
!tA.includes('5') ||
|
|
978
|
+
!tA.includes('6') ||
|
|
979
|
+
!tA.includes('7')
|
|
980
|
+
) return false;
|
|
981
|
+
if (bA[0] !== bA[1] ||
|
|
982
|
+
bA[3] !== bA[4] ||
|
|
983
|
+
bA[6] !== bA[7] ||
|
|
984
|
+
bA[9] !== bA[10]
|
|
985
|
+
) return false;
|
|
986
|
+
if (parseInt(bA[0], 16) % 2 !== 1 ||
|
|
987
|
+
parseInt(bA[2], 16) % 2 !== 0 ||
|
|
988
|
+
parseInt(bA[3], 16) % 2 !== 1 ||
|
|
989
|
+
parseInt(bA[5], 16) % 2 !== 0 ||
|
|
990
|
+
parseInt(bA[6], 16) % 2 !== 1 ||
|
|
991
|
+
parseInt(bA[8], 16) % 2 !== 0 ||
|
|
992
|
+
parseInt(bA[9], 16) % 2 !== 1 ||
|
|
993
|
+
parseInt(bA[11], 16) % 2 !== 0
|
|
994
|
+
) return false;
|
|
995
|
+
if (!bA.includes('8') ||
|
|
996
|
+
!bA.includes('9') ||
|
|
997
|
+
!bA.includes('a') ||
|
|
998
|
+
!bA.includes('b') ||
|
|
999
|
+
!bA.includes('c') ||
|
|
1000
|
+
!bA.includes('d') ||
|
|
1001
|
+
!bA.includes('e') ||
|
|
1002
|
+
!bA.includes('f')
|
|
1003
|
+
) return false;
|
|
1004
|
+
return true;
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
|
|
1008
|
+
// =========================================================================
|
|
1009
|
+
// SECTION 5: KARNIFY (WCA → karn)
|
|
1010
|
+
// =========================================================================
|
|
1011
|
+
|
|
1012
|
+
/**
|
|
1013
|
+
* karnify: converts WCA to karn.
|
|
1014
|
+
*
|
|
1015
|
+
* 1. assert that no two slices are next to each other and it's fully numeric
|
|
1016
|
+
* 2. compute any startingSlice and endingSlice
|
|
1017
|
+
* 3. normalize slice symbols
|
|
1018
|
+
* 4. go through move by move for base karns and only karnify first/last non-zero move
|
|
1019
|
+
* if they have slices around.
|
|
1020
|
+
* 5. join back into an alg while reattaching leading/trailing slices,
|
|
1021
|
+
* and dictReplace for high karns
|
|
1022
|
+
*
|
|
1023
|
+
* @param {string} alg WCA format
|
|
1024
|
+
* @returns {string} karn
|
|
1025
|
+
*/
|
|
1026
|
+
karnify(alg) {
|
|
1027
|
+
alg = alg.trim();
|
|
1028
|
+
if (/[\/\\\|]{2,}/.test(alg)) throw new Error("karnify: Two slices in a row.");
|
|
1029
|
+
if (this.isKarn(alg)) throw new Error("karnify: Alg has letters. Try unkarnifying first.")
|
|
1030
|
+
let startsSlice = ["/", "\\", "|"].includes(alg.charAt(0));
|
|
1031
|
+
let startingSlice = startsSlice ? alg.charAt(0) : "";
|
|
1032
|
+
let endsSlice = "/" === alg.at(-1);
|
|
1033
|
+
let endingSlice = endsSlice ? "/" : "";
|
|
1034
|
+
|
|
1035
|
+
// replace all possible slices with spaces
|
|
1036
|
+
alg = alg.replaceAll(/[/\\| ]+/g, ' ');
|
|
1037
|
+
|
|
1038
|
+
// now go through scramble move by move to apply base karn
|
|
1039
|
+
let s = alg.split(" ").filter(Boolean);
|
|
1040
|
+
for (let i = 0; i < s.length; i++) {
|
|
1041
|
+
if (i === 0 && !startsSlice) continue;
|
|
1042
|
+
if (i === s.length - 1 && !endsSlice) break;
|
|
1043
|
+
// good to replace
|
|
1044
|
+
s[i] = SquanLib.wcaToBaseKarn[s[i]] ? SquanLib.wcaToBaseKarn[s[i]] : s[i].replace(",", "");
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
alg = startingSlice + s.join(" ") + endingSlice;
|
|
1048
|
+
alg = this.dictReplace(alg, SquanLib.baseKarnToHighKarn);
|
|
1049
|
+
return alg;
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
|
|
1053
|
+
// =========================================================================
|
|
1054
|
+
// SECTION 6: MOVE MATH & SEQUENCE OPTIMIZER
|
|
1055
|
+
// =========================================================================
|
|
1056
|
+
|
|
1057
|
+
/**
|
|
1058
|
+
* legalMove: normalizes a raw turn value into the canonical squan range [−5, 6].
|
|
1059
|
+
*
|
|
1060
|
+
* @param {number} m raw turn value, e.g. −7 or 9
|
|
1061
|
+
* @returns {number} normalized value in [−5, 6]
|
|
1062
|
+
*/
|
|
1063
|
+
legalMove(m) {
|
|
1064
|
+
m = m % 12; // get a range from -11 to 11
|
|
1065
|
+
if (m < -5) return m + 12; // send -11 to -6 up
|
|
1066
|
+
if (m > 6) return m - 12; // send 7 to 11 down
|
|
1067
|
+
return m;
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
/**
|
|
1071
|
+
* addMoves: adds two move strings component-wise and legalizes each result.
|
|
1072
|
+
*
|
|
1073
|
+
* Tolerates "A"/"a" alignment markers. When one operand is an alignment
|
|
1074
|
+
* marker and the other is a move, the marker is flipped iff the top
|
|
1075
|
+
* component of the numeric move changes alignment (not a multiple of 3).
|
|
1076
|
+
* The above assumes that the move is in-CS.
|
|
1077
|
+
* Both operands cannot simultaneously be alignment markers.
|
|
1078
|
+
*
|
|
1079
|
+
* @param {string} move1 "top,bot" string, OR "A"/"a" alignment marker
|
|
1080
|
+
* @param {string} move2 "top,bot" string, OR "A"/"a" alignment marker
|
|
1081
|
+
* @returns {string}
|
|
1082
|
+
*
|
|
1083
|
+
* @example
|
|
1084
|
+
* addMoves("3,0", "-3,0") // → "0,0"
|
|
1085
|
+
* addMoves("2,-1", "1,-2") // → "3,-3"
|
|
1086
|
+
* addMoves("5,-1", "3,0") // → "-4,-1"
|
|
1087
|
+
* addMoves("A", "2,-1") // → "a" (2 % 3 !== 0 → flip)
|
|
1088
|
+
* addMoves("A", "3,0") // → "A" (3 % 3 === 0 → no flip)
|
|
1089
|
+
*/
|
|
1090
|
+
addMoves(move1, move2) {
|
|
1091
|
+
if (!move1 && !move2) throw new Error("addMoves: both moves are empty.");
|
|
1092
|
+
else if (!move1) return move2;
|
|
1093
|
+
else if (!move2) return move1;
|
|
1094
|
+
const flip = { A: 'a', a: 'A' };
|
|
1095
|
+
if (move1 in flip && move2 in flip)
|
|
1096
|
+
throw new Error("addMoves: both moves cannot be alignment markers.");
|
|
1097
|
+
if (move1 in flip) {
|
|
1098
|
+
const top = parseInt(move2.split(',')[0], 10);
|
|
1099
|
+
return this.changesAlignment(top) ? flip[move1] : move1;
|
|
1100
|
+
} else if (move2 in flip) {
|
|
1101
|
+
const top = parseInt(move1.split(',')[0], 10);
|
|
1102
|
+
return this.changesAlignment(top) ? flip[move2] : move2;
|
|
1103
|
+
}
|
|
1104
|
+
const [u1, d1] = move1.split(',').map(Number);
|
|
1105
|
+
const [u2, d2] = move2.split(',').map(Number);
|
|
1106
|
+
return `${this.legalMove(u1 + u2)},${this.legalMove(d1 + d2)}`;
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
/**
|
|
1110
|
+
* changesAlignment: check if performing this turn value changes the
|
|
1111
|
+
* alignment state (i.e. the turn is not a multiple of 3). Assumes everything
|
|
1112
|
+
* is in CS.
|
|
1113
|
+
*
|
|
1114
|
+
* @param {number} m top-layer turn value,
|
|
1115
|
+
* @returns {boolean}
|
|
1116
|
+
*/
|
|
1117
|
+
changesAlignment(m) {
|
|
1118
|
+
return m % 3 !== 0;
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
/**
|
|
1122
|
+
* optimize: replaces known optimizable moves (the OPTIM table) in a WCA alg
|
|
1123
|
+
*
|
|
1124
|
+
* 1. if no more optimization can be done, exit.
|
|
1125
|
+
* 2. split on "/" into an array
|
|
1126
|
+
* 3. walk to the next slice
|
|
1127
|
+
* 4. at each slice, check if any OPTIM key match the alg starting at that position
|
|
1128
|
+
* 5. if match:
|
|
1129
|
+
* - "/0,0/": merge the two surrounding moves
|
|
1130
|
+
* - generally: merge the first replacement move into the preceding move, merge
|
|
1131
|
+
* the last into the succeeding move, and replace the inners
|
|
1132
|
+
* 6. restart the outer loop after any change
|
|
1133
|
+
*
|
|
1134
|
+
* @param {string} alg slash-separated WCA alg, e.g. "A/-3,0/3,3/3,3/a"
|
|
1135
|
+
* @returns {string} optimized alg
|
|
1136
|
+
*/
|
|
1137
|
+
optimize(alg) {
|
|
1138
|
+
const optimKeys = Object.keys(SquanLib.OPTIM);
|
|
1139
|
+
while (this.dictReplace(alg, SquanLib.OPTIM) !== alg) {
|
|
1140
|
+
const moves = alg.split('/').map(m => m.trim());
|
|
1141
|
+
let atSlice = 0;
|
|
1142
|
+
let cycleCompleted = false;
|
|
1143
|
+
for (let i = 0; i < alg.length; i++) {
|
|
1144
|
+
if (cycleCompleted) break;
|
|
1145
|
+
if (alg[i] !== '/') continue;
|
|
1146
|
+
// only stop when scramble[i] is a slice
|
|
1147
|
+
atSlice++;
|
|
1148
|
+
for (const optimable of optimKeys) {
|
|
1149
|
+
// if the OPTIM key is longer than what's left of scramble
|
|
1150
|
+
if (alg.length - 1 - i < optimable.length) continue;
|
|
1151
|
+
// if it doesn't match
|
|
1152
|
+
if (alg.slice(i, i + optimable.length) !== optimable) continue;
|
|
1153
|
+
|
|
1154
|
+
// match!!
|
|
1155
|
+
if (optimable === '/0,0/') {
|
|
1156
|
+
// special case: merge surrounding moves
|
|
1157
|
+
moves[atSlice - 1] = this.addMoves(moves[atSlice - 1], moves[atSlice + 1]);
|
|
1158
|
+
moves.splice(atSlice, 2);
|
|
1159
|
+
} else {
|
|
1160
|
+
const optimableLen = optimable.split('/').length;
|
|
1161
|
+
const optimTo = SquanLib.OPTIM[optimable].split('/');
|
|
1162
|
+
// 2 represents the leading and trailing slash of optimable (forced)
|
|
1163
|
+
const delSliceNum = optimableLen - 2;
|
|
1164
|
+
// merge moves, even when empty (handled by addMoves)
|
|
1165
|
+
moves[atSlice - 1] = this.addMoves(moves[atSlice - 1], optimTo.shift());
|
|
1166
|
+
moves[atSlice + optimableLen - 2] = this.addMoves(
|
|
1167
|
+
moves[atSlice + optimableLen - 2],
|
|
1168
|
+
optimTo.pop()
|
|
1169
|
+
);
|
|
1170
|
+
moves.splice(atSlice, delSliceNum, ...optimTo);
|
|
1171
|
+
}
|
|
1172
|
+
alg = moves.join('/');
|
|
1173
|
+
cycleCompleted = true;
|
|
1174
|
+
break;
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
return alg;
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
|
|
1182
|
+
// =========================================================================
|
|
1183
|
+
// SECTION 7: ERGONOMICS RATING
|
|
1184
|
+
// =========================================================================
|
|
1185
|
+
|
|
1186
|
+
/**
|
|
1187
|
+
* getMoveValue: look up the ergonomic cost of a single move.
|
|
1188
|
+
* @param {boolean} startA is it top misalign right now?
|
|
1189
|
+
* @param {boolean} upslice is this an upslice?
|
|
1190
|
+
* @param {string} move e.g. "3,0"
|
|
1191
|
+
* @returns {number}
|
|
1192
|
+
*/
|
|
1193
|
+
getMoveValue(startA, upslice, move) {
|
|
1194
|
+
let comma = move.indexOf(',');
|
|
1195
|
+
if (comma === -1) {
|
|
1196
|
+
comma = this.addCommas(move).indexOf(',');
|
|
1197
|
+
if (comma === -1) throw new Error(`getMoveValue: move: ${move} is weird`);
|
|
1198
|
+
}
|
|
1199
|
+
const topMove = parseInt(move.slice(0, comma), 10);
|
|
1200
|
+
const slashCh = upslice ? '/' : '\\';
|
|
1201
|
+
let key;
|
|
1202
|
+
if (topMove % 3 === 0) {
|
|
1203
|
+
key = (startA ? 'A' : 'a') + slashCh + move;
|
|
1204
|
+
} else {
|
|
1205
|
+
key = slashCh + move;
|
|
1206
|
+
}
|
|
1207
|
+
return SquanLib.MOVE_VALUES.get(key) ?? 5; // if unknown, just say 5
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
/**
|
|
1211
|
+
* getOverwork: calculates how much overwork is present in an alg, along with bonus.
|
|
1212
|
+
* @param {string[]} moves array of "top,bot" strings (interior moves only)
|
|
1213
|
+
* @returns {{ movement: number, bonus: number }}
|
|
1214
|
+
*/
|
|
1215
|
+
getOverwork(moves) {
|
|
1216
|
+
const tops = [], bots = [];
|
|
1217
|
+
for (const m of moves) {
|
|
1218
|
+
let c = m.indexOf(',');
|
|
1219
|
+
if (c === -1) {
|
|
1220
|
+
const m2 = this.addCommas(m);
|
|
1221
|
+
c = m2.indexOf(",");
|
|
1222
|
+
if (c === -1) throw new Error(`getOverwork: in moves, m: ${m} is weird.`);
|
|
1223
|
+
}
|
|
1224
|
+
tops.push(parseInt(m.slice(0, c), 10) || 0);
|
|
1225
|
+
bots.push(parseInt(m.slice(c + 1), 10) || 0);
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
let movement = 0, bonus = 0;
|
|
1229
|
+
|
|
1230
|
+
// penalize streaks of lefty turns for both layers
|
|
1231
|
+
// U layer
|
|
1232
|
+
let streak = 0, closestMov = 0, buffer = 0;
|
|
1233
|
+
for (const t of tops) {
|
|
1234
|
+
const isLeft = (t === 6 || t < 0);
|
|
1235
|
+
if (isLeft) {
|
|
1236
|
+
streak++;
|
|
1237
|
+
if (!SquanLib.CLOSEST_MAP.has(t))
|
|
1238
|
+
throw new Error("getOverwork: top move is weird: " + t)
|
|
1239
|
+
closestMov += Math.abs(SquanLib.CLOSEST_MAP.get(t));
|
|
1240
|
+
buffer += Math.abs(t);
|
|
1241
|
+
// has streak and nontrivial move → penalize
|
|
1242
|
+
if (streak > 1 && closestMov > 3) { movement += buffer; buffer = 0; }
|
|
1243
|
+
} else { streak = 0; closestMov = 0; buffer = 0; } // reset streak
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
// D layer
|
|
1247
|
+
streak = 0; closestMov = 0; buffer = 0;
|
|
1248
|
+
for (const b of bots) {
|
|
1249
|
+
const isLeft = b > 0;
|
|
1250
|
+
if (isLeft) {
|
|
1251
|
+
streak++;
|
|
1252
|
+
if (!SquanLib.CLOSEST_MAP.has(b))
|
|
1253
|
+
throw new Error("getOverwork: top move is weird: " + b)
|
|
1254
|
+
closestMov += Math.abs(SquanLib.CLOSEST_MAP.get(b));
|
|
1255
|
+
buffer += Math.abs(b);
|
|
1256
|
+
if (streak > 1 && closestMov > 3) { movement += buffer; buffer = 0; }
|
|
1257
|
+
} else { streak = 0; closestMov = 0; buffer = 0; }
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
// Bonus: count consecutive pairs that cancel.
|
|
1261
|
+
for (let i = 0; i + 1 < tops.length; i++) {
|
|
1262
|
+
if (tops[i] + tops[i + 1] === 0) bonus++;
|
|
1263
|
+
if (bots[i] + bots[i + 1] === 0) bonus++;
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
return { movement, bonus };
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
/**
|
|
1270
|
+
* rateAlg: ergonomic rating for a single in CS squan alg
|
|
1271
|
+
*
|
|
1272
|
+
* @param {string} algRaw raw alg (karn or WCA)
|
|
1273
|
+
* @param {boolean} initialTopA if the alg starts top misalign
|
|
1274
|
+
* @param {object} [weights] override default weight constants
|
|
1275
|
+
* @returns {{ score: number, sliceStart: string }}
|
|
1276
|
+
* sliceStart: '/' (prefer upslice), '\' (prefer downslice), or ' ' (no preference)
|
|
1277
|
+
*/
|
|
1278
|
+
rateAlg(algRaw, initialTopA = false, weights = {}) {
|
|
1279
|
+
const W1 = weights.W1 ?? 34; // per-move ergonomic rating
|
|
1280
|
+
const W2 = weights.W2 ?? 100; // slice-count penalty
|
|
1281
|
+
const W3 = weights.W3 ?? 38; // overwork penalty
|
|
1282
|
+
const W4 = weights.W4 ?? 500; // constant term
|
|
1283
|
+
const W5 = weights.W5 ?? 10; // bonus
|
|
1284
|
+
|
|
1285
|
+
// strip brackets e.g. "[7|14]"
|
|
1286
|
+
let a = algRaw.replace(/\[.*$/, '').trim();
|
|
1287
|
+
|
|
1288
|
+
// unkarnify if needed
|
|
1289
|
+
const numeric = this.isKarn(a) ? this.unkarnify(a) : a.replaceAll(' ', '');
|
|
1290
|
+
|
|
1291
|
+
const rawParts = numeric.split('/');
|
|
1292
|
+
// keep leading slice for up/downslice, drop trailing slice
|
|
1293
|
+
const r = rawParts.filter((pt, i) => i === 0 || pt.trim() !== '').map(p => p.trim());
|
|
1294
|
+
const sliceCount = r.length - 1;
|
|
1295
|
+
if (sliceCount <= 0) return { score: W4, sliceStart: ' ' };
|
|
1296
|
+
|
|
1297
|
+
let ergoUp = 0, ergoDown = 0;
|
|
1298
|
+
let isTopA = false, oddSlice = true;
|
|
1299
|
+
|
|
1300
|
+
for (let i = 0; i < r.length - 1; i++) {
|
|
1301
|
+
let c = r[i].indexOf(',');
|
|
1302
|
+
if (c === -1) {
|
|
1303
|
+
const m = this.addCommas(r[i]);
|
|
1304
|
+
c = m.indexOf(',');
|
|
1305
|
+
if (c === -1)
|
|
1306
|
+
throw new Error(`rateAlg:\nalg: ${algRaw}\nmove: ${r[i]}\nis weird.`)
|
|
1307
|
+
}
|
|
1308
|
+
const t = parseInt(r[i].slice(0, c), 10);
|
|
1309
|
+
if (isNaN(t)) throw new Error(`rateAlg:\nalg: ${algRaw}\nmove: ${r[i]}\nis weird.`)
|
|
1310
|
+
|
|
1311
|
+
if (i === 0) {
|
|
1312
|
+
// 1st move: use to determine initial alignment
|
|
1313
|
+
isTopA = initialTopA !== (t % 3 !== 0);
|
|
1314
|
+
oddSlice = true;
|
|
1315
|
+
continue;
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
ergoUp += this.getMoveValue(isTopA, oddSlice, r[i]);
|
|
1319
|
+
ergoDown += this.getMoveValue(isTopA, !oddSlice, r[i]);
|
|
1320
|
+
isTopA = isTopA !== (t % 3 !== 0);
|
|
1321
|
+
oddSlice = !oddSlice;
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
const phase1 = W1 * Math.max(ergoUp, ergoDown) / sliceCount; // average move rating
|
|
1325
|
+
|
|
1326
|
+
let sliceStart = '|';
|
|
1327
|
+
if (Math.abs(ergoUp - ergoDown) / sliceCount > 5)
|
|
1328
|
+
sliceStart = ergoUp > ergoDown ? '/' : '\\';
|
|
1329
|
+
|
|
1330
|
+
const phase2 = W2 * sliceCount;
|
|
1331
|
+
const interior = r.slice(1, -1);
|
|
1332
|
+
const { movement, bonus } = this.getOverwork(interior);
|
|
1333
|
+
const phase3 = W3 * movement / sliceCount;
|
|
1334
|
+
const phase4 = bonus * W5 / sliceCount;
|
|
1335
|
+
|
|
1336
|
+
return { score: phase1 - phase2 - phase3 + phase4 + W4, sliceStart };
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
/**
|
|
1340
|
+
* rateAndSort: rate a list of algs and return them sorted by ergonomics high to low
|
|
1341
|
+
*
|
|
1342
|
+
* @param {string[]} algLines raw alg strings
|
|
1343
|
+
* @param {string} [posHex=''] position hex (used to determine initialTopA)
|
|
1344
|
+
* @returns {{ alg: string, score: number }[]}
|
|
1345
|
+
*/
|
|
1346
|
+
rateAndSort(algLines, posHex = '') {
|
|
1347
|
+
// Determine initial top-layer alignment from position hex.
|
|
1348
|
+
let initialTopA = false;
|
|
1349
|
+
if (posHex) {
|
|
1350
|
+
const ch = posHex[0];
|
|
1351
|
+
initialTopA = /[A-HU-W]/i.test(ch);
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
return algLines.map(line => {
|
|
1355
|
+
const bracketPos = line.indexOf('[');
|
|
1356
|
+
const algOnly = bracketPos > 0 ? line.slice(0, bracketPos).trim() : line.trim();
|
|
1357
|
+
let result = { alg: line, score: 500 };
|
|
1358
|
+
let rated = false;
|
|
1359
|
+
let sliceStart = ' ';
|
|
1360
|
+
|
|
1361
|
+
// Pre-unkarnify so rateAlg always receives a numeric string.
|
|
1362
|
+
const numericAlg = this.isKarn(algOnly) ? this.unkarnify(algOnly) : algOnly;
|
|
1363
|
+
({ score: result.score, sliceStart } = this.rateAlg(numericAlg, initialTopA));
|
|
1364
|
+
rated = true;
|
|
1365
|
+
|
|
1366
|
+
if (rated && ["/", "\\", "|"].includes(sliceStart)) {
|
|
1367
|
+
// Replace the first '/' in the alg-only portion of the display line.
|
|
1368
|
+
const slashPos = line.indexOf('/');
|
|
1369
|
+
if (slashPos >= 0)
|
|
1370
|
+
result.alg = line.slice(0, slashPos) + sliceStart + line.slice(slashPos + 1);
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1373
|
+
return result;
|
|
1374
|
+
}).sort((a, b) => b.score - a.score);
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
// =========================================================================
|
|
1378
|
+
// SECTION 8: ALG TRANSFORM
|
|
1379
|
+
// =========================================================================
|
|
1380
|
+
|
|
1381
|
+
/**
|
|
1382
|
+
* sepIndex: helper to identify where the D move starts in a move without comma
|
|
1383
|
+
*
|
|
1384
|
+
* @param {string} a a move without commas, e.g. "3-3"
|
|
1385
|
+
* @returns {number} the 0-based index of the start of the D move
|
|
1386
|
+
* @example "3-3" → 1
|
|
1387
|
+
*/
|
|
1388
|
+
sepIndex(a) {
|
|
1389
|
+
let inx = 0;
|
|
1390
|
+
for (const ch of a) { inx++; if (/\d/.test(ch)) break; }
|
|
1391
|
+
return inx;
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1394
|
+
/**
|
|
1395
|
+
* compl: get the complement of a move
|
|
1396
|
+
*
|
|
1397
|
+
* @param {string} a a move without commas, e.g. "-12"
|
|
1398
|
+
* @returns {string} the complement move
|
|
1399
|
+
* @example "-12" → "5-4"
|
|
1400
|
+
*/
|
|
1401
|
+
compl(a) {
|
|
1402
|
+
if (!a) return a;
|
|
1403
|
+
const inx = this.sepIndex(a);
|
|
1404
|
+
return String(this.legalMove(6 + parseInt(a.slice(0, inx), 10))) +
|
|
1405
|
+
String(this.legalMove(6 + parseInt(a.slice(inx), 10)));
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
/**
|
|
1409
|
+
* lf: get the layer flip of a move
|
|
1410
|
+
*
|
|
1411
|
+
* @param {string} a a move without commas, e.g. "-12"
|
|
1412
|
+
* @returns {string} the layer flip move
|
|
1413
|
+
* @example "-12" → "2-1"
|
|
1414
|
+
*/
|
|
1415
|
+
lf(a) {
|
|
1416
|
+
if (!a) return a;
|
|
1417
|
+
const inx = this.sepIndex(a);
|
|
1418
|
+
return a.slice(inx) + a.slice(0, inx);
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
/**
|
|
1422
|
+
* compact: parse any input format into space-separated no-comma compact
|
|
1423
|
+
* segments, e.g. "30 -33 30". uses full unkarnify for karn input.
|
|
1424
|
+
*
|
|
1425
|
+
* @param {string} algIn the inputted alg. any input format
|
|
1426
|
+
* @returns {string} the compact format
|
|
1427
|
+
*/
|
|
1428
|
+
compact(algIn) {
|
|
1429
|
+
let alg = algIn
|
|
1430
|
+
.replace(/\[.*?\]/g, "")
|
|
1431
|
+
.replace(/[()]/g, "")
|
|
1432
|
+
.replaceAll(" ", "").trim();
|
|
1433
|
+
if (this.isKarn(alg)) {
|
|
1434
|
+
const numeric = this.unkarnify(alg);
|
|
1435
|
+
return numeric.split("/").filter(Boolean).map(m => {
|
|
1436
|
+
if (!m.includes(","))
|
|
1437
|
+
throw new Error(
|
|
1438
|
+
`algToInternal: m doesn't have commas post karnifying: ${m}`
|
|
1439
|
+
)
|
|
1440
|
+
const [u, d] = m.split(",");
|
|
1441
|
+
return String(this.legalMove(parseInt(u, 10))) +
|
|
1442
|
+
String(this.legalMove(parseInt(d, 10)));
|
|
1443
|
+
}).join(" ");
|
|
1444
|
+
}
|
|
1445
|
+
alg = alg.trim();
|
|
1446
|
+
if (alg.includes("/")) {
|
|
1447
|
+
return alg.split("/").filter(Boolean).map(m => {
|
|
1448
|
+
if (!m.includes(",")) m = this.addCommas(m);
|
|
1449
|
+
if (!m.includes(","))
|
|
1450
|
+
throw new Error(
|
|
1451
|
+
`algToInternal: m doesn't have commas post addComma: ${m}`
|
|
1452
|
+
)
|
|
1453
|
+
const [u, d] = m.split(",");
|
|
1454
|
+
return String(this.legalMove(parseInt(u, 10))) +
|
|
1455
|
+
String(this.legalMove(parseInt(d, 10)));
|
|
1456
|
+
}).join(" ");
|
|
1457
|
+
}
|
|
1458
|
+
return alg.split(" ").filter(p => p).map(m => {
|
|
1459
|
+
const p = m.includes(",")
|
|
1460
|
+
? m.split(",")
|
|
1461
|
+
: [m.slice(0, this.sepIndex(m)), m.slice(this.sepIndex(m))];
|
|
1462
|
+
return String(this.legalMove(parseInt(p[0], 10))) +
|
|
1463
|
+
String(this.legalMove(parseInt(p[1], 10)));
|
|
1464
|
+
}).join(" ");
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
/**
|
|
1468
|
+
* countY2Positions: count the number of possible y2 positions
|
|
1469
|
+
*
|
|
1470
|
+
* @param {string} algIn the alg
|
|
1471
|
+
* @returns {number} how many positions the alg can y2 at
|
|
1472
|
+
*/
|
|
1473
|
+
countY2Positions(algIn) {
|
|
1474
|
+
const segs = this.compact(algIn).split(" ").filter(p => p);
|
|
1475
|
+
return Math.max(0, segs.length - 3);
|
|
1476
|
+
}
|
|
1477
|
+
|
|
1478
|
+
/**
|
|
1479
|
+
* applyY2s: y2 an alg at a list of specified slices, and give comments
|
|
1480
|
+
*
|
|
1481
|
+
* @param {string} algIn the alg
|
|
1482
|
+
* @param {number[]} lfLst the list of slice positions to y2 at
|
|
1483
|
+
* @param {boolean} k whether to output as karn, leave to use input format
|
|
1484
|
+
* @returns {string} the y2'ed alg, plus comments
|
|
1485
|
+
*/
|
|
1486
|
+
applyY2s(algIn, lfLst, k = null) {
|
|
1487
|
+
let alg = algIn.replaceAll(/\[.*?\]/g, "").trim();
|
|
1488
|
+
if (!lfLst) lfLst = [];
|
|
1489
|
+
|
|
1490
|
+
const ki = this.isKarn(alg);
|
|
1491
|
+
const kOut = k === null ? ki : k;
|
|
1492
|
+
|
|
1493
|
+
alg = this.unkarnify(algIn);
|
|
1494
|
+
let alst = alg.split("/");
|
|
1495
|
+
|
|
1496
|
+
// save for alignment-changes check
|
|
1497
|
+
const firstMove = alst[0];
|
|
1498
|
+
const lastMove = alst[alst.length - 1];
|
|
1499
|
+
|
|
1500
|
+
// Apply explicit y2s to interior moves
|
|
1501
|
+
let lfing = false, facingD = false;
|
|
1502
|
+
for (let i = 1; i <= alst.length - 3; i++) {
|
|
1503
|
+
let m = alst[i];
|
|
1504
|
+
m = facingD ? this.lf(m) : m;
|
|
1505
|
+
if (lfLst.includes(i)) { m = this.compl(m); lfing = !lfing; }
|
|
1506
|
+
facingD = lfing ? !facingD : facingD;
|
|
1507
|
+
alst[i] = m;
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
// fix last interior move
|
|
1511
|
+
const lastIntIdx = alst.length - 2;
|
|
1512
|
+
if (facingD) alst[lastIntIdx] = this.lf(alst[lastIntIdx]);
|
|
1513
|
+
if (lfing !== facingD) alst[lastIntIdx] = this.compl(alst[lastIntIdx]);
|
|
1514
|
+
const lastIntMove = alst[lastIntIdx];
|
|
1515
|
+
|
|
1516
|
+
// add commas to all moves
|
|
1517
|
+
for (let i = 0; i < alst.length; i++) alst[i] = this.addCommas(alst[i]);
|
|
1518
|
+
alg = alst.join('/');
|
|
1519
|
+
|
|
1520
|
+
// build comment
|
|
1521
|
+
let comment = "";
|
|
1522
|
+
if (lastIntMove && !SquanLib.GOOD_FINISHES.has(lastIntMove))
|
|
1523
|
+
comment += ' (bad finish)';
|
|
1524
|
+
const { topA: topAstart, bottomA: bottomAstart } = this.getAlignment(firstMove);
|
|
1525
|
+
const { topA: topAend, bottomA: bottomAend } = this.getAlignment(lastMove);
|
|
1526
|
+
if (topAstart !== bottomAstart &&
|
|
1527
|
+
topAstart !== topAend &&
|
|
1528
|
+
topAend !== bottomAend)
|
|
1529
|
+
comment += ' (alignment changes in CS)';
|
|
1530
|
+
|
|
1531
|
+
if (kOut) {
|
|
1532
|
+
alg = this.karnify(alg);
|
|
1533
|
+
}
|
|
1534
|
+
|
|
1535
|
+
return alg + comment;
|
|
1536
|
+
}
|
|
1537
|
+
|
|
1538
|
+
// =========================================================================
|
|
1539
|
+
// SECTION 9: OBL and OBLP Utilities
|
|
1540
|
+
// =========================================================================
|
|
1541
|
+
|
|
1542
|
+
/**
|
|
1543
|
+
* layerFlip: give the layer flipped state of a CSP-style OBL state
|
|
1544
|
+
*
|
|
1545
|
+
* @param {string} state an OBL state of however long
|
|
1546
|
+
* @returns {string} the layer flip of that
|
|
1547
|
+
*/
|
|
1548
|
+
layerFlip(state) {
|
|
1549
|
+
const layerFlipMap = { 'b': 'w', 'B': 'W', 'w': 'b', 'W': 'B' };
|
|
1550
|
+
return [...state].map(c => {
|
|
1551
|
+
if (c in layerFlipMap) return layerFlipMap[c];
|
|
1552
|
+
throw new Error("layerFlip: unrecognized character: " + c)
|
|
1553
|
+
}).join('');
|
|
1554
|
+
}
|
|
1555
|
+
|
|
1556
|
+
/**
|
|
1557
|
+
* shift: basically does a move on a layer
|
|
1558
|
+
*
|
|
1559
|
+
* @param {string} a CSP-style (layer) state
|
|
1560
|
+
* @param {number} amount the move. literally. cw = positive
|
|
1561
|
+
* @returns {string} the state after the move
|
|
1562
|
+
*/
|
|
1563
|
+
shift(a, amount) {
|
|
1564
|
+
amount = ((-amount % a.length) + a.length) % a.length;
|
|
1565
|
+
return a.slice(amount) + a.slice(0, amount);
|
|
1566
|
+
}
|
|
1567
|
+
|
|
1568
|
+
/**
|
|
1569
|
+
* oblName: turns a possibleOBL array into a string
|
|
1570
|
+
*
|
|
1571
|
+
* @param {string[]} obl a possibleOBL-style array
|
|
1572
|
+
* @returns {string} the actual OBL name
|
|
1573
|
+
*/
|
|
1574
|
+
oblName(obl) {
|
|
1575
|
+
return obl[0] ? `${obl[0]} ${obl[1]}/${obl[2]}` : `${obl[1]}/${obl[2]}`;
|
|
1576
|
+
}
|
|
1577
|
+
|
|
1578
|
+
/**
|
|
1579
|
+
* layerFlipName: layer flips an OBL name
|
|
1580
|
+
*
|
|
1581
|
+
* @param {string} obl an unspecific OBL name
|
|
1582
|
+
* @returns {string} the layer flipped version
|
|
1583
|
+
* @example "good bunny/thumb" → "good thumb/bunny"
|
|
1584
|
+
*/
|
|
1585
|
+
layerFlipName(obl) {
|
|
1586
|
+
obl = obl.replace('/', ' ');
|
|
1587
|
+
const parts = obl.split(' ');
|
|
1588
|
+
if (parts.length === 2) return parts[1] + '/' + parts[0];
|
|
1589
|
+
return parts[0] + ' ' + parts[2] + '/' + parts[1];
|
|
1590
|
+
}
|
|
1591
|
+
|
|
1592
|
+
/**
|
|
1593
|
+
* speToNonSpe: get the nonspecific OBL of a specific one
|
|
1594
|
+
*
|
|
1595
|
+
* @param {string} obl the specific obl
|
|
1596
|
+
* @returns {string} the unspecific obl
|
|
1597
|
+
*/
|
|
1598
|
+
speToNonSpe(obl) {
|
|
1599
|
+
const [uObl, dObl] = obl.split('/');
|
|
1600
|
+
const u = uObl.split(' ').pop();
|
|
1601
|
+
const d = dObl.split(' ').pop();
|
|
1602
|
+
const candidates = SquanLib.POSSIBLE_OBL
|
|
1603
|
+
.filter(c => c.includes(u) && c.includes(d))
|
|
1604
|
+
.map(c => this.oblName(c));
|
|
1605
|
+
for (const cand of candidates) {
|
|
1606
|
+
const specials = SquanLib.OBL_TRANSLATION[cand] || [];
|
|
1607
|
+
for (const spe of specials) {
|
|
1608
|
+
if (spe === obl) return cand;
|
|
1609
|
+
const [s1, s2] = spe.split('/');
|
|
1610
|
+
if (`${s2}/${s1}` === obl) return this.layerFlipName(cand);
|
|
1611
|
+
}
|
|
1612
|
+
}
|
|
1613
|
+
throw new Error(`speToNonSpe: No non-specific OBL found for: ${obl}`);
|
|
1614
|
+
}
|
|
1615
|
+
|
|
1616
|
+
/**
|
|
1617
|
+
* isOBLCase: checks if a CSP-style OBL state is an OBL
|
|
1618
|
+
*
|
|
1619
|
+
* @param {string} l the CSP-style state
|
|
1620
|
+
* @param {string} target the OBL name (one layer)
|
|
1621
|
+
* @returns {number | boolean} the angle offset, 1-4, or false
|
|
1622
|
+
*/
|
|
1623
|
+
isOBLCase(l, target) {
|
|
1624
|
+
const targetPattern = Object.entries(SquanLib.OBLToEnglish)
|
|
1625
|
+
.find(([, v]) => v === target
|
|
1626
|
+
)?.[0];
|
|
1627
|
+
if (!targetPattern) return false;
|
|
1628
|
+
// to corner first
|
|
1629
|
+
if (l[0] !== l[0].toUpperCase()) l = this.shift(l, -1);
|
|
1630
|
+
for (let m = 0; m < 4; m++) {
|
|
1631
|
+
if (targetPattern === this.shift(l, -3 * m)) return m;
|
|
1632
|
+
}
|
|
1633
|
+
const noTT = !['T', 'tie'].includes(target.split(' ').pop());
|
|
1634
|
+
if (noTT) {
|
|
1635
|
+
// free to change the color
|
|
1636
|
+
const fl = this.layerFlip(l);
|
|
1637
|
+
for (let m = 0; m < 4; m++) {
|
|
1638
|
+
if (targetPattern === this.shift(fl, -3 * m)) return m;
|
|
1639
|
+
}
|
|
1640
|
+
}
|
|
1641
|
+
return false;
|
|
1642
|
+
}
|
|
1643
|
+
|
|
1644
|
+
/**
|
|
1645
|
+
* layerToOBL: convert a CSP-style "BbWw" layer to the OBL name and angle offset
|
|
1646
|
+
*
|
|
1647
|
+
* @param {string} layer CSP-style OBL layer
|
|
1648
|
+
* @returns {{obl: string, angleOffset: number}} the obl and and angle offset
|
|
1649
|
+
*/
|
|
1650
|
+
layerToOBL(layer) {
|
|
1651
|
+
for (const obl of Object.keys(SquanLib.OBLToState))
|
|
1652
|
+
if (this.isOBLCase(layer, obl))
|
|
1653
|
+
return { obl, angleOffset: this.isOBLCase(layer, obl) };
|
|
1654
|
+
throw new Error('layerToOBL: no OBL matched layer: ' + layer);
|
|
1655
|
+
}
|
|
1656
|
+
|
|
1657
|
+
|
|
1658
|
+
|
|
1659
|
+
/**
|
|
1660
|
+
* getAngle: get the angle from the OBL cases and angle offsets
|
|
1661
|
+
*
|
|
1662
|
+
* @param {string} u new naming U layer OBL
|
|
1663
|
+
* @param {string} d new naming D layer OBL
|
|
1664
|
+
* @param {number} au U layer angle offset
|
|
1665
|
+
* @param {number} ad D layer angle offset
|
|
1666
|
+
* @returns {string} the angle inside a <>
|
|
1667
|
+
* @example "left pair", "right arrow", "1", "2" → "DL DL"
|
|
1668
|
+
*/
|
|
1669
|
+
getAngle(u, d, au, ad) {
|
|
1670
|
+
let uAngle = SquanLib.OBL_ANGLES[u];
|
|
1671
|
+
let dAngle = SquanLib.OBL_ANGLES[d];
|
|
1672
|
+
for (let i = 0; i < au % 4; i++) uAngle = SquanLib.nextAngle[uAngle];
|
|
1673
|
+
for (let i = 0; i < ad % 4; i++) dAngle = SquanLib.nextAngle[dAngle];
|
|
1674
|
+
return `${uAngle} ${dAngle}`;
|
|
1675
|
+
}
|
|
1676
|
+
|
|
1677
|
+
/**
|
|
1678
|
+
* cubeToSpe: convert an OBL cube state to the OBL of the layers and their
|
|
1679
|
+
* angle offset
|
|
1680
|
+
*
|
|
1681
|
+
* @param {string} state the CSP-style OBL cube state
|
|
1682
|
+
* @returns {[{obl: string, angleOffset: number}, {obl: string, angleOffset: number}]}
|
|
1683
|
+
* the result
|
|
1684
|
+
*/
|
|
1685
|
+
cubeToSpe(state) {
|
|
1686
|
+
return [
|
|
1687
|
+
this.layerToOBL(state.slice(0, SquanLib.LAYERL)),
|
|
1688
|
+
this.layerToOBL(state.slice(SquanLib.LAYERL))
|
|
1689
|
+
];
|
|
1690
|
+
}
|
|
1691
|
+
|
|
1692
|
+
/**
|
|
1693
|
+
* getOBLLen: get the optimal slicecount for the OBL
|
|
1694
|
+
*
|
|
1695
|
+
* @param {string} o nonspe OBL
|
|
1696
|
+
* @returns {number} optimal slicecount for it
|
|
1697
|
+
* @example "good bunny/thumb" → 4
|
|
1698
|
+
*/
|
|
1699
|
+
getOBLLen(o) {
|
|
1700
|
+
if (o in OBL_LEN) return OBL_LEN[o];
|
|
1701
|
+
return OBL_LEN[layer_flip_name(o)];
|
|
1702
|
+
}
|
|
1703
|
+
|
|
1704
|
+
/**
|
|
1705
|
+
* stateToLen: get the optimal slicecount for this CSP-style OBL cube state
|
|
1706
|
+
*
|
|
1707
|
+
* @param {string} u U layer CSP-style OBL state
|
|
1708
|
+
* @param {string} d D layer CSP-style OBL state
|
|
1709
|
+
* @returns {number} the optimal slicecount for the OBL
|
|
1710
|
+
*/
|
|
1711
|
+
stateToLen(u, d) {
|
|
1712
|
+
return this.getOBLLen(this.speToNonSpe(u + '/' + d));
|
|
1713
|
+
}
|
|
1714
|
+
|
|
1715
|
+
/**
|
|
1716
|
+
* getOBLNaming: get the new naming from the old naming
|
|
1717
|
+
*
|
|
1718
|
+
* @param {string} u the U layer old naming
|
|
1719
|
+
* @param {string} d the D layer old naming
|
|
1720
|
+
* @returns {string} the matt naming for the case, slash separated
|
|
1721
|
+
* @example "left bunny", "right thumb" → "Uc/Thc"
|
|
1722
|
+
*/
|
|
1723
|
+
getOBLNaming(u, d) {
|
|
1724
|
+
return SquanLib.NAMING[u] + '/' + SquanLib.NAMING[d];
|
|
1725
|
+
}
|
|
1726
|
+
|
|
1727
|
+
|
|
1728
|
+
/**
|
|
1729
|
+
* stateToMatt: convert an CSP-style OBL cube state to matt tracing memo
|
|
1730
|
+
*
|
|
1731
|
+
* @param {string} s CSP-style OBL cube state
|
|
1732
|
+
* @returns {string} matt tracing memo for the state
|
|
1733
|
+
*/
|
|
1734
|
+
stateToMatt(s) {
|
|
1735
|
+
let u = s.slice(0, SquanLib.LAYERL), d = s.slice(SquanLib.LAYERL);
|
|
1736
|
+
u = (u[0] !== u[0].toLowerCase()) ? this.shift(u, 3) : this.shift(u, 2);
|
|
1737
|
+
d = (d[0] !== d[0].toLowerCase()) ? this.shift(d, 3) : this.shift(d, 2);
|
|
1738
|
+
let mem = '';
|
|
1739
|
+
let p = 1;
|
|
1740
|
+
for (let x = 0; x < SquanLib.LAYERL; x += 3) {
|
|
1741
|
+
// do one "pair" at one time
|
|
1742
|
+
if (u[x] === 'B') mem += p;
|
|
1743
|
+
if (u[x + 2] === 'b') mem += (p + 1);
|
|
1744
|
+
p += 2;
|
|
1745
|
+
}
|
|
1746
|
+
// if it's just the solved state
|
|
1747
|
+
mem = (mem === '') ? '- ' : mem + ' ';
|
|
1748
|
+
p = 1;
|
|
1749
|
+
for (let x = 0; x < SquanLib.LAYERL; x += 3) {
|
|
1750
|
+
if (d[x] === 'B') mem += p;
|
|
1751
|
+
if (d[x + 2] === 'b') mem += (p + 1);
|
|
1752
|
+
p += 2;
|
|
1753
|
+
}
|
|
1754
|
+
return (mem[mem.length - 1] === ' ') ? mem + '-' : mem;
|
|
1755
|
+
}
|
|
1756
|
+
|
|
1757
|
+
/**
|
|
1758
|
+
* mattToLayer: converts one layer matt tracing memo to OBL layer state
|
|
1759
|
+
*
|
|
1760
|
+
* @param {string} m matt tracing memo, one layer
|
|
1761
|
+
* @returns {string} CSP-style OBL layer
|
|
1762
|
+
*/
|
|
1763
|
+
mattToLayer(m) {
|
|
1764
|
+
const bw = ['W', 'W', 'w', 'W', 'W', 'w', 'W', 'W', 'w', 'W', 'W', 'w'];
|
|
1765
|
+
for (const ch of m) {
|
|
1766
|
+
const num = parseInt(ch, 10);
|
|
1767
|
+
if (num % 2 !== 0) {
|
|
1768
|
+
// corner
|
|
1769
|
+
bw[Math.floor(num / 2) * 3] = 'B';
|
|
1770
|
+
bw[Math.floor(num / 2) * 3 + 1] = 'B';
|
|
1771
|
+
} else {
|
|
1772
|
+
// edge
|
|
1773
|
+
bw[Math.floor(num / 2) * 3 - 1] = 'b';
|
|
1774
|
+
}
|
|
1775
|
+
}
|
|
1776
|
+
return bw.join('');
|
|
1777
|
+
}
|
|
1778
|
+
|
|
1779
|
+
/**
|
|
1780
|
+
* mattToNonSpe: converts a full matt tracing memo to non specific OBL name
|
|
1781
|
+
*
|
|
1782
|
+
* @param {string} m a full matt tracing memo
|
|
1783
|
+
* @returns {string} a 2-layer nonspecific OBL name
|
|
1784
|
+
*/
|
|
1785
|
+
mattToNonSpe(m) {
|
|
1786
|
+
const [u, d] = m.split(' ');
|
|
1787
|
+
return this.speToNonSpe(
|
|
1788
|
+
`${this.layerToOBL(this.mattToLayer(u))}/${this.layerToOBL(this.mattToLayer(d))}`
|
|
1789
|
+
);
|
|
1790
|
+
}
|
|
1791
|
+
|
|
1792
|
+
/**
|
|
1793
|
+
* sortOblp: sort one layer of matt tracing OBLP memo
|
|
1794
|
+
*
|
|
1795
|
+
* @param {string} seq one layer of matt tracing OBLP memo
|
|
1796
|
+
* @returns {string} the sorted memo
|
|
1797
|
+
*/
|
|
1798
|
+
sortOblp(seq) {
|
|
1799
|
+
return [...seq].sort().join('');
|
|
1800
|
+
}
|
|
1801
|
+
}
|
|
1802
|
+
|
|
1803
|
+
return SquanLib;
|
|
1804
|
+
|
|
1805
|
+
}));
|