Code for connecting to a bluetooth smart cube
The only external dependency is lz-string
Adapted from cstimer by Shuang Chen (cs0x7f) https://github.com/cs0x7f/cstimer Copyright (C) 2023 Shuang Chen Licensed under the GPLv3
Code for connecting to a bluetooth smart cube
The only external dependency is lz-string
Adapted from cstimer by Shuang Chen (cs0x7f) https://github.com/cs0x7f/cstimer Copyright (C) 2023 Shuang Chen Licensed under the GPLv3
// @ts-nocheck | |
/* | |
AES-128 Encryption/Decryption | |
This file is adapted from cstimer by Shuang Chen (cs0x7f) | |
https://github.com/cs0x7f/cstimer | |
Copyright (C) 2023 Shuang Chen | |
Licensed under the GPLv3 | |
*/ | |
function createAES128(key) { | |
var Sbox = [ | |
99, 124, 119, 123, 242, 107, 111, 197, 48, 1, 103, 43, 254, 215, 171, | |
118, 202, 130, 201, 125, 250, 89, 71, 240, 173, 212, 162, 175, 156, | |
164, 114, 192, 183, 253, 147, 38, 54, 63, 247, 204, 52, 165, 229, 241, | |
113, 216, 49, 21, 4, 199, 35, 195, 24, 150, 5, 154, 7, 18, 128, 226, | |
235, 39, 178, 117, 9, 131, 44, 26, 27, 110, 90, 160, 82, 59, 214, 179, | |
41, 227, 47, 132, 83, 209, 0, 237, 32, 252, 177, 91, 106, 203, 190, | |
57, 74, 76, 88, 207, 208, 239, 170, 251, 67, 77, 51, 133, 69, 249, 2, | |
127, 80, 60, 159, 168, 81, 163, 64, 143, 146, 157, 56, 245, 188, 182, | |
218, 33, 16, 255, 243, 210, 205, 12, 19, 236, 95, 151, 68, 23, 196, | |
167, 126, 61, 100, 93, 25, 115, 96, 129, 79, 220, 34, 42, 144, 136, | |
70, 238, 184, 20, 222, 94, 11, 219, 224, 50, 58, 10, 73, 6, 36, 92, | |
194, 211, 172, 98, 145, 149, 228, 121, 231, 200, 55, 109, 141, 213, | |
78, 169, 108, 86, 244, 234, 101, 122, 174, 8, 186, 120, 37, 46, 28, | |
166, 180, 198, 232, 221, 116, 31, 75, 189, 139, 138, 112, 62, 181, | |
102, 72, 3, 246, 14, 97, 53, 87, 185, 134, 193, 29, 158, 225, 248, | |
152, 17, 105, 217, 142, 148, 155, 30, 135, 233, 206, 85, 40, 223, 140, | |
161, 137, 13, 191, 230, 66, 104, 65, 153, 45, 15, 176, 84, 187, 22, | |
]; | |
var SboxI = []; | |
var ShiftTabI = [0, 13, 10, 7, 4, 1, 14, 11, 8, 5, 2, 15, 12, 9, 6, 3]; | |
var xtime = []; | |
function addRoundKey(state, rkey) { | |
for (var i = 0; i < 16; i++) { | |
state[i] ^= rkey[i]; | |
} | |
} | |
function shiftSubAdd(state, rkey) { | |
var state0 = state.slice(); | |
for (var i = 0; i < 16; i++) { | |
state[i] = SboxI[state0[ShiftTabI[i]]] ^ rkey[i]; | |
} | |
} | |
function shiftSubAddI(state, rkey) { | |
var state0 = state.slice(); | |
for (var i = 0; i < 16; i++) { | |
state[ShiftTabI[i]] = Sbox[state0[i] ^ rkey[i]]; | |
} | |
} | |
function mixColumns(state) { | |
for (var i = 12; i >= 0; i -= 4) { | |
var s0 = state[i + 0]; | |
var s1 = state[i + 1]; | |
var s2 = state[i + 2]; | |
var s3 = state[i + 3]; | |
var h = s0 ^ s1 ^ s2 ^ s3; | |
state[i + 0] ^= h ^ xtime[s0 ^ s1]; | |
state[i + 1] ^= h ^ xtime[s1 ^ s2]; | |
state[i + 2] ^= h ^ xtime[s2 ^ s3]; | |
state[i + 3] ^= h ^ xtime[s3 ^ s0]; | |
} | |
} | |
function mixColumnsInv(state) { | |
for (var i = 0; i < 16; i += 4) { | |
var s0 = state[i + 0]; | |
var s1 = state[i + 1]; | |
var s2 = state[i + 2]; | |
var s3 = state[i + 3]; | |
var h = s0 ^ s1 ^ s2 ^ s3; | |
var xh = xtime[h]; | |
var h1 = xtime[xtime[xh ^ s0 ^ s2]] ^ h; | |
var h2 = xtime[xtime[xh ^ s1 ^ s3]] ^ h; | |
state[i + 0] ^= h1 ^ xtime[s0 ^ s1]; | |
state[i + 1] ^= h2 ^ xtime[s1 ^ s2]; | |
state[i + 2] ^= h1 ^ xtime[s2 ^ s3]; | |
state[i + 3] ^= h2 ^ xtime[s3 ^ s0]; | |
} | |
} | |
function init() { | |
if (xtime.length != 0) { | |
return; | |
} | |
for (var i = 0; i < 256; i++) { | |
SboxI[Sbox[i]] = i; | |
} | |
for (var i = 0; i < 128; i++) { | |
xtime[i] = i << 1; | |
xtime[128 + i] = (i << 1) ^ 0x1b; | |
} | |
} | |
class AES128 { | |
constructor (key) { | |
init(); | |
var exKey = key.slice(); | |
var Rcon = 1; | |
for (var i = 16; i < 176; i += 4) { | |
var tmp = exKey.slice(i - 4, i); | |
if (i % 16 == 0) { | |
tmp = [ | |
Sbox[tmp[1]] ^ Rcon, | |
Sbox[tmp[2]], | |
Sbox[tmp[3]], | |
Sbox[tmp[0]], | |
]; | |
Rcon = xtime[Rcon]; | |
} | |
for (var j = 0; j < 4; j++) { | |
exKey[i + j] = exKey[i + j - 16] ^ tmp[j]; | |
} | |
} | |
this.key = exKey; | |
} | |
decrypt(block) { | |
addRoundKey(block, this.key.slice(160, 176)); | |
for (var i = 144; i >= 16; i -= 16) { | |
shiftSubAdd(block, this.key.slice(i, i + 16)); | |
mixColumnsInv(block); | |
} | |
shiftSubAdd(block, this.key.slice(0, 16)); | |
return block; | |
} | |
encrypt(block) { | |
shiftSubAddI(block, this.key.slice(0, 16)); | |
for (var i = 16; i < 160; i += 16) { | |
mixColumns(block); | |
shiftSubAddI(block, this.key.slice(i, i + 16)); | |
} | |
addRoundKey(block, this.key.slice(160, 176)); | |
return block; | |
} | |
} | |
return new AES128(key); | |
} | |
export default function aes128(key) { | |
return createAES128(key); | |
} |
// @ts-nocheck | |
/* | |
Lets you connect to bluetooth cubes | |
This file is adapted from cstimer by Shuang Chen (cs0x7f) | |
https://github.com/cs0x7f/cstimer | |
Copyright (C) 2023 Shuang Chen | |
Licensed under the GPLv3 | |
*/ | |
import mathlib from './mathlib'; | |
import aes128 from './aes128'; | |
import LZString from 'lz-string'; | |
const DEBUG = false; | |
class BTState { | |
private _solved: any; | |
private _macMap: any = {}; | |
constructor() { | |
const macMap = localStorage.getItem('macMap'); | |
if (macMap) { | |
this._macMap = JSON.parse(macMap); | |
} | |
} | |
getSolved() { | |
if (!this._solved) { | |
this._solved = mathlib.SOLVED_FACELET; | |
} | |
return this._solved; | |
} | |
getMacMap() { | |
return this._macMap; | |
} | |
setMacMap(macMap) { | |
this._macMap = macMap; | |
localStorage.setItem('macMap', JSON.stringify(macMap)); | |
} | |
} | |
const btState = new BTState(); | |
export type BluetoothCube = { | |
init(): Promise<any>, | |
stop(): Promise<void>, | |
isConnected: () => boolean, | |
setCallback: (func: any) => void, | |
setEventCallback: (func: any) => void | |
getCube: () => undefined, | |
getDeviceName: () => string | undefined, | |
}; | |
export function createBluetooth(): BluetoothCube { | |
var cube = undefined; | |
var _device = null; | |
function matchUUID(uuid1, uuid2) { | |
return uuid1.toUpperCase() == uuid2.toUpperCase(); | |
} | |
var GiikerCube = (function() { | |
var _gatt = null; | |
var _chrct = null; | |
var UUID_SUFFIX = '-0000-1000-8000-00805f9b34fb'; | |
var SERVICE_UUID_DATA = '0000aadb' + UUID_SUFFIX; | |
var CHRCT_UUID_DATA = '0000aadc' + UUID_SUFFIX; | |
var SERVICE_UUID_RW = '0000aaaa' + UUID_SUFFIX; | |
var CHRCT_UUID_READ = '0000aaab' + UUID_SUFFIX; | |
var CHRCT_UUID_WRITE = '0000aaac' + UUID_SUFFIX; | |
var deviceName; | |
function init(device) { | |
clear(); | |
deviceName = device.name.startsWith('Gi') ? 'Giiker' : 'Mi Smart'; | |
return device.gatt.connect().then(function(gatt) { | |
_gatt = gatt; | |
return gatt.getPrimaryService(SERVICE_UUID_DATA); | |
}).then(function(service) { | |
return service.getCharacteristic(CHRCT_UUID_DATA); | |
}).then(function(chrct) { | |
_chrct = chrct; | |
return _chrct.startNotifications(); | |
}).then(function() { | |
return _chrct.readValue(); | |
}).then(function(value) { | |
var initState = parseState(value); | |
if (initState[0] != btState.getSolved()) { | |
// var rst = btState.rst; | |
// if (rst == 'a' || rst == 'p' && confirm(CONFIRM_GIIRST)) { | |
// giikerutil.markSolved(); | |
// } | |
// TODO: Add a way to mark the cube as solved if it goes out of sync | |
} | |
return _chrct.addEventListener('characteristicvaluechanged', onStateChanged); | |
}); | |
} | |
function onStateChanged(event) { | |
var value = event.target.value; | |
parseState(value); | |
} | |
var cFacelet = [ | |
[26, 15, 29], | |
[20, 8, 9], | |
[18, 38, 6], | |
[24, 27, 44], | |
[51, 35, 17], | |
[45, 11, 2], | |
[47, 0, 36], | |
[53, 42, 33] | |
]; | |
var eFacelet = [ | |
[25, 28], | |
[23, 12], | |
[19, 7], | |
[21, 41], | |
[32, 16], | |
[5, 10], | |
[3, 37], | |
[30, 43], | |
[52, 34], | |
[48, 14], | |
[46, 1], | |
[50, 39] | |
]; | |
function toHexVal(value) { | |
var raw = []; | |
for (var i = 0; i < 20; i++) { | |
raw.push(value.getUint8(i)); | |
} | |
if (raw[18] == 0xa7) { // decrypt | |
var key = [176, 81, 104, 224, 86, 137, 237, 119, 38, 26, 193, 161, 210, 126, 150, 81, 93, 13, 236, 249, 89, 235, 88, 24, 113, 81, 214, 131, 130, 199, 2, 169, 39, 165, 171, 41]; | |
var k1 = raw[19] >> 4 & 0xf; | |
var k2 = raw[19] & 0xf; | |
for (var i = 0; i < 18; i++) { | |
raw[i] += key[i + k1] + key[i + k2]; | |
} | |
raw = raw.slice(0, 18); | |
} | |
var valhex = []; | |
for (var i = 0; i < raw.length; i++) { | |
valhex.push(raw[i] >> 4 & 0xf); | |
valhex.push(raw[i] & 0xf); | |
} | |
return valhex; | |
} | |
function parseState(value) { | |
var locTime = (new Date()).getTime(); | |
var valhex = toHexVal(value); | |
var eo = []; | |
for (var i = 0; i < 3; i++) { | |
for (var mask = 8; mask != 0; mask >>= 1) { | |
eo.push((valhex[i + 28] & mask) ? 1 : 0); | |
} | |
} | |
var cc = new mathlib.CubieCube(); | |
var coMask = [-1, 1, -1, 1, 1, -1, 1, -1]; | |
for (var i = 0; i < 8; i++) { | |
cc.ca[i] = (valhex[i] - 1) | ((3 + valhex[i + 8] * coMask[i]) % 3) << 3; | |
} | |
for (var i = 0; i < 12; i++) { | |
cc.ea[i] = (valhex[i + 16] - 1) << 1 | eo[i]; | |
} | |
var facelet = cc.toFaceCube(cFacelet, eFacelet); | |
var moves = valhex.slice(32, 40); | |
var prevMoves = []; | |
for (var i = 0; i < moves.length; i += 2) { | |
prevMoves.push("BDLURF".charAt(moves[i] - 1) + " 2'".charAt((moves[i + 1] - 1) % 7)); | |
} | |
if (DEBUG) { | |
var hexstr = []; | |
for (var i = 0; i < 40; i++) { | |
hexstr.push("0123456789abcdef".charAt(valhex[i])); | |
} | |
console.log('[giiker]', "Raw Data: ", valhex.join("")); | |
console.log('[giiker]', "Current State: ", facelet); | |
console.log('[giiker]', "A Valid Generator: ", scramble_333.genFacelet(facelet)); | |
console.log('[giiker]', "Previous Moves: ", prevMoves.reverse().join(" ")); | |
prevMoves.reverse(); | |
} | |
callback(facelet, prevMoves, [locTime, locTime], deviceName); | |
return [facelet, prevMoves]; | |
} | |
function getBatteryLevel() { | |
if (!_gatt) { | |
return Promise.reject("Bluetooth Cube is not connected"); | |
} | |
var _service; | |
var _read; | |
var _resolve; | |
var listener = function(event) { | |
_resolve([event.target.value.getUint8(1), deviceName]); | |
_read.removeEventListener('characteristicvaluechanged', listener); | |
_read.stopNotifications(); | |
}; | |
return _gatt.getPrimaryService(SERVICE_UUID_RW).then(function(service) { | |
_service = service; | |
return service.getCharacteristic(CHRCT_UUID_READ); | |
}).then(function(chrct) { | |
_read = chrct; | |
return _read.startNotifications(); | |
}).then(function() { | |
return _read.addEventListener('characteristicvaluechanged', listener); | |
}).then(function() { | |
return _service.getCharacteristic(CHRCT_UUID_WRITE); | |
}).then(function(chrct) { | |
chrct.writeValue(new Uint8Array([0xb5]).buffer); | |
return new Promise(function(resolve) { | |
_resolve = resolve; | |
}); | |
}); | |
} | |
function clear() { | |
var result = Promise.resolve(); | |
if (_chrct) { | |
_chrct.removeEventListener('characteristicvaluechanged', onStateChanged); | |
result = _chrct.stopNotifications().catch(()=>undefined); | |
_chrct = null; | |
} | |
_gatt = null; | |
deviceName = null; | |
return result; | |
} | |
return { | |
init: init, | |
opservs: [SERVICE_UUID_DATA, SERVICE_UUID_RW], | |
getBatteryLevel: getBatteryLevel, | |
getName: () => deviceName, | |
clear: clear | |
} | |
})(); | |
var GanCube = (function() { | |
var _gatt; | |
var _service_data; | |
var _service_meta; | |
var _chrct_f2; | |
var _chrct_f5; | |
var _chrct_f6; | |
var _chrct_f7; | |
var UUID_SUFFIX = '-0000-1000-8000-00805f9b34fb'; | |
var SERVICE_UUID_META = '0000180a' + UUID_SUFFIX; | |
var CHRCT_UUID_VERSION = '00002a28' + UUID_SUFFIX; | |
var CHRCT_UUID_HARDWARE = '00002a23' + UUID_SUFFIX; | |
var SERVICE_UUID_DATA = '0000fff0' + UUID_SUFFIX; | |
var CHRCT_UUID_F2 = '0000fff2' + UUID_SUFFIX; // cube state, (54 - 6) facelets, 3 bit per facelet | |
var CHRCT_UUID_F3 = '0000fff3' + UUID_SUFFIX; // prev moves | |
var CHRCT_UUID_F5 = '0000fff5' + UUID_SUFFIX; // gyro state, move counter, pre moves | |
var CHRCT_UUID_F6 = '0000fff6' + UUID_SUFFIX; // move counter, time offsets between premoves | |
var CHRCT_UUID_F7 = '0000fff7' + UUID_SUFFIX; | |
var _service_v2data; | |
var _chrct_v2read; | |
var _chrct_v2write; | |
var SERVICE_UUID_V2DATA = '6e400001-b5a3-f393-e0a9-e50e24dc4179'; | |
var CHRCT_UUID_V2READ = '28be4cb6-cd67-11e9-a32f-2a2ae2dbcce4'; | |
var CHRCT_UUID_V2WRITE = '28be4a4a-cd67-11e9-a32f-2a2ae2dbcce4'; | |
var GAN_CIC_LIST = [0x0001, 0x0501]; // List of Company Identifier Codes seen for GAN cubes | |
var decoder = null; | |
var deviceName = null; | |
var deviceMac = null; | |
var KEYS = [ | |
"NoRgnAHANATADDWJYwMxQOxiiEcfYgSK6Hpr4TYCs0IG1OEAbDszALpA", | |
"NoNg7ANATFIQnARmogLBRUCs0oAYN8U5J45EQBmFADg0oJAOSlUQF0g", | |
"NoRgNATGBs1gLABgQTjCeBWSUDsYBmKbCeMADjNnXxHIoIF0g", | |
"NoRg7ANAzBCsAMEAsioxBEIAc0Cc0ATJkgSIYhXIjhMQGxgC6QA", | |
"NoVgNAjAHGBMYDYCcdJgCwTFBkYVgAY9JpJYUsYBmAXSA", | |
"NoRgNAbAHGAsAMkwgMyzClH0LFcArHnAJzIqIBMGWEAukA" | |
]; | |
function getKey(version, value) { | |
var key = KEYS[version >> 8 & 0xff]; | |
if (!key) { | |
return; | |
} | |
key = JSON.parse(LZString.decompressFromEncodedURIComponent(key)); | |
for (var i = 0; i < 6; i++) { | |
key[i] = (key[i] + value.getUint8(5 - i)) & 0xff; | |
} | |
return key; | |
} | |
function getKeyV2(value, ver) { | |
ver = ver || 0; | |
var key = JSON.parse(LZString.decompressFromEncodedURIComponent(KEYS[2 + ver * 2])); | |
var iv = JSON.parse(LZString.decompressFromEncodedURIComponent(KEYS[3 + ver * 2])); | |
for (var i = 0; i < 6; i++) { | |
key[i] = (key[i] + value[5 - i]) % 255; | |
iv[i] = (iv[i] + value[5 - i]) % 255; | |
} | |
return [key, iv]; | |
} | |
function decode(value) { | |
var ret = []; | |
for (var i = 0; i < value.byteLength; i++) { | |
ret[i] = value.getUint8(i); | |
} | |
if (decoder == null) { | |
return ret; | |
} | |
var iv = decoder.iv || []; | |
if (ret.length > 16) { | |
var offset = ret.length - 16; | |
var block = decoder.decrypt(ret.slice(offset)); | |
for (var i = 0; i < 16; i++) { | |
ret[i + offset] = block[i] ^ (~~iv[i]); | |
} | |
} | |
decoder.decrypt(ret); | |
for (var i = 0; i < 16; i++) { | |
ret[i] ^= (~~iv[i]); | |
} | |
return ret; | |
} | |
function encode(ret) { | |
if (decoder == null) { | |
return ret; | |
} | |
var iv = decoder.iv || []; | |
for (var i = 0; i < 16; i++) { | |
ret[i] ^= ~~iv[i]; | |
} | |
decoder.encrypt(ret); | |
if (ret.length > 16) { | |
var offset = ret.length - 16; | |
var block = ret.slice(offset); | |
for (var i = 0; i < 16; i++) { | |
block[i] ^= ~~iv[i]; | |
} | |
decoder.encrypt(block); | |
for (var i = 0; i < 16; i++) { | |
ret[i + offset] = block[i]; | |
} | |
} | |
return ret; | |
} | |
function v1init() { | |
DEBUG && console.log('[gancube] v1init start'); | |
return _service_meta.getCharacteristic(CHRCT_UUID_VERSION).then(function(chrct) { | |
return chrct.readValue(); | |
}).then(function(value) { | |
var version = value.getUint8(0) << 16 | value.getUint8(1) << 8 | value.getUint8(2); | |
DEBUG && console.log('[gancube] version', version.toString(16)); | |
decoder = null; | |
if (version > 0x010007 && (version & 0xfffe00) == 0x010000) { | |
return _service_meta.getCharacteristic(CHRCT_UUID_HARDWARE).then(function(chrct) { | |
return chrct.readValue(); | |
}).then(function(value) { | |
var key = getKey(version, value); | |
if (!key) { | |
logohint.push('Not support your Gan cube'); | |
return; | |
} | |
DEBUG && console.log('[gancube] key', JSON.stringify(key)); | |
decoder = aes128(key); | |
}); | |
} else { //not support | |
logohint.push('Not support your Gan cube'); | |
} | |
}).then(function() { | |
return _service_data.getCharacteristics(); | |
}).then(function(chrcts) { | |
for (var i = 0; i < chrcts.length; i++) { | |
var chrct = chrcts[i] | |
DEBUG && console.log('[gancube] v1init find chrct', chrct.uuid); | |
if (matchUUID(chrct.uuid, CHRCT_UUID_F2)) { | |
_chrct_f2 = chrct; | |
} else if (matchUUID(chrct.uuid, CHRCT_UUID_F5)) { | |
_chrct_f5 = chrct; | |
} else if (matchUUID(chrct.uuid, CHRCT_UUID_F6)) { | |
_chrct_f6 = chrct; | |
} else if (matchUUID(chrct.uuid, CHRCT_UUID_F7)) { | |
_chrct_f7 = chrct; | |
} | |
} | |
}).then(loopRead); | |
} | |
function getManufacturerDataBytes(mfData) { | |
if (mfData instanceof DataView) { // this is workaround for Bluefy browser | |
return mfData; | |
} | |
for (var id of GAN_CIC_LIST) { | |
if (mfData.has(id)) { | |
DEBUG && console.log('[gancube] found Manufacturer Data under CIC = 0x' + id.toString(16).padStart(4, '0')); | |
return mfData.get(id); | |
} | |
} | |
DEBUG && console.log('[gancube] Looks like this cube has new unknown CIC'); | |
} | |
function waitForAdvs() { | |
if (!_device || !_device.watchAdvertisements) { | |
return Promise.reject(-1); | |
} | |
var abortController = new AbortController(); | |
return new Promise(function(resolve, reject) { | |
var onAdvEvent = function(event) { | |
DEBUG && console.log('[gancube] receive adv event', event); | |
var mfData = event.manufacturerData; | |
var dataView = getManufacturerDataBytes(mfData); | |
if (dataView && dataView.byteLength >= 6) { | |
var mac = []; | |
for (var i = 0; i < 6; i++) { | |
mac.push((dataView.getUint8(dataView.byteLength - i - 1) + 0x100).toString(16).slice(1)); | |
} | |
_device && _device.removeEventListener('advertisementreceived', onAdvEvent); | |
abortController.abort(); | |
resolve(mac.join(':')); | |
} | |
}; | |
_device.addEventListener('advertisementreceived', onAdvEvent); | |
_device.watchAdvertisements({ signal: abortController.signal }); | |
setTimeout(function() { // reject if no mac found | |
_device && _device.removeEventListener('advertisementreceived', onAdvEvent); | |
abortController.abort(); | |
reject(-2); | |
}, 5000); | |
}); | |
} | |
function v2initKey(forcePrompt, isWrongKey, ver) { | |
var savedMacMap = btState.getMacMap(); | |
var mac = savedMacMap[deviceName]; | |
if (!mac || forcePrompt) { | |
mac = prompt((isWrongKey ? 'The key provided might be wrong. ' : '') + 'MAC address (xx:xx:xx:xx:xx:xx) of your cube, can be found in CubeStation or about://bluetooth-internals/#devices', mac || 'xx:xx:xx:xx:xx:xx'); | |
} | |
var m = /^([0-9a-f]{2}[:-]){5}[0-9a-f]{2}$/i.exec(mac); | |
if (!m) { | |
logohint.push('Not a valid mac address, cannot connect to your Gan cube'); | |
decoder = null; | |
return; | |
} | |
if (mac != savedMacMap[deviceName]) { | |
savedMacMap[deviceName] = mac; | |
// kernel.setProp('giiMacMap', JSON.stringify(savedMacMap)); | |
btState.setMacMap(savedMacMap); | |
} | |
v2initDecoder(mac, ver); | |
} | |
function v2initDecoder(mac, ver) { | |
var value = []; | |
for (var i = 0; i < 6; i++) { | |
value.push(parseInt(mac.slice(i * 3, i * 3 + 2), 16)); | |
} | |
var keyiv = getKeyV2(value, ver); | |
DEBUG && console.log('[gancube] ver=', ver, ' key=', JSON.stringify(keyiv)); | |
decoder = aes128(keyiv[0]); | |
decoder.iv = keyiv[1]; | |
} | |
function v2sendRequest(req) { | |
if (!_chrct_v2write) { | |
DEBUG && console.log('[gancube] v2sendRequest cannot find v2write chrct'); | |
return; | |
} | |
var encodedReq = encode(req.slice()); | |
DEBUG && console.log('[gancube] v2sendRequest', req, encodedReq); | |
return _chrct_v2write.writeValue(new Uint8Array(encodedReq).buffer); | |
} | |
function v2sendSimpleRequest(opcode) { | |
var req = mathlib.valuedArray(20, 0); | |
req[0] = opcode; | |
return v2sendRequest(req); | |
} | |
function v2requestFacelets() { | |
return v2sendSimpleRequest(4); | |
} | |
function v2requestBattery() { | |
return v2sendSimpleRequest(9); | |
} | |
function v2requestHardwareInfo() { | |
return v2sendSimpleRequest(5); | |
} | |
function v2requestReset() { | |
return v2sendRequest([10, 5, 57, 119, 0, 0, 1, 35, 69, 103, 137, 171, 0, 0, 0, 0, 0, 0, 0, 0]); | |
} | |
function v2init(ver) { | |
DEBUG && console.log('[gancube] v2init start'); | |
keyCheck = 0; | |
if (deviceMac) { | |
// var savedMacMap = JSON.parse(kernel.getProp('giiMacMap', '{}')); | |
var savedMacMap = btState.getMacMap(); | |
var prevMac = savedMacMap[deviceName]; | |
if (prevMac && prevMac.toUpperCase() == deviceMac.toUpperCase()) { | |
DEBUG && console.log('[gancube] v2init mac matched'); | |
} else { | |
DEBUG && console.log('[gancube] v2init mac updated'); | |
savedMacMap[deviceName] = deviceMac; | |
// kernel.setProp('giiMacMap', JSON.stringify(savedMacMap)); | |
btState.setMacMap(savedMacMap); | |
} | |
v2initDecoder(deviceMac, ver); | |
} else { | |
v2initKey(false, false, ver); | |
} | |
return _service_v2data.getCharacteristics().then(function(chrcts) { | |
DEBUG && console.log('[gancube] v2init find chrcts', chrcts); | |
for (var i = 0; i < chrcts.length; i++) { | |
var chrct = chrcts[i] | |
DEBUG && console.log('[gancube] v2init find chrct', chrct.uuid); | |
if (matchUUID(chrct.uuid, CHRCT_UUID_V2READ)) { | |
_chrct_v2read = chrct; | |
} else if (matchUUID(chrct.uuid, CHRCT_UUID_V2WRITE)) { | |
_chrct_v2write = chrct; | |
} | |
} | |
if (!_chrct_v2read) { | |
DEBUG && console.log('[gancube] v2init cannot find v2read chrct'); | |
} | |
}).then(function() { | |
DEBUG && console.log('[gancube] v2init v2read start notifications'); | |
return _chrct_v2read.startNotifications(); | |
}).then(function() { | |
DEBUG && console.log('[gancube] v2init v2read notification started'); | |
return _chrct_v2read.addEventListener('characteristicvaluechanged', onStateChangedV2); | |
}).then(function() { | |
return v2requestHardwareInfo(); | |
}).then(function() { | |
return v2requestFacelets(); | |
}).then(function() { | |
return v2requestBattery(); | |
}); | |
} | |
function init(device) { | |
clear(); | |
deviceName = device.name; | |
DEBUG && console.log('[gancube] init gan cube start'); | |
return waitForAdvs().then(function(mac) { | |
DEBUG && console.log('[gancube] init, found cube bluetooth hardware MAC = ' + mac); | |
deviceMac = mac; | |
}, function(err) { | |
DEBUG && console.log('[gancube] init, unable to automatically determine cube MAC, error code = ' + err); | |
}).then(function() { | |
return device.gatt.connect(); | |
}).then(function(gatt) { | |
_gatt = gatt; | |
return gatt.getPrimaryServices(); | |
}).then(function(services) { | |
for (var i = 0; i < services.length; i++) { | |
var service = services[i]; | |
DEBUG && console.log('[gancube] checkHardware find service', service.uuid); | |
if (matchUUID(service.uuid, SERVICE_UUID_META)) { | |
_service_meta = service; | |
} else if (matchUUID(service.uuid, SERVICE_UUID_DATA)) { | |
_service_data = service; | |
} else if (matchUUID(service.uuid, SERVICE_UUID_V2DATA)) { | |
_service_v2data = service; | |
} | |
} | |
if (_service_v2data) { | |
return v2init((deviceName || '').startsWith('AiCube') ? 1 : 0); | |
} | |
if (_service_data && _service_meta) { | |
return v1init(); | |
} | |
logohint.push('Not support your Gan cube'); | |
}); | |
} | |
var prevMoves = []; | |
var timeOffs = []; | |
var prevCubie = new mathlib.CubieCube(); | |
var curCubie = new mathlib.CubieCube(); | |
var latestFacelet = mathlib.SOLVED_FACELET; | |
var deviceTime = 0; | |
var deviceTimeOffset = 0; | |
var moveCnt = -1; | |
var prevMoveCnt = -1; | |
var movesFromLastCheck = 1000; | |
var batteryLevel = 100; | |
function initCubeState() { | |
var locTime = (new Date()).getTime(); | |
DEBUG && console.log('[gancube]', 'init cube state'); | |
callback(latestFacelet, prevMoves, [null, locTime], deviceName); | |
prevCubie.fromFacelet(latestFacelet); | |
prevMoveCnt = moveCnt; | |
if (latestFacelet != btState.getSolved()) { | |
// var rst = kernel.getProp('giiRST'); | |
// if (rst == 'a' || rst == 'p' && confirm(CONFIRM_GIIRST)) { | |
// giikerutil.markSolved(); | |
// } | |
//TODO: Add a way to mark the cube as solved if it goes out of sync | |
} | |
} | |
function checkState() { | |
if (movesFromLastCheck < 50) { | |
return Promise.resolve(false); | |
} | |
return _chrct_f2.readValue().then(function(value) { | |
value = decode(value); | |
var state = []; | |
for (var i = 0; i < value.length - 2; i += 3) { | |
var face = value[i ^ 1] << 16 | value[i + 1 ^ 1] << 8 | value[i + 2 ^ 1]; | |
for (var j = 21; j >= 0; j -= 3) { | |
state.push("URFDLB".charAt(face >> j & 0x7)); | |
if (j == 12) { | |
state.push("URFDLB".charAt(i / 3)); | |
} | |
} | |
} | |
latestFacelet = state.join(""); | |
movesFromLastCheck = 0; | |
if (prevMoveCnt == -1) { | |
initCubeState(); | |
return; | |
} | |
return Promise.resolve(true); | |
}); | |
} | |
function updateMoveTimes(locTime, isV2) { | |
var moveDiff = (moveCnt - prevMoveCnt) & 0xff; | |
DEBUG && moveDiff > 1 && console.log('[gancube]', 'bluetooth event was lost, moveDiff = ' + moveDiff); | |
prevMoveCnt = moveCnt; | |
movesFromLastCheck += moveDiff; | |
if (moveDiff > prevMoves.length) { | |
movesFromLastCheck = 50; | |
moveDiff = prevMoves.length; | |
} | |
var calcTs = deviceTime + deviceTimeOffset; | |
for (var i = moveDiff - 1; i >= 0; i--) { | |
calcTs += timeOffs[i]; | |
} | |
if (!deviceTime || Math.abs(locTime - calcTs) > 2000) { | |
DEBUG && console.log('[gancube]', 'time adjust', locTime - calcTs, '@', locTime); | |
deviceTime += locTime - calcTs; | |
} | |
for (var i = moveDiff - 1; i >= 0; i--) { | |
var m = "URFDLB".indexOf(prevMoves[i][0]) * 3 + " 2'".indexOf(prevMoves[i][1]); | |
mathlib.CubieCube.EdgeMult(prevCubie, mathlib.CubieCube.moveCube[m], curCubie); | |
mathlib.CubieCube.CornMult(prevCubie, mathlib.CubieCube.moveCube[m], curCubie); | |
deviceTime += timeOffs[i]; | |
callback(curCubie.toFaceCube(), prevMoves.slice(i), [deviceTime, i == 0 ? locTime : null], deviceName + (isV2 ? '*' : '')); | |
var tmp = curCubie; | |
curCubie = prevCubie; | |
prevCubie = tmp; | |
DEBUG && console.log('[gancube] move', prevMoves[i], timeOffs[i]); | |
} | |
deviceTimeOffset = locTime - deviceTime; | |
} | |
function loopRead() { | |
if (!_device) { | |
return; | |
} | |
return _chrct_f5.readValue().then(function(value) { | |
value = decode(value); | |
var locTime = (new Date()).getTime(); | |
moveCnt = value[12]; | |
if (moveCnt == prevMoveCnt) { | |
return; | |
} | |
prevMoves = []; | |
for (var i = 0; i < 6; i++) { | |
var m = value[13 + i]; | |
prevMoves.unshift("URFDLB".charAt(~~(m / 3)) + " 2'".charAt(m % 3)); | |
} | |
var f6val; | |
return _chrct_f6.readValue().then(function(value) { | |
value = decode(value); | |
f6val = value; | |
return checkState(); | |
}).then(function(isUpdated) { | |
if (isUpdated) { | |
return; | |
} | |
timeOffs = []; | |
for (var i = 0; i < 9; i++) { | |
var off = f6val[i * 2 + 1] | f6val[i * 2 + 2] << 8; | |
timeOffs.unshift(off); | |
} | |
updateMoveTimes(locTime, 0); | |
if (isUpdated && prevCubie.toFaceCube() != latestFacelet) { | |
DEBUG && console.log('[gancube]', 'Cube state check error'); | |
DEBUG && console.log('[gancube]', 'calc', prevCubie.toFaceCube()); | |
DEBUG && console.log('[gancube]', 'read', latestFacelet); | |
prevCubie.fromFacelet(latestFacelet); | |
} | |
}); | |
}).then(loopRead); | |
} | |
function getBatteryLevel() { | |
if (!_gatt) { | |
return Promise.reject("Bluetooth Cube is not connected"); | |
} | |
if (_service_v2data) { | |
return Promise.resolve([batteryLevel, deviceName + '*']); | |
} else if (_chrct_f7) { | |
return _chrct_f7.readValue().then(function(value) { | |
value = decode(value); | |
return Promise.resolve([value[7], deviceName]); | |
}); | |
} else { | |
return Promise.resolve([batteryLevel, deviceName]); | |
} | |
} | |
var keyCheck = 0; | |
function onStateChangedV2(event) { | |
var value = event.target.value; | |
if (decoder == null) { | |
return; | |
} | |
parseV2Data(value); | |
} | |
function parseV2Data(value) { | |
var locTime = (new Date()).getTime(); | |
// DEBUG && console.log('[gancube]', 'dec v2', value); | |
value = decode(value); | |
for (var i = 0; i < value.length; i++) { | |
value[i] = (value[i] + 256).toString(2).slice(1); | |
} | |
value = value.join(''); | |
var mode = parseInt(value.slice(0, 4), 2); | |
if (mode == 1) { // gyro | |
} else if (mode == 2) { // cube move | |
DEBUG && console.log('[gancube]', 'v2 received move event', value); | |
moveCnt = parseInt(value.slice(4, 12), 2); | |
if (moveCnt == prevMoveCnt) { | |
return; | |
} else if (prevMoveCnt == -1) { | |
prevMoveCnt = moveCnt; | |
return; | |
} | |
timeOffs = []; | |
prevMoves = []; | |
var keyChkInc = 0; | |
for (var i = 0; i < 7; i++) { | |
var m = parseInt(value.slice(12 + i * 5, 17 + i * 5), 2); | |
timeOffs[i] = parseInt(value.slice(47 + i * 16, 63 + i * 16), 2); | |
prevMoves[i] = "URFDLB".charAt(m >> 1) + " '".charAt(m & 1); | |
if (m >= 12) { // invalid data | |
prevMoves[i] = "U "; | |
keyChkInc = 1; | |
} | |
} | |
keyCheck += keyChkInc; | |
if (keyChkInc == 0) { | |
updateMoveTimes(locTime, 1); | |
} | |
} else if (mode == 4) { // cube state | |
DEBUG && console.log('[gancube]', 'v2 received facelets event', value); | |
moveCnt = parseInt(value.slice(4, 12), 2); | |
if (moveCnt != prevMoveCnt && prevMoveCnt != -1) { | |
return; | |
} | |
var cc = new mathlib.CubieCube(); | |
var echk = 0; | |
var cchk = 0xf00; | |
for (var i = 0; i < 7; i++) { | |
var perm = parseInt(value.slice(12 + i * 3, 15 + i * 3), 2); | |
var ori = parseInt(value.slice(33 + i * 2, 35 + i * 2), 2); | |
cchk -= ori << 3; | |
cchk ^= perm; | |
cc.ca[i] = ori << 3 | perm; | |
} | |
cc.ca[7] = (cchk & 0xff8) % 24 | cchk & 0x7; | |
for (var i = 0; i < 11; i++) { | |
var perm = parseInt(value.slice(47 + i * 4, 51 + i * 4), 2); | |
var ori = parseInt(value.slice(91 + i, 92 + i), 2); | |
echk ^= perm << 1 | ori; | |
cc.ea[i] = perm << 1 | ori; | |
} | |
cc.ea[11] = echk; | |
if (cc.verify() != 0) { | |
keyCheck++; | |
return; | |
} | |
latestFacelet = cc.toFaceCube(); | |
if (prevMoveCnt == -1) { | |
initCubeState(); | |
} else if (prevCubie.toFaceCube() != latestFacelet) { | |
DEBUG && console.log('[gancube]', 'Cube state check error'); | |
DEBUG && console.log('[gancube]', 'calc', prevCubie.toFaceCube()); | |
DEBUG && console.log('[gancube]', 'read', latestFacelet); | |
prevCubie.fromFacelet(latestFacelet); | |
callback(latestFacelet, prevMoves, [null, locTime], deviceName + '*'); | |
} | |
prevMoveCnt = moveCnt; | |
} else if (mode == 5) { // hardware info | |
DEBUG && console.log('[gancube]', 'v2 received hardware info event', value); | |
var hardwareVersion = parseInt(value.slice(8, 16), 2) + "." + parseInt(value.slice(16, 24), 2); | |
var softwareVersion = parseInt(value.slice(24, 32), 2) + "." + parseInt(value.slice(32, 40), 2); | |
var devName = ''; | |
for (var i = 0; i < 8; i++) | |
devName += String.fromCharCode(parseInt(value.slice(40 + i * 8, 48 + i * 8), 2)); | |
var gyroEnabled = 1 === parseInt(value.slice(104, 105), 2); | |
DEBUG && console.log('[gancube]', 'Hardware Version', hardwareVersion); | |
DEBUG && console.log('[gancube]', 'Software Version', softwareVersion); | |
DEBUG && console.log('[gancube]', 'Device Name', devName); | |
DEBUG && console.log('[gancube]', 'Gyro Enabled', gyroEnabled); | |
} else if (mode == 9) { // battery | |
DEBUG && console.log('[gancube]', 'v2 received battery event', value); | |
// batteryLevel = parseInt(value.slice(8, 16), 2); | |
// giikerutil.updateBattery([batteryLevel, deviceName + '*']); | |
// TODO: Add a way to surface battery level | |
} else { | |
DEBUG && console.log('[gancube]', 'v2 received unknown event', value); | |
} | |
} | |
function clear() { | |
var result = Promise.resolve(); | |
if (_chrct_v2read) { | |
_chrct_v2read.removeEventListener('characteristicvaluechanged', onStateChangedV2); | |
result = _chrct_v2read.stopNotifications().catch(()=>undefined); | |
_chrct_v2read = null; | |
} | |
_service_data = null; | |
_service_meta = null; | |
_service_v2data = null; | |
_gatt = null; | |
deviceName = null; | |
deviceMac = null; | |
prevMoves = []; | |
timeOffs = []; | |
prevCubie = new mathlib.CubieCube(); | |
curCubie = new mathlib.CubieCube(); | |
latestFacelet = mathlib.SOLVED_FACELET; | |
deviceTime = 0; | |
deviceTimeOffset = 0; | |
moveCnt = -1; | |
prevMoveCnt = -1; | |
movesFromLastCheck = 1000; | |
batteryLevel = 100; | |
return result; | |
} | |
return { | |
init: init, | |
opservs: [SERVICE_UUID_DATA, SERVICE_UUID_META, SERVICE_UUID_V2DATA], | |
cics: GAN_CIC_LIST, | |
getBatteryLevel: getBatteryLevel, | |
getName: () => deviceName, | |
clear: clear | |
}; | |
})(); | |
var GoCube = (function() { | |
var _gatt; | |
var _service; | |
var _read; | |
var _write; | |
var _deviceName; | |
var UUID_SUFFIX = '-b5a3-f393-e0a9-e50e24dcca9e'; | |
var SERVICE_UUID = '6e400001' + UUID_SUFFIX; | |
var CHRCT_UUID_WRITE = '6e400002' + UUID_SUFFIX; | |
var CHRCT_UUID_READ = '6e400003' + UUID_SUFFIX; | |
var WRITE_BATTERY = 50; | |
var WRITE_STATE = 51; | |
function init(device) { | |
clear(); | |
_deviceName = device.name.startsWith('GoCube') ? 'GoCube' : 'Rubiks Connected' | |
return device.gatt.connect().then(function(gatt) { | |
_gatt = gatt; | |
return gatt.getPrimaryService(SERVICE_UUID); | |
}).then(function(service) { | |
_service = service; | |
return _service.getCharacteristic(CHRCT_UUID_WRITE); | |
}).then(function(chrct) { | |
_write = chrct; | |
return _service.getCharacteristic(CHRCT_UUID_READ); | |
}).then(function(chrct) { | |
_read = chrct; | |
return _read.startNotifications(); | |
}).then(function() { | |
return _read.addEventListener('characteristicvaluechanged', onStateChanged); | |
}).then(function() { | |
return _write.writeValue(new Uint8Array([WRITE_STATE]).buffer); | |
}); | |
} | |
function onStateChanged(event) { | |
var value = event.target.value; | |
parseData(value); | |
} | |
function toHexVal(value) { | |
var valhex = []; | |
for (var i = 0; i < value.byteLength; i++) { | |
valhex.push(value.getUint8(i) >> 4 & 0xf); | |
valhex.push(value.getUint8(i) & 0xf); | |
} | |
return valhex; | |
} | |
var _batteryLevel; | |
var axisPerm = [5, 2, 0, 3, 1, 4]; | |
var facePerm = [0, 1, 2, 5, 8, 7, 6, 3]; | |
var faceOffset = [0, 0, 6, 2, 0, 0]; | |
var moveCntFree = 100; | |
var curFacelet = mathlib.SOLVED_FACELET; | |
var curCubie = new mathlib.CubieCube(); | |
var prevCubie = new mathlib.CubieCube(); | |
function parseData(value) { | |
var locTime = (new Date()).getTime(); | |
if (value.byteLength < 4) { | |
return; | |
} | |
if (value.getUint8(0) != 0x2a || | |
value.getUint8(value.byteLength - 2) != 0x0d || | |
value.getUint8(value.byteLength - 1) != 0x0a) { | |
return; | |
} | |
var msgType = value.getUint8(2); | |
var msgLen = value.byteLength - 6; | |
if (msgType == 1) { // move | |
// console.log(toHexVal(value)); | |
for (var i = 0; i < msgLen; i += 2) { | |
var axis = axisPerm[value.getUint8(3 + i) >> 1]; | |
var power = [0, 2][value.getUint8(3 + i) & 1]; | |
var m = axis * 3 + power; | |
DEBUG && console.log('move', "URFDLB".charAt(axis) + " 2'".charAt(power)); | |
mathlib.CubieCube.EdgeMult(prevCubie, mathlib.CubieCube.moveCube[m], curCubie); | |
mathlib.CubieCube.CornMult(prevCubie, mathlib.CubieCube.moveCube[m], curCubie); | |
curFacelet = curCubie.toFaceCube(); | |
callback(curFacelet, ["URFDLB".charAt(axis) + " 2'".charAt(power)], [locTime, locTime], _deviceName); | |
var tmp = curCubie; | |
curCubie = prevCubie; | |
prevCubie = tmp; | |
if (++moveCntFree > 20) { | |
moveCntFree = 0; | |
_write.writeValue(new Uint8Array([WRITE_STATE]).buffer); | |
} | |
} | |
} else if (msgType == 2) { // cube state | |
var facelet = []; | |
for (var a = 0; a < 6; a++) { | |
var axis = axisPerm[a] * 9; | |
var aoff = faceOffset[a]; | |
facelet[axis + 4] = "BFUDRL".charAt(value.getUint8(3 + a * 9)); | |
for (var i = 0; i < 8; i++) { | |
facelet[axis + facePerm[(i + aoff) % 8]] = "BFUDRL".charAt(value.getUint8(3 + a * 9 + i + 1)); | |
} | |
} | |
var newFacelet = facelet.join(''); | |
if (newFacelet != curFacelet) { | |
DEBUG && console.log('facelet', newFacelet); | |
curCubie.fromFacelet(newFacelet); | |
} | |
} else if (msgType == 3) { // quaternion | |
} else if (msgType == 5) { // battery level | |
_batteryLevel = value.getUint8(3); | |
DEBUG && console.log('battery level', _batteryLevel); | |
} else if (msgType == 7) { // offline stats | |
DEBUG && console.log('offline stats', toHexVal(value)); | |
} else if (msgType == 8) { // cube type | |
DEBUG && console.log('cube type', toHexVal(value)); | |
} | |
} | |
function getBatteryLevel() { | |
if (!_write) { | |
return Promise.reject("Bluetooth Cube is not connected"); | |
} | |
_write.writeValue(new Uint8Array([WRITE_BATTERY]).buffer); | |
return new Promise(function (resolve) { | |
setTimeout('getBatteryLevel', function () { | |
resolve([_batteryLevel, _deviceName]); | |
}, 1000); | |
}); | |
} | |
function clear() { | |
var result = Promise.resolve(); | |
if (_read) { | |
_read.removeEventListener('characteristicvaluechanged', onStateChanged); | |
result = _read.stopNotifications().catch(()=>undefined); | |
_read = null; | |
} | |
_write = null; | |
_service = null; | |
_gatt = null; | |
_deviceName = null; | |
return result; | |
} | |
return { | |
init: init, | |
opservs: [SERVICE_UUID], | |
getBatteryLevel: getBatteryLevel, | |
getName: () => _deviceName, | |
clear: clear | |
}; | |
})(); | |
var MoyuCube = (function() { | |
var _gatt; | |
var _service; | |
var _deviceName; | |
var _chrct_write; | |
var _chrct_read; | |
var _chrct_turn; | |
var _chrct_gyro; | |
var UUID_SUFFIX = '-0000-1000-8000-00805f9b34fb'; | |
var SERVICE_UUID = '00001000' + UUID_SUFFIX; | |
var CHRCT_UUID_WRITE = '00001001' + UUID_SUFFIX; | |
var CHRCT_UUID_READ = '00001002' + UUID_SUFFIX; | |
var CHRCT_UUID_TURN = '00001003' + UUID_SUFFIX; | |
var CHRCT_UUID_GYRO = '00001004' + UUID_SUFFIX; | |
function init(device) { | |
clear(); | |
_deviceName = device.name; | |
return device.gatt.connect().then(function(gatt) { | |
_gatt = gatt; | |
return gatt.getPrimaryService(SERVICE_UUID); | |
}).then(function(service) { | |
_service = service; | |
return _service.getCharacteristics(); | |
}).then(function(chrcts) { | |
for (var i = 0; i < chrcts.length; i++) { | |
var chrct = chrcts[i] | |
DEBUG && console.log('[moyucube]', 'init find chrct', chrct.uuid); | |
if (matchUUID(chrct.uuid, CHRCT_UUID_WRITE)) { | |
_chrct_write = chrct; | |
} else if (matchUUID(chrct.uuid, CHRCT_UUID_READ)) { | |
_chrct_read = chrct; | |
} else if (matchUUID(chrct.uuid, CHRCT_UUID_TURN)) { | |
_chrct_turn = chrct; | |
} else if (matchUUID(chrct.uuid, CHRCT_UUID_GYRO)) { | |
_chrct_gyro = chrct; | |
} | |
} | |
}).then(function() { | |
_chrct_read.addEventListener('characteristicvaluechanged', onReadEvent); | |
_chrct_turn.addEventListener('characteristicvaluechanged', onTurnEvent); | |
_chrct_gyro.addEventListener('characteristicvaluechanged', onGyroEvent); | |
_chrct_read.startNotifications(); | |
_chrct_turn.startNotifications(); | |
_chrct_gyro.startNotifications(); | |
}); | |
} | |
var faceStatus = [0, 0, 0, 0, 0, 0]; | |
var curFacelet = mathlib.SOLVED_FACELET; | |
var curCubie = new mathlib.CubieCube(); | |
var prevCubie = new mathlib.CubieCube(); | |
var timeOffset = 0; | |
function onReadEvent(event) { | |
var value = event.target.value; | |
DEBUG && console.log('[moyucube]', 'Received read event', value); | |
} | |
function onGyroEvent(event) { | |
var value = event.target.value; | |
DEBUG && console.log('[moyucube]', 'Received gyro event', value); | |
} | |
function onTurnEvent(event) { | |
var value = event.target.value; | |
DEBUG && console.log('[moyucube]', 'Received turn event', value); | |
parseTurn(value); | |
} | |
function parseTurn(data) { | |
var locTime = (new Date()).getTime(); | |
if (data.byteLength < 1) { | |
return; | |
} | |
var n_moves = data.getUint8(0); | |
if (data.byteLength < 1 + n_moves * 6) { | |
return; | |
} | |
for (var i = 0; i < n_moves; i++) { | |
var offset = 1 + i * 6; | |
var ts = data.getUint8(offset + 1) << 24 | |
| data.getUint8(offset + 0) << 16 | |
| data.getUint8(offset + 3) << 8 | |
| data.getUint8(offset + 2); | |
ts = Math.round(ts / 65536 * 1000); | |
var face = data.getUint8(offset + 4); | |
var dir = Math.round(data.getUint8(offset + 5) / 36); | |
var prevRot = faceStatus[face]; | |
var curRot = faceStatus[face] + dir; | |
faceStatus[face] = (curRot + 9) % 9; | |
var axis = [3, 4, 5, 1, 2, 0][face]; | |
var pow = 0; | |
if (prevRot >= 5 && curRot <= 4) { | |
pow = 2; | |
} else if (prevRot <= 4 && curRot >= 5) { | |
pow = 0; | |
} else { | |
continue; | |
} | |
var calcTs = ts + timeOffset; | |
// if (timeOffset == 0 || Math.abs(locTime - calcTs) > 2000 || timer.getStatus() == -1 && Math.abs(locTime - calcTs) > 300) { | |
// timeOffset = locTime - ts; | |
// calcTs = locTime; | |
// } | |
var m = axis * 3 + pow; | |
DEBUG && console.log('[moyucube]', 'move', "URFDLB".charAt(axis) + " 2'".charAt(pow)); | |
mathlib.CubieCube.EdgeMult(prevCubie, mathlib.CubieCube.moveCube[m], curCubie); | |
mathlib.CubieCube.CornMult(prevCubie, mathlib.CubieCube.moveCube[m], curCubie); | |
curFacelet = curCubie.toFaceCube(); | |
callback(curFacelet, ["URFDLB".charAt(axis) + " 2'".charAt(pow)], [calcTs, locTime], _deviceName); | |
var tmp = curCubie; | |
curCubie = prevCubie; | |
prevCubie = tmp; | |
} | |
} | |
function getBatteryLevel() { | |
if (!_gatt) { | |
return Promise.reject("Bluetooth Cube is not connected"); | |
} | |
Promise.resolve([100, _deviceName]); | |
} | |
function clear() { | |
var result = Promise.resolve(); | |
if (_chrct_read || _chrct_turn || _chrct_gyro) { | |
_chrct_read && _chrct_read.removeEventListener('characteristicvaluechanged', onReadEvent); | |
_chrct_turn && _chrct_turn.removeEventListener('characteristicvaluechanged', onTurnEvent); | |
_chrct_gyro && _chrct_gyro.removeEventListener('characteristicvaluechanged', onGyroEvent); | |
result = Promise.all([ | |
_chrct_read && _chrct_read.stopNotifications().catch(()=>undefined), | |
_chrct_turn && _chrct_turn.stopNotifications().catch(()=>undefined), | |
_chrct_gyro && _chrct_gyro.stopNotifications().catch(()=>undefined), | |
]); | |
_chrct_read = null; | |
_chrct_turn = null; | |
_chrct_gyro = null; | |
} | |
_chrct_write = null; | |
_service = null; | |
_gatt = null; | |
_deviceName = null; | |
return result; | |
} | |
return { | |
init: init, | |
opservs: [SERVICE_UUID], | |
getBatteryLevel: getBatteryLevel, | |
getName: () => _deviceName, | |
clear: clear | |
} | |
})(); | |
function onHardwareEvent(info, event) { | |
var res = Promise.resolve(); | |
if (info == 'disconnect') { | |
res = Promise.resolve(stop()); | |
} | |
return res.then(function () { | |
return typeof evtCallback == 'function' && evtCallback(info, event); | |
}); | |
} | |
var onDisconnect = onHardwareEvent.bind(null, 'disconnect'); | |
function init() { | |
if (!window.navigator || !window.navigator.bluetooth) { | |
alert("Bluetooth API is not available. Ensure https access, and try chrome with chrome://flags/#enable-experimental-web-platform-features enabled"); | |
return Promise.reject(); | |
} | |
var chkAvail = Promise.resolve(true); | |
if (window.navigator.bluetooth.getAvailability) { | |
chkAvail = window.navigator.bluetooth.getAvailability(); | |
} | |
return chkAvail.then(function(available) { | |
DEBUG && console.log('[bluetooth]', 'is available', available); | |
if (!available) { | |
return Promise.reject("Bluetooth is not available. Ensure HTTPS access, and check bluetooth is enabled on your device"); | |
} | |
return window.navigator.bluetooth.requestDevice({ | |
filters: [{ | |
namePrefix: 'Gi' | |
}, { | |
namePrefix: 'Mi Smart' | |
}, { | |
namePrefix: 'GAN' | |
}, { | |
namePrefix: 'MG' | |
}, { | |
namePrefix: 'AiCube' | |
}, { | |
namePrefix: 'GoCube' | |
}, { | |
namePrefix: 'Rubiks' | |
}, { | |
namePrefix: 'MHC' | |
}], | |
optionalServices: [].concat(GiikerCube.opservs, GanCube.opservs, GoCube.opservs, MoyuCube.opservs), | |
optionalManufacturerData: [].concat(GanCube.cics) | |
}); | |
}).then(function(device) { | |
DEBUG && console.log('[bluetooth]', device); | |
_device = device; | |
device.addEventListener('gattserverdisconnected', onDisconnect); | |
if (device.name.startsWith('Gi') || device.name.startsWith('Mi Smart Magic Cube')) { | |
cube = GiikerCube; | |
return GiikerCube.init(device); | |
} else if (device.name.startsWith('GAN') || device.name.startsWith('MG') || device.name.startsWith('AiCube')) { | |
cube = GanCube; | |
return GanCube.init(device); | |
} else if (device.name.startsWith('GoCube') || device.name.startsWith('Rubiks')) { | |
cube = GoCube; | |
return GoCube.init(device); | |
} else if (device.name.startsWith('MHC')) { | |
cube = MoyuCube; | |
return MoyuCube.init(device); | |
} else { | |
return Promise.reject('Cannot detect device type'); | |
} | |
}); | |
} | |
function stop() { | |
if (!_device) { | |
return Promise.resolve(); | |
} | |
return Promise.resolve(cube && cube.clear()).then(function () { | |
_device.removeEventListener('gattserverdisconnected', onDisconnect); | |
_device.gatt.disconnect(); | |
_device = null; | |
}); | |
} | |
var callback = ()=>undefined; | |
var evtCallback = ()=>undefined; | |
return { | |
init: init, | |
stop: stop, | |
isConnected: function() { | |
return _device != null; | |
}, | |
setCallback: function(func) { | |
callback = func; | |
}, | |
setEventCallback: function(func) { | |
evtCallback = func; | |
}, | |
getCube: function() { | |
return cube; | |
}, | |
getDeviceName: function() { | |
return cube ? cube.getName() : undefined; | |
} | |
}; | |
}; | |
// @ts-nocheck | |
/* | |
Functions for doing cube related maths | |
This file is adapted from cstimer by Shuang Chen (cs0x7f) | |
https://github.com/cs0x7f/cstimer | |
Copyright (C) 2023 Shuang Chen | |
Licensed under the GPLv3 | |
*/ | |
var mathlib = (function() { | |
var Cnk = [], | |
fact = [1]; | |
for (var i = 0; i < 32; ++i) { | |
Cnk[i] = []; | |
for (var j = 0; j < 32; ++j) { | |
Cnk[i][j] = 0; | |
} | |
} | |
for (var i = 0; i < 32; ++i) { | |
Cnk[i][0] = Cnk[i][i] = 1; | |
fact[i + 1] = fact[i] * (i + 1); | |
for (var j = 1; j < i; ++j) { | |
Cnk[i][j] = Cnk[i - 1][j - 1] + Cnk[i - 1][j]; | |
} | |
} | |
function circleOri(arr, a, b, c, d, ori) { | |
var temp = arr[a]; | |
arr[a] = arr[d] ^ ori; | |
arr[d] = arr[c] ^ ori; | |
arr[c] = arr[b] ^ ori; | |
arr[b] = temp ^ ori; | |
} | |
function circle(arr) { | |
var length = arguments.length - 1, | |
temp = arr[arguments[length]]; | |
for (var i = length; i > 1; i--) { | |
arr[arguments[i]] = arr[arguments[i - 1]]; | |
} | |
arr[arguments[1]] = temp; | |
return circle; | |
} | |
//perm: [idx1, idx2, ..., idxn] | |
//pow: 1, 2, 3, ... | |
//ori: ori1, ori2, ..., orin, base | |
// arr[perm[idx2]] = arr[perm[idx1]] + ori[idx2] - ori[idx1] + base | |
function acycle(arr, perm, pow, ori) { | |
pow = pow || 1; | |
var plen = perm.length; | |
var tmp = []; | |
for (var i = 0; i < plen; i++) { | |
tmp[i] = arr[perm[i]]; | |
} | |
for (var i = 0; i < plen; i++) { | |
var j = (i + pow) % plen; | |
arr[perm[j]] = tmp[i]; | |
if (ori) { | |
arr[perm[j]] += ori[j] - ori[i] + ori[ori.length - 1]; | |
} | |
} | |
return acycle; | |
} | |
function getPruning(table, index) { | |
return table[index >> 3] >> ((index & 7) << 2) & 15; | |
} | |
function setNPerm(arr, idx, n) { | |
var i, j; | |
arr[n - 1] = 0; | |
for (i = n - 2; i >= 0; --i) { | |
arr[i] = idx % (n - i); | |
idx = ~~(idx / (n - i)); | |
for (j = i + 1; j < n; ++j) { | |
arr[j] >= arr[i] && ++arr[j]; | |
} | |
} | |
} | |
function getNPerm(arr, n) { | |
var i, idx, j; | |
idx = 0; | |
for (i = 0; i < n; ++i) { | |
idx *= n - i; | |
for (j = i + 1; j < n; ++j) { | |
arr[j] < arr[i] && ++idx; | |
} | |
} | |
return idx; | |
} | |
function getNParity(idx, n) { | |
var i, p; | |
p = 0; | |
for (i = n - 2; i >= 0; --i) { | |
p ^= idx % (n - i); | |
idx = ~~(idx / (n - i)); | |
} | |
return p & 1; | |
} | |
function get8Perm(arr, n, even) { | |
n = n || 8; | |
var idx = 0; | |
var val = 0x76543210; | |
for (var i = 0; i < n - 1; ++i) { | |
var v = arr[i] << 2; | |
idx = (n - i) * idx + (val >> v & 7); | |
val -= 0x11111110 << v; | |
} | |
return even < 0 ? (idx >> 1) : idx; | |
} | |
function set8Perm(arr, idx, n, even) { | |
n = (n || 8) - 1; | |
var val = 0x76543210; | |
var prt = 0; | |
if (even < 0) { | |
idx <<= 1; | |
} | |
for (var i = 0; i < n; ++i) { | |
var p = fact[n - i]; | |
var v = ~~(idx / p); | |
prt ^= v; | |
idx %= p; | |
v <<= 2; | |
arr[i] = val >> v & 7; | |
var m = (1 << v) - 1; | |
val = (val & m) + (val >> 4 & ~m); | |
} | |
if (even < 0 && (prt & 1) != 0) { | |
arr[n] = arr[n - 1]; | |
arr[n - 1] = val & 7; | |
} else { | |
arr[n] = val & 7; | |
} | |
return arr; | |
} | |
function getNOri(arr, n, evenbase) { | |
var base = Math.abs(evenbase); | |
var idx = evenbase < 0 ? 0 : arr[0] % base; | |
for (var i = n - 1; i > 0; i--) { | |
idx = idx * base + arr[i] % base; | |
} | |
return idx; | |
} | |
function setNOri(arr, idx, n, evenbase) { | |
var base = Math.abs(evenbase); | |
var parity = base * n; | |
for (var i = 1; i < n; i++) { | |
arr[i] = idx % base; | |
parity -= arr[i]; | |
idx = ~~(idx / base); | |
} | |
arr[0] = (evenbase < 0 ? parity : idx) % base; | |
return arr; | |
} | |
// type: 'p', 'o' | |
// evenbase: base for ori, sign for even parity | |
function coord(type, length, evenbase) { | |
this.length = length; | |
this.evenbase = evenbase; | |
this.get = type == 'p' ? | |
function(arr) { | |
return get8Perm(arr, this.length, this.evenbase); | |
} : function(arr) { | |
return getNOri(arr, this.length, this.evenbase); | |
}; | |
this.set = type == 'p' ? | |
function(arr, idx) { | |
return set8Perm(arr, idx, this.length, this.evenbase); | |
} : function(arr, idx) { | |
return setNOri(arr, idx, this.length, this.evenbase); | |
}; | |
} | |
function fillFacelet(facelets, f, perm, ori, divcol) { | |
for (var i = 0; i < facelets.length; i++) { | |
for (var j = 0; j < facelets[i].length; j++) { | |
f[facelets[i][(j + ori[i]) % facelets[i].length]] = ~~(facelets[perm[i]][j] / divcol); | |
} | |
} | |
} | |
function createMove(moveTable, size, doMove, N_MOVES) { | |
N_MOVES = N_MOVES || 6; | |
if ($.isArray(doMove)) { | |
var cord = new coord(doMove[1], doMove[2], doMove[3]); | |
doMove = doMove[0]; | |
for (var j = 0; j < N_MOVES; j++) { | |
moveTable[j] = []; | |
for (var i = 0; i < size; i++) { | |
var arr = cord.set([], i); | |
doMove(arr, j); | |
moveTable[j][i] = cord.get(arr); | |
} | |
} | |
} else { | |
for (var j = 0; j < N_MOVES; j++) { | |
moveTable[j] = []; | |
for (var i = 0; i < size; i++) { | |
moveTable[j][i] = doMove(i, j); | |
} | |
} | |
} | |
} | |
function edgeMove(arr, m) { | |
if (m == 0) { //F | |
circleOri(arr, 0, 7, 8, 4, 1); | |
} else if (m == 1) { //R | |
circleOri(arr, 3, 6, 11, 7, 0); | |
} else if (m == 2) { //U | |
circleOri(arr, 0, 1, 2, 3, 0); | |
} else if (m == 3) { //B | |
circleOri(arr, 2, 5, 10, 6, 1); | |
} else if (m == 4) { //L | |
circleOri(arr, 1, 4, 9, 5, 0); | |
} else if (m == 5) { //D | |
circleOri(arr, 11, 10, 9, 8, 0); | |
} | |
} | |
function CubieCube() { | |
this.ca = [0, 1, 2, 3, 4, 5, 6, 7]; | |
this.ea = [0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22]; | |
} | |
CubieCube.SOLVED = new CubieCube(); | |
CubieCube.EdgeMult = function(a, b, prod) { | |
for (var ed = 0; ed < 12; ed++) { | |
prod.ea[ed] = a.ea[b.ea[ed] >> 1] ^ (b.ea[ed] & 1); | |
} | |
}; | |
CubieCube.CornMult = function(a, b, prod) { | |
for (var corn = 0; corn < 8; corn++) { | |
var ori = ((a.ca[b.ca[corn] & 7] >> 3) + (b.ca[corn] >> 3)) % 3; | |
prod.ca[corn] = a.ca[b.ca[corn] & 7] & 7 | ori << 3; | |
} | |
}; | |
CubieCube.CubeMult = function(a, b, prod) { | |
CubieCube.CornMult(a, b, prod); | |
CubieCube.EdgeMult(a, b, prod); | |
}; | |
CubieCube.prototype.init = function(ca, ea) { | |
this.ca = ca.slice(); | |
this.ea = ea.slice(); | |
return this; | |
}; | |
CubieCube.prototype.hashCode = function() { | |
var ret = 0; | |
for (var i = 0; i < 20; i++) { | |
ret = 0 | (ret * 31 + (i < 12 ? this.ea[i] : this.ca[i - 12])); | |
} | |
return ret; | |
} | |
CubieCube.prototype.isEqual = function(c) { | |
c = c || CubieCube.SOLVED; | |
for (var i = 0; i < 8; i++) { | |
if (this.ca[i] != c.ca[i]) { | |
return false; | |
} | |
} | |
for (var i = 0; i < 12; i++) { | |
if (this.ea[i] != c.ea[i]) { | |
return false; | |
} | |
} | |
return true; | |
}; | |
var cornerFacelet = [ | |
[8, 9, 20], // URF | |
[6, 18, 38], // UFL | |
[0, 36, 47], // ULB | |
[2, 45, 11], // UBR | |
[29, 26, 15], // DFR | |
[27, 44, 24], // DLF | |
[33, 53, 42], // DBL | |
[35, 17, 51] // DRB | |
]; | |
var edgeFacelet = [ | |
[5, 10], // UR | |
[7, 19], // UF | |
[3, 37], // UL | |
[1, 46], // UB | |
[32, 16], // DR | |
[28, 25], // DF | |
[30, 43], // DL | |
[34, 52], // DB | |
[23, 12], // FR | |
[21, 41], // FL | |
[50, 39], // BL | |
[48, 14] // BR | |
]; | |
CubieCube.prototype.toFaceCube = function(cFacelet, eFacelet) { | |
cFacelet = cFacelet || cornerFacelet; | |
eFacelet = eFacelet || edgeFacelet; | |
var ts = "URFDLB"; | |
var f = []; | |
for (var i = 0; i < 54; i++) { | |
f[i] = ts[~~(i / 9)]; | |
} | |
for (var c = 0; c < 8; c++) { | |
var j = this.ca[c] & 0x7; // cornercubie with index j is at | |
var ori = this.ca[c] >> 3; // Orientation of this cubie | |
for (var n = 0; n < 3; n++) | |
f[cFacelet[c][(n + ori) % 3]] = ts[~~(cFacelet[j][n] / 9)]; | |
} | |
for (var e = 0; e < 12; e++) { | |
var j = this.ea[e] >> 1; // edgecubie with index j is at edgeposition | |
var ori = this.ea[e] & 1; // Orientation of this cubie | |
for (var n = 0; n < 2; n++) | |
f[eFacelet[e][(n + ori) % 2]] = ts[~~(eFacelet[j][n] / 9)]; | |
} | |
return f.join(""); | |
} | |
CubieCube.prototype.invFrom = function(cc) { | |
for (var edge = 0; edge < 12; edge++) { | |
this.ea[cc.ea[edge] >> 1] = edge << 1 | cc.ea[edge] & 1; | |
} | |
for (var corn = 0; corn < 8; corn++) { | |
this.ca[cc.ca[corn] & 0x7] = corn | 0x20 >> (cc.ca[corn] >> 3) & 0x18; | |
} | |
return this; | |
} | |
CubieCube.prototype.fromFacelet = function(facelet, cFacelet, eFacelet) { | |
cFacelet = cFacelet || cornerFacelet; | |
eFacelet = eFacelet || edgeFacelet; | |
var count = 0; | |
var f = []; | |
var centers = facelet[4] + facelet[13] + facelet[22] + facelet[31] + facelet[40] + facelet[49]; | |
for (var i = 0; i < 54; ++i) { | |
f[i] = centers.indexOf(facelet[i]); | |
if (f[i] == -1) { | |
return -1; | |
} | |
count += 1 << (f[i] << 2); | |
} | |
if (count != 0x999999) { | |
return -1; | |
} | |
var col1, col2, i, j, ori; | |
for (i = 0; i < 8; ++i) { | |
for (ori = 0; ori < 3; ++ori) | |
if (f[cFacelet[i][ori]] == 0 || f[cFacelet[i][ori]] == 3) | |
break; | |
col1 = f[cFacelet[i][(ori + 1) % 3]]; | |
col2 = f[cFacelet[i][(ori + 2) % 3]]; | |
for (j = 0; j < 8; ++j) { | |
if (col1 == ~~(cFacelet[j][1] / 9) && col2 == ~~(cFacelet[j][2] / 9)) { | |
this.ca[i] = j | ori % 3 << 3; | |
break; | |
} | |
} | |
} | |
for (i = 0; i < 12; ++i) { | |
for (j = 0; j < 12; ++j) { | |
if (f[eFacelet[i][0]] == ~~(eFacelet[j][0] / 9) && f[eFacelet[i][1]] == ~~(eFacelet[j][1] / 9)) { | |
this.ea[i] = j << 1; | |
break; | |
} | |
if (f[eFacelet[i][0]] == ~~(eFacelet[j][1] / 9) && f[eFacelet[i][1]] == ~~(eFacelet[j][0] / 9)) { | |
this.ea[i] = j << 1 | 1; | |
break; | |
} | |
} | |
} | |
return this; | |
} | |
CubieCube.prototype.verify = function() { | |
var mask = 0; | |
var sum = 0; | |
for (var e = 0; e < 12; e++) { | |
mask |= 1 << 8 << (this.ea[e] >> 1); | |
sum ^= this.ea[e] & 1; | |
} | |
var cp = []; | |
for (var c = 0; c < 8; c++) { | |
mask |= 1 << (this.ca[c] & 7); | |
sum += this.ca[c] >> 3 << 1; | |
cp.push(this.ca[c] & 0x7); | |
} | |
if (mask != 0xfffff || sum % 6 != 0 | |
|| getNParity(getNPerm(this.ea, 12), 12) != getNParity(getNPerm(cp, 8), 8)) { | |
return -1; | |
} | |
return 0; | |
} | |
CubieCube.moveCube = (function() { | |
var moveCube = []; | |
for (var i = 0; i < 18; i++) { | |
moveCube[i] = new CubieCube(); | |
} | |
moveCube[0].init([3, 0, 1, 2, 4, 5, 6, 7], [6, 0, 2, 4, 8, 10, 12, 14, 16, 18, 20, 22]); | |
moveCube[3].init([20, 1, 2, 8, 15, 5, 6, 19], [16, 2, 4, 6, 22, 10, 12, 14, 8, 18, 20, 0]); | |
moveCube[6].init([9, 21, 2, 3, 16, 12, 6, 7], [0, 19, 4, 6, 8, 17, 12, 14, 3, 11, 20, 22]); | |
moveCube[9].init([0, 1, 2, 3, 5, 6, 7, 4], [0, 2, 4, 6, 10, 12, 14, 8, 16, 18, 20, 22]); | |
moveCube[12].init([0, 10, 22, 3, 4, 17, 13, 7], [0, 2, 20, 6, 8, 10, 18, 14, 16, 4, 12, 22]); | |
moveCube[15].init([0, 1, 11, 23, 4, 5, 18, 14], [0, 2, 4, 23, 8, 10, 12, 21, 16, 18, 7, 15]); | |
for (var a = 0; a < 18; a += 3) { | |
for (var p = 0; p < 2; p++) { | |
CubieCube.EdgeMult(moveCube[a + p], moveCube[a], moveCube[a + p + 1]); | |
CubieCube.CornMult(moveCube[a + p], moveCube[a], moveCube[a + p + 1]); | |
} | |
} | |
return moveCube; | |
})(); | |
CubieCube.rotCube = (function() { | |
var u4 = new CubieCube().init([3, 0, 1, 2, 7, 4, 5, 6], [6, 0, 2, 4, 14, 8, 10, 12, 23, 17, 19, 21]); | |
var f2 = new CubieCube().init([5, 4, 7, 6, 1, 0, 3, 2], [12, 10, 8, 14, 4, 2, 0, 6, 18, 16, 22, 20]); | |
var urf = new CubieCube().init([8, 20, 13, 17, 19, 15, 22, 10], [3, 16, 11, 18, 7, 22, 15, 20, 1, 9, 13, 5]); | |
var c = new CubieCube(); | |
var d = new CubieCube(); | |
var rotCube = []; | |
for (var i = 0; i < 24; i++) { | |
rotCube[i] = new CubieCube().init(c.ca, c.ea); | |
CubieCube.CornMult(c, u4, d); | |
CubieCube.EdgeMult(c, u4, d); | |
c.init(d.ca, d.ea); | |
if (i % 4 == 3) { | |
CubieCube.CornMult(c, f2, d); | |
CubieCube.EdgeMult(c, f2, d); | |
c.init(d.ca, d.ea); | |
} | |
if (i % 8 == 7) { | |
CubieCube.CornMult(c, urf, d); | |
CubieCube.EdgeMult(c, urf, d); | |
c.init(d.ca, d.ea); | |
} | |
} | |
var movHash = []; | |
var rotHash = []; | |
var rotMult = []; | |
var rotMulI = []; | |
var rotMulM = []; | |
for (var i = 0; i < 24; i++) { | |
rotHash[i] = rotCube[i].hashCode(); | |
rotMult[i] = []; | |
rotMulI[i] = []; | |
rotMulM[i] = []; | |
} | |
for (var i = 0; i < 18; i++) { | |
movHash[i] = CubieCube.moveCube[i].hashCode(); | |
} | |
for (var i = 0; i < 24; i++) { | |
for (var j = 0; j < 24; j++) { | |
CubieCube.CornMult(rotCube[i], rotCube[j], c); | |
CubieCube.EdgeMult(rotCube[i], rotCube[j], c); | |
var k = rotHash.indexOf(c.hashCode()); | |
rotMult[i][j] = k; | |
rotMulI[k][j] = i; | |
} | |
} | |
for (var i = 0; i < 24; i++) { | |
for (var j = 0; j < 18; j++) { | |
CubieCube.CornMult(rotCube[rotMulI[0][i]], CubieCube.moveCube[j], c); | |
CubieCube.EdgeMult(rotCube[rotMulI[0][i]], CubieCube.moveCube[j], c); | |
CubieCube.CornMult(c, rotCube[i], d); | |
CubieCube.EdgeMult(c, rotCube[i], d); | |
var k = movHash.indexOf(d.hashCode()); | |
rotMulM[i][j] = k; | |
} | |
} | |
var rot2str = [ | |
"", "y'", "y2", "y", | |
"z2", "y' z2", "y2 z2", "y z2", | |
"y' x'", "y2 x'", "y x'", "x'", | |
"y' x", "y2 x", "y x", "x", | |
"y z", "z", "y' z", "y2 z", | |
"y' z'", "y2 z'", "y z'", "z'" | |
]; | |
CubieCube.rotMult = rotMult; | |
CubieCube.rotMulI = rotMulI; | |
CubieCube.rotMulM = rotMulM; | |
CubieCube.rot2str = rot2str; | |
return rotCube; | |
})(); | |
CubieCube.prototype.edgeCycles = function() { | |
var visited = []; | |
var small_cycles = [0, 0, 0]; | |
var cycles = 0; | |
var parity = false; | |
for (var x = 0; x < 12; ++x) { | |
if (visited[x]) { | |
continue | |
} | |
var length = -1; | |
var flip = false; | |
var y = x; | |
do { | |
visited[y] = true; | |
++length; | |
flip ^= this.ea[y] & 1; | |
y = this.ea[y] >> 1; | |
} while (y != x); | |
cycles += length >> 1; | |
if (length & 1) { | |
parity = !parity; | |
++cycles; | |
} | |
if (flip) { | |
if (length == 0) { | |
++small_cycles[0]; | |
} else if (length & 1) { | |
small_cycles[2] ^= 1; | |
} else { | |
++small_cycles[1]; | |
} | |
} | |
} | |
small_cycles[1] += small_cycles[2]; | |
if (small_cycles[0] < small_cycles[1]) { | |
cycles += (small_cycles[0] + small_cycles[1]) >> 1; | |
} else { | |
var flip_cycles = [0, 2, 3, 5, 6, 8, 9]; | |
cycles += small_cycles[1] + flip_cycles[(small_cycles[0] - small_cycles[1]) >> 1]; | |
} | |
return cycles - parity; | |
}; | |
var CubeMoveRE = /^\s*([URFDLB]w?|[EMSyxz]|2-2[URFDLB]w)(['2]?)(@\d+)?\s*$/; | |
var tmpCubie = new CubieCube(); | |
CubieCube.prototype.selfMoveStr = function(moveStr, isInv) { | |
var m = CubeMoveRE.exec(moveStr); | |
if (!m) { | |
return; | |
} | |
var face = m[1]; | |
var pow = "2'".indexOf(m[2] || '-') + 2; | |
if (isInv) { | |
pow = 4 - pow; | |
} | |
if (m[3]) { | |
this.tstamp = ~~m[3].slice(1); | |
} | |
this.ori = this.ori || 0; | |
var axis = 'URFDLB'.indexOf(face); | |
if (axis != -1) { | |
m = axis * 3 + pow % 4 - 1 | |
m = CubieCube.rotMulM[this.ori][m]; | |
CubieCube.EdgeMult(this, CubieCube.moveCube[m], tmpCubie); | |
CubieCube.CornMult(this, CubieCube.moveCube[m], tmpCubie); | |
this.init(tmpCubie.ca, tmpCubie.ea); | |
return m; | |
} | |
axis = 'UwRwFwDwLwBw'.indexOf(face); | |
if (axis != -1) { | |
axis >>= 1; | |
m = (axis + 3) % 6 * 3 + pow % 4 - 1 | |
m = CubieCube.rotMulM[this.ori][m]; | |
CubieCube.EdgeMult(this, CubieCube.moveCube[m], tmpCubie); | |
CubieCube.CornMult(this, CubieCube.moveCube[m], tmpCubie); | |
this.init(tmpCubie.ca, tmpCubie.ea); | |
var rot = [3, 15, 17, 1, 11, 23][axis]; | |
for (var i = 0; i < pow; i++) { | |
this.ori = CubieCube.rotMult[rot][this.ori]; | |
} | |
return m; | |
} | |
axis = ['2-2Uw', '2-2Rw', '2-2Fw', '2-2Dw', '2-2Lw', '2-2Bw'].indexOf(face); | |
if (axis == -1) { | |
axis = [null, null, 'S', 'E', 'M', null].indexOf(face); | |
} | |
if (axis != -1) { | |
var m1 = axis * 3 + (4 - pow) % 4 - 1; | |
var m2 = (axis + 3) % 6 * 3 + pow % 4 - 1; | |
m1 = CubieCube.rotMulM[this.ori][m1]; | |
CubieCube.EdgeMult(this, CubieCube.moveCube[m1], tmpCubie); | |
CubieCube.CornMult(this, CubieCube.moveCube[m1], tmpCubie); | |
this.init(tmpCubie.ca, tmpCubie.ea); | |
m2 = CubieCube.rotMulM[this.ori][m2]; | |
CubieCube.EdgeMult(this, CubieCube.moveCube[m2], tmpCubie); | |
CubieCube.CornMult(this, CubieCube.moveCube[m2], tmpCubie); | |
this.init(tmpCubie.ca, tmpCubie.ea); | |
var rot = [3, 15, 17, 1, 11, 23][axis]; | |
for (var i = 0; i < pow; i++) { | |
this.ori = CubieCube.rotMult[rot][this.ori]; | |
} | |
return m1 + 18; | |
} | |
axis = 'yxz'.indexOf(face); | |
if (axis != -1) { | |
var rot = [3, 15, 17][axis]; | |
for (var i = 0; i < pow; i++) { | |
this.ori = CubieCube.rotMult[rot][this.ori]; | |
} | |
return; | |
} | |
} | |
CubieCube.prototype.selfConj = function(conj) { | |
if (conj === undefined) { | |
conj = this.ori; | |
} | |
if (conj != 0) { | |
CubieCube.CornMult(CubieCube.rotCube[conj], this, tmpCubie); | |
CubieCube.EdgeMult(CubieCube.rotCube[conj], this, tmpCubie); | |
CubieCube.CornMult(tmpCubie, CubieCube.rotCube[CubieCube.rotMulI[0][conj]], this); | |
CubieCube.EdgeMult(tmpCubie, CubieCube.rotCube[CubieCube.rotMulI[0][conj]], this); | |
this.ori = CubieCube.rotMulI[this.ori][conj] || 0; | |
} | |
} | |
var minx = (function() { | |
var U = 0, R = 1, F = 2, L = 3, BL = 4, BR = 5, DR = 6, DL = 7, DBL = 8, B = 9, DBR = 10, D = 11; | |
var oppFace = [D, DBL, B, DBR, DR, DL, BL, BR, R, F, L, U]; | |
var adjFaces = [ | |
[BR, R, F, L, BL], //U | |
[DBR, DR, F, U, BR], //R | |
[DR, DL, L, U, R], //F | |
[DL, DBL, BL, U, F], //L | |
[DBL, B, BR, U, L], //BL | |
[B, DBR, R, U, BL], //BR | |
[D, DL, F, R, DBR], //DR | |
[D, DBL, L, F, DR], //DL | |
[D, B, BL, L, DL], //DBL | |
[D, DBR, BR, BL, DBL], //B | |
[D, DR, R, BR, B], //DBR | |
[DR, DBR, B, DBL, DL] //D | |
]; | |
// wide: 0=single, 1=all, 2=all but single | |
// state: corn*5, edge*5, center*1 | |
function doMove(state, face, pow, wide) { | |
pow = (pow % 5 + 5) % 5; | |
if (pow == 0) { | |
return; | |
} | |
var base = face * 11; | |
var adjs = []; | |
var swaps = [[], [], [], [], []]; | |
for (var i = 0; i < 5; i++) { | |
var aface = adjFaces[face][i]; | |
var ridx = adjFaces[aface].indexOf(face); | |
if (wide == 0 || wide == 1) { | |
swaps[i].push(base + i); | |
swaps[i].push(base + i + 5); | |
swaps[i].push(aface * 11 + ridx % 5 + 5); | |
swaps[i].push(aface * 11 + ridx % 5); | |
swaps[i].push(aface * 11 + (ridx + 1) % 5); | |
} | |
if (wide == 1 || wide == 2) { | |
swaps[i].push(aface * 11 + 10); | |
for (var j = 1; j < 5; j++) { | |
swaps[i].push(aface * 11 + (ridx + j) % 5 + 5); | |
} | |
for (var j = 2; j < 5; j++) { | |
swaps[i].push(aface * 11 + (ridx + j) % 5); | |
} | |
var ii = 4 - i; | |
var opp = oppFace[face]; | |
var oaface = adjFaces[opp][ii]; | |
var oridx = adjFaces[oaface].indexOf(opp); | |
swaps[i].push(opp * 11 + ii); | |
swaps[i].push(opp * 11 + ii + 5); | |
swaps[i].push(oaface * 11 + 10); | |
for (var j = 0; j < 5; j++) { | |
swaps[i].push(oaface * 11 + (oridx + j) % 5 + 5); | |
swaps[i].push(oaface * 11 + (oridx + j) % 5); | |
} | |
} | |
} | |
for (var i = 0; i < swaps[0].length; i++) { | |
mathlib.acycle(state, [swaps[0][i], swaps[1][i], swaps[2][i], swaps[3][i], swaps[4][i]], pow); | |
} | |
} | |
return { | |
doMove: doMove, | |
oppFace: oppFace, | |
adjFaces: adjFaces | |
} | |
})(); | |
function SchreierSims(gen, shuffle) { | |
if (gen.sgs) { | |
this.copy(gen); | |
return; | |
} | |
this.sgs = []; | |
this.sgsi = []; | |
this.t2i = []; | |
this.i2t = []; | |
this.keyIdx = []; | |
this.Tk = []; | |
this.e = []; | |
var n = gen[0].length; | |
for (var i = 0; i < n; i++) { | |
this.e[i] = i; | |
} | |
for (var i = 0; i < n; i++) { | |
this.sgs.push([]); | |
this.sgsi.push([]); | |
this.t2i.push([]); | |
this.i2t.push([i]); | |
this.Tk.push([]); | |
this.sgs[i][i] = this.e; | |
this.sgsi[i][i] = this.e; | |
this.t2i[i][i] = 0; | |
} | |
for (var i = 0; i < gen.length; i++) { | |
var g = gen[i]; | |
if (shuffle) { | |
g = this.permMult(this.permMult(this.permInv(shuffle), g), shuffle); | |
} | |
this.knutha(n - 1, g); | |
} | |
// for minkwitz algorithm | |
// this.invMap = {}; | |
// this.gen = gen; | |
} | |
SchreierSims.prototype.copy = function(obj) { | |
this.sgs = []; | |
this.sgsi = []; | |
this.t2i = []; | |
this.i2t = []; | |
this.keyIdx = obj.keyIdx.slice(); | |
this.Tk = []; | |
this.e = obj.e; | |
var n = this.e.length; | |
for (var i = 0; i < n; i++) { | |
this.sgs[i] = obj.sgs[i].slice(); | |
this.sgsi[i] = obj.sgsi[i].slice(); | |
this.t2i[i] = obj.t2i[i].slice(); | |
this.i2t[i] = obj.i2t[i].slice(); | |
this.Tk[i] = obj.Tk[i].slice(); | |
} | |
} | |
SchreierSims.prototype.permMult = function(permA, permB) { | |
var ret = []; | |
for (var i = 0; i < permA.length; i++) { | |
ret[i] = permB[permA[i]]; | |
} | |
return ret; | |
} | |
SchreierSims.prototype.permMMultKey = function(perms) { | |
var ret = []; | |
for (var i = 0; i < this.keyIdx.length; i++) { | |
var idx = this.keyIdx[i]; | |
for (var j = 0; j < perms.length; j++) { | |
idx = perms[j][idx]; | |
} | |
ret[i] = idx; | |
} | |
return ret; | |
} | |
SchreierSims.prototype.toKeyIdx = function(perm) { | |
var ret = []; | |
perm = perm || this.e; | |
for (var i = 0; i < this.keyIdx.length; i++) { | |
ret[i] = perm[this.keyIdx[i]]; | |
} | |
return ret; | |
} | |
SchreierSims.prototype.permInv = function(perm) { | |
var ret = []; | |
for (var i = 0; i < perm.length; i++) { | |
ret[perm[i]] = i; | |
} | |
return ret; | |
} | |
SchreierSims.prototype.permCmp = function(perm1, perm2) { | |
if (perm1.length != perm2.length) { | |
return perm1.length - perm2.length; | |
} | |
for (var i = 0; i < perm1.length; i++) { | |
if (perm1[i] != perm2[i]) { | |
return perm1[i] - perm2[i]; | |
} | |
} | |
return 0; | |
} | |
SchreierSims.prototype.isMember = function(p, depth) { | |
depth = depth || 0; | |
var idx = 0; | |
for (var i = p.length - 1; i >= depth; i--) { | |
var j = p[i]; | |
if (j !== i) { | |
if (!this.sgs[i][j]) { | |
return -1; | |
} | |
p = this.permMult(p, this.sgsi[i][j]); | |
} | |
idx = idx * this.i2t[i].length + this.t2i[i][j]; | |
} | |
return idx; | |
} | |
SchreierSims.prototype.isMember2 = function(p, depth) { | |
depth = depth || 0; | |
var idx = 0; | |
var ps = []; | |
for (var i = p.length - 1; i >= depth; i--) { | |
var j = p[i]; | |
for (var k = 0; k < ps.length; k++) { | |
j = ps[k][j]; | |
} | |
if (j !== i) { | |
if (!this.sgs[i][j]) { | |
return -1; | |
} | |
ps.push(this.sgsi[i][j]); | |
} | |
idx = idx * this.i2t[i].length + this.t2i[i][j]; | |
} | |
return idx; | |
} | |
SchreierSims.prototype.knutha = function(k, p) { | |
this.Tk[k].push(p); | |
for (var i = 0; i < this.sgs[k].length; i++) { | |
if (this.sgs[k][i]) { | |
this.knuthb(k, this.permMult(this.sgs[k][i], p)); | |
} | |
} | |
} | |
SchreierSims.prototype.knuthb = function(k, p) { | |
var j = p[k]; | |
if (!this.sgs[k][j]) { | |
this.sgs[k][j] = p; | |
this.sgsi[k][j] = this.permInv(p); | |
this.t2i[k][j] = this.i2t[k].length; | |
this.i2t[k].push(j); | |
if (this.i2t[k].length == 2) { | |
this.keyIdx.push(k); | |
} | |
for (var i = 0; i < this.Tk[k].length; i++) { | |
this.knuthb(k, this.permMult(p, this.Tk[k][i])); | |
} | |
return; | |
} | |
var p2 = this.permMult(p, this.sgsi[k][j]); | |
if (this.isMember(p2) < 0) { | |
this.knutha(k - 1, p2); | |
} | |
} | |
SchreierSims.prototype.size = function() { | |
var n = this.sgs.length; | |
var size = 1; | |
for (var j = 0; j < n; j++) { | |
var cnt = 0; | |
for (var k = 0; k < n; k++) { | |
if (this.sgs[j][k]) { | |
cnt++; | |
} | |
} | |
size *= cnt; | |
} | |
return size; | |
} | |
SchreierSims.prototype.minElem = function(p, depth) { | |
depth = depth || 0; | |
p = this.permInv(p); | |
for (var i = p.length - 1; i >= depth; i--) { | |
var maxi = p[i]; | |
var j = i; | |
for (var k = 0; k < this.i2t[i].length; k++) { | |
var m = this.i2t[i][k]; | |
if (p[this.sgs[i][m][i]] > maxi) { | |
maxi = p[this.sgs[i][m][i]]; | |
j = m; | |
} | |
} | |
if (j !== i) { | |
p = this.permMult(this.sgs[i][j], p); | |
} | |
} | |
return this.permInv(p); | |
} | |
SchreierSims.prototype.rndElem = function() { | |
var perm = this.e.slice(); | |
for (var i = this.e.length - 1; i >= 0; i--) { | |
var cnt = 0; | |
var p = 0; | |
for (var j = 0; j <= i; j++) { | |
if (!this.sgs[i][j]) { | |
continue; | |
} | |
if (rn(++cnt) < 1) { | |
p = j; | |
} | |
} | |
if (p !== i) { | |
perm = this.permMult(perm, this.sgsi[i][p]); | |
} | |
} | |
return perm; | |
} | |
function CanonSeqGen(gens) { | |
this.gens = gens; | |
this.glen = gens.length; | |
this.trieNodes = [null]; | |
this.trieNodes.push([]); | |
} | |
CanonSeqGen.prototype.permMult = function(permA, permB) { | |
var ret = []; | |
for (var i = 0; i < permA.length; i++) { | |
ret[i] = permB[permA[i]]; | |
} | |
return ret; | |
} | |
CanonSeqGen.prototype.addSkipSeq = function(seq) { | |
var node = 1; | |
for (var i = 0; i < seq.length; i++) { | |
var next = ~~this.trieNodes[node][seq[i]]; | |
if (next == -1) { | |
return; | |
} | |
if (i == seq.length - 1) { | |
this.trieNodes[node][seq[i]] = -1; | |
break; | |
} | |
if (next <= 0) { // empty node, create a new node | |
next = this.trieNodes.length; | |
this.trieNodes.push([]); | |
this.trieNodes[node][seq[i]] = next; | |
for (var m = 0; m < this.glen; m++) { | |
this.updateNext(seq.slice(0, i + 1).concat(m)); | |
} | |
} | |
node = next; | |
} | |
} | |
CanonSeqGen.prototype.traversalTrie = function(node, seq, callback) { | |
if (node <= 0) { | |
return; | |
} | |
for (var i = 0; i < this.glen; i++) { | |
seq.push(i); | |
this.traversalTrie(~~this.trieNodes[node][i], seq, callback) | |
seq.pop(); | |
} | |
callback(node, seq); | |
} | |
CanonSeqGen.prototype.updateNext = function(seq) { | |
var node = 1; | |
for (var i = 0; i < seq.length; i++) { | |
var next = ~~this.trieNodes[node][seq[i]]; | |
if (next == 0) { | |
next = this.updateNext(seq.slice(1, i + 1)); | |
next = next > 0 ? ~next : next; | |
this.trieNodes[node][seq[i]] = next; | |
} | |
if (next == -1) { | |
return -1; | |
} else if (next < 0) { | |
next = ~next; | |
} | |
node = next; | |
} | |
return node; | |
} | |
CanonSeqGen.prototype.refillNext = function() { | |
// clear next nodes | |
this.traversalTrie(1, [], function(node, seq) { | |
for (var i = 0; i < this.glen; i++) { | |
var next = ~~this.trieNodes[node][i]; | |
if (next != -1 && next <= node) { // skip or to sub-trie | |
this.trieNodes[node][i] = 0; | |
} | |
} | |
}.bind(this)); | |
// calculate next nodes | |
this.traversalTrie(1, [], function(node, seq) { | |
for (var i = 0; i < this.glen; i++) { | |
if ((i & 0x1f) == 0) { | |
this.trieNodes[node][this.glen + (i >> 5)] = 0; | |
} | |
var next = ~~this.trieNodes[node][i]; | |
if (next != -1 && next <= node) { // skip or to sub-trie | |
this.updateNext(seq.concat(i)); | |
} | |
if (~~this.trieNodes[node][i] == -1) { | |
this.trieNodes[node][this.glen + (i >> 5)] |= 1 << (i & 0x1f); | |
} | |
} | |
}.bind(this)); | |
} | |
/* | |
CanonSeqGen.prototype.finalize = function() { | |
var diff = []; | |
for (var i = 1; i < this.trieNodes.length; i++) { | |
diff[i] = []; | |
} | |
var changed = true; | |
while (changed) { | |
changed = false; | |
for (var node1 = 1; node1 < this.trieNodes.length; node1++) { | |
for (var node2 = 1; node2 < node1; node2++) { | |
if (diff[node1][node2]) { | |
continue; | |
} | |
for (var i = 0; i < this.glen; i++) { | |
var next1 = ~~this.trieNodes[node1][i]; | |
var next2 = ~~this.trieNodes[node2][i]; | |
if (next1 == -1 && next2 == -1) { | |
continue; | |
} | |
if ((next1 == -1) != (next2 == -1)) { | |
diff[node1][node2] = true; | |
changed = true; | |
break; | |
} | |
next1 ^= next1 >> 31; | |
next2 ^= next2 >> 31; | |
if (next1 != next2 && diff[Math.max(next1, next2)][Math.min(next1, next2)]) { | |
diff[node1][node2] = true; | |
changed = true; | |
break; | |
} | |
} | |
} | |
} | |
} | |
var nodeMap = [0]; | |
var idx = 1; | |
for (var i = 1; i < this.trieNodes.length; i++) { | |
nodeMap[i] = i; | |
for (var j = 1; j < i; j++) { | |
if (!diff[i][j]) { | |
nodeMap[i] = nodeMap[j]; | |
} | |
} | |
if (nodeMap[i] == i) { | |
nodeMap[i] = idx; | |
idx++; | |
} | |
} | |
for (var node = 1; node < this.trieNodes.length; node++) { | |
for (var i = 0; i < this.glen; i++) { | |
var next = ~~this.trieNodes[node][i]; | |
var old = next ^ (next >> 31); | |
this.trieNodes[node][i] = nodeMap[old] ^ (next >> 31); | |
} | |
} | |
for (var i = 1; i < this.trieNodes.length; i++) { | |
this.trieNodes[nodeMap[i]] = this.trieNodes[i]; | |
} | |
while (this.trieNodes.length > idx) { | |
this.trieNodes.pop(); | |
} | |
console.log(diff, nodeMap, idx); | |
} | |
*/ | |
CanonSeqGen.prototype.countSeq = function(depth) { | |
var counts = [0, 1]; | |
var ret = [1]; | |
for (var d = 0; d < depth; d++) { | |
var newCounts = []; | |
var depthCnt = 0; | |
for (var node = 1; node < this.trieNodes.length; node++) { | |
var curCount = counts[node] || 0; | |
if (curCount == 0) { | |
continue; | |
} | |
for (var i = 0; i < this.glen; i++) { | |
var next = ~~this.trieNodes[node][i]; | |
if (next != -1) { | |
next = next < 0 ? ~next : next; | |
newCounts[next] = (newCounts[next] || 0) + curCount; | |
depthCnt += curCount; | |
} | |
} | |
} | |
counts = newCounts; | |
ret.push(depthCnt); | |
} | |
return ret; | |
} | |
CanonSeqGen.prototype.countSeqMove = function(depth, moveTable, initState) { | |
var counts = []; | |
counts[initState * this.trieNodes.length + 1 - 1] = 1; | |
var ret = []; | |
for (var d = 0; d < depth; d++) { | |
var newCounts = []; | |
var depthCnts = []; | |
var depthCnt = 0; | |
for (var state = 0; state < moveTable[0].length; state++) { | |
for (var node = 1; node < this.trieNodes.length; node++) { | |
var curCount = counts[state * this.trieNodes.length + node - 1] || 0; | |
if (curCount == 0) { | |
continue; | |
} | |
for (var i = 0; i < this.glen; i++) { | |
var next = ~~this.trieNodes[node][i]; | |
if (next != -1) { | |
next = next < 0 ? ~next : next; | |
var newState = moveTable[i][state]; | |
var idx = newState * this.trieNodes.length + next - 1; | |
newCounts[idx] = (newCounts[idx] || 0) + curCount; | |
depthCnts[newState] = (depthCnts[newState] || 0) + curCount; | |
depthCnt += curCount; | |
} | |
} | |
} | |
} | |
counts = newCounts; | |
ret.push(depthCnts, depthCnt); | |
} | |
return ret; | |
} | |
CanonSeqGen.prototype.initTrie = function(depth) { | |
this.trieNodes = [null]; | |
this.trieNodes.push([]); | |
this.refillNext(); | |
var e = []; | |
for (var i = 0; i < this.gens[0].length; i++) { | |
e[i] = i; | |
} | |
var visited = new Map(); | |
for (var seqlen = 0; seqlen <= depth; seqlen++) { | |
this.searchSkip(e, seqlen, [], 1, visited); | |
this.refillNext(); | |
} | |
} | |
CanonSeqGen.prototype.searchSkip = function(perm, maxl, seq, node, visited) { | |
if (maxl == 0) { | |
var key = String.fromCharCode.apply(null, perm); | |
if (visited.has(key)) { | |
// console.log('find skip seq', seq, 'replaced by', visited.get(key)); | |
this.addSkipSeq(seq); | |
} else { | |
visited.set(key, seq.slice()); | |
} | |
return; | |
} | |
for (var i = 0; i < this.glen; i++) { | |
var next = this.trieNodes[node][i]; | |
if (next == -1) { | |
continue; | |
} else if (next < 0) { | |
next = ~next; | |
} | |
var gen = this.gens[i]; | |
var permNew = this.permMult(gen, perm); | |
seq.push(i); | |
this.searchSkip(permNew, maxl - 1, seq, next, visited); | |
seq.pop(); | |
} | |
} | |
function SubgroupSolver(genG, genH) { | |
this.genG = genG; | |
this.genH = genH; | |
} | |
SubgroupSolver.prototype.permHash = function(perm) { | |
return String.fromCharCode.apply(null, perm); | |
} | |
SubgroupSolver.prototype.cosetHash = function(perm) { | |
return this.sgsH == null ? this.sgsG.isMember2(this.sgsG.permInv(perm), this.sgsHdepth) : this.permHash(this.sgsH.minElem(perm)); | |
} | |
SubgroupSolver.prototype.initTables = function(maxCosetSize) { | |
if (this.coset2idx) { | |
return; | |
} | |
maxCosetSize = maxCosetSize || 100000; | |
var cosetSize = 1; | |
this.sgsG = new SchreierSims(this.genG); | |
if (this.genH) { | |
this.sgsH = new SchreierSims(this.genH); | |
cosetSize = this.sgsG.size() / this.sgsH.size(); | |
} else { | |
this.sgsH = null; | |
this.sgsHdepth = 0; | |
for (var i = this.sgsG.e.length - 1; i >= 0; i--) { | |
if (cosetSize * this.sgsG.i2t[i].length > maxCosetSize) { | |
break; | |
} | |
this.sgsHdepth = i; | |
cosetSize *= this.sgsG.i2t[i].length; | |
} | |
/* | |
console.log('target space:', cosetSize); | |
var genH = []; | |
for (var i = this.sgsHdepth - 1; i >= 0; i--) { | |
for (var j = 1; j < this.sgsG.i2t[i].length; j++) { | |
genH.push(this.sgsG.sgs[i][this.sgsG.i2t[i][j]]); | |
} | |
} | |
this.sgsH = new SchreierSims(genH); | |
cosetSize = this.sgsG.size() / this.sgsH.size(); | |
*/ | |
} | |
DEBUG && console.log('[Subgroup Solver] coset space:', cosetSize); | |
this.genEx = []; | |
this.genExi = []; | |
this.genExMap = []; | |
var genExSet = new Map(); | |
genExSet.set(this.permHash(this.sgsG.e), -1); | |
for (var i = 0; i < this.genG.length; i++) { | |
var perm = this.genG[i]; | |
var pow = 1; | |
while (true) { | |
var key = this.permHash(perm); | |
if (genExSet.has(key)) { | |
break; | |
} | |
genExSet.set(key, this.genEx.length); | |
this.genEx.push(perm); | |
this.genExi.push(this.sgsG.permInv(perm)); | |
this.genExMap.push([i, pow]); | |
perm = this.sgsG.permMult(this.genG[i], perm); | |
pow++; | |
} | |
} | |
this.glen = this.genEx.length; | |
for (var i = 0; i < this.glen; i++) { | |
var genInv = this.sgsG.permInv(this.genEx[i]); | |
this.genExMap[i][2] = genExSet.get(this.permHash(genInv)); | |
} | |
this.canon = new CanonSeqGen(this.genEx); | |
this.canon.initTrie(2); | |
this.moveTable = []; | |
this.idx2coset = [this.sgsG.e]; | |
this.coset2idx = {}; | |
var tt = +new Date; | |
this.coset2idx[this.cosetHash(this.sgsG.e)] = 0; | |
var sumPrun = 0; | |
for (var i = 0; i < this.idx2coset.length; i++) { | |
if (i > cosetSize) { | |
console.log('ERROR!'); | |
break; | |
} | |
var perm = this.idx2coset[i]; | |
for (var j = 0; j < this.glen; j++) { | |
if (this.genExMap[j][1] != 1) { | |
continue; | |
} | |
var newp = this.sgsG.permMult(this.genEx[j], perm); | |
var key = this.cosetHash(newp); | |
if (!(key in this.coset2idx)) { | |
this.coset2idx[key] = this.idx2coset.length; | |
this.idx2coset.push(newp); | |
} | |
this.moveTable[i * this.glen + j] = this.coset2idx[key]; | |
} | |
} | |
var stdMove = null; | |
for (var j = 0; j < this.glen; j++) { | |
if (this.genExMap[j][1] == 1) { | |
stdMove = j; | |
continue; | |
} | |
for (var i = 0; i < this.idx2coset.length; i++) { | |
this.moveTable[i * this.glen + j] = this.moveTable[this.moveTable[i * this.glen + j - 1] * this.glen + stdMove]; | |
} | |
} | |
this.prunTable = this.initPrunTable(0); | |
DEBUG && console.log('[Subgroup Solver] prun table size:', this.prunTable[0].length); | |
} | |
SubgroupSolver.prototype.idaSearch = function(pidx, maxl, lm, moves, perm, prunTable, callback) { | |
var nodePrun = prunTable[0][pidx]; | |
if (nodePrun > maxl) { | |
return false; | |
} | |
if (maxl == 0) { | |
return callback(moves, perm); | |
} | |
var node = this.canon.trieNodes[lm]; | |
var glenBase = pidx * ((this.glen + 31) >> 5); | |
for (var mbase = 0; mbase < this.glen; mbase += 32) { | |
var mask = node[this.glen + (mbase >> 5)]; | |
mask |= (nodePrun >= maxl - 1) ? prunTable[nodePrun - maxl + 2][glenBase + (mbase >> 5)] : 0; | |
mask = ~mask & ((1 << Math.min(32, this.glen - mbase)) - 1); | |
while (mask != 0) { | |
var midx = 31 - Math.clz32(mask); | |
mask -= 1 << midx; | |
midx += mbase; | |
var newpidx = this.moveTable[pidx * this.glen + midx]; | |
if (DEBUG && prunTable[0][newpidx] >= maxl) { | |
debugger; | |
} | |
var nextCanon = node[midx]; | |
moves.push(midx); | |
// var newPerm = []; | |
var newPerm = this.sgsG.permMult(perm, this.genExi[midx]); | |
var ret = this.idaSearch(newpidx, maxl - 1, nextCanon ^ (nextCanon >> 31), moves, newPerm, prunTable, callback); | |
moves.pop(); | |
if (ret) { | |
return ret; | |
} | |
} | |
} | |
return false; | |
} | |
SubgroupSolver.prototype.initPrunTable = function(solvedIdx) { | |
var prunTable = []; | |
var fartherMask = []; | |
var nocloserMask = []; | |
var maskBase = (this.glen + 31) >> 5; | |
for (var i = 0; i < this.idx2coset.length; i++) { | |
prunTable[i] = -1; | |
} | |
prunTable[solvedIdx] = 0; | |
var fill = 1; | |
var lastfill = 0; | |
var cur = 0; | |
while (fill != lastfill) { | |
lastfill = fill; | |
for (var idx = 0; idx < this.idx2coset.length; idx++) { | |
if (prunTable[idx] != cur) { | |
continue; | |
} | |
for (var j = 0; j < this.glen; j++) { | |
var newIdx = this.moveTable[idx * this.glen + j]; | |
var newPrun = prunTable[newIdx]; | |
if (prunTable[newIdx] == -1) { | |
prunTable[newIdx] = cur + 1; | |
newPrun = cur + 1; | |
fill++; | |
} | |
if (newPrun > cur) { | |
fartherMask[idx * maskBase + (j >> 5)] |= 1 << (j & 0x1f); | |
} | |
if (newPrun >= cur) { | |
nocloserMask[idx * maskBase + (j >> 5)] |= 1 << (j & 0x1f); | |
} | |
} | |
} | |
cur++; | |
} | |
return [prunTable, fartherMask, nocloserMask]; | |
} | |
SubgroupSolver.prototype.DissectionSolve = function(perm, maxl, onlyIDA) { | |
this.initTables(); | |
if (this.sgsG.isMember(perm) < 0) { | |
console.log('[Subgroup Solver] NOT A MEMBER OF G'); | |
return; | |
} | |
var pidx = this.coset2idx[this.cosetHash(perm)]; | |
if (!pidx && pidx !== 0) { | |
console.log('[Subgroup Solver] ERROR!'); | |
return; | |
} | |
var solution = null; | |
var prunTable2 = null; | |
for (var depth = 0; depth <= maxl; depth++) { | |
var tt = performance.now(); | |
var s1tot = 0; | |
var s2tot = 0; | |
var permi = this.sgsG.permInv(perm); | |
if (onlyIDA || depth <= this.prunTable[0][this.prunTable[0].length - 1]) { | |
this.idaSearch(pidx, depth, 1, [], this.sgsG.toKeyIdx(permi), this.prunTable, function(moves, permKey) { | |
s1tot++; | |
for (var k = 0; k < this.sgsG.keyIdx.length; k++) { | |
if (permKey[k] != this.sgsG.keyIdx[k]) { | |
return; | |
} | |
} | |
solution = moves.slice(); | |
return true; | |
}.bind(this)); | |
DEBUG && console.log('[Subgroup Solver] ida ', s1tot + s2tot, 'node(s) checked at', depth, 'tt=', performance.now() - tt); | |
if (solution != null) { | |
return solution; | |
} | |
continue; | |
} | |
var mid = ~~(depth / 2); | |
if (!prunTable2) { | |
prunTable2 = this.initPrunTable(pidx); | |
} | |
var mpcnt = 0; | |
var mpsizes = []; | |
for (var mpidx = 0; mpidx < this.idx2coset.length; mpidx++) { | |
//pidx at mid == mpidx | |
if (this.prunTable[0][mpidx] > mid || prunTable2[0][mpidx] > depth - mid) { | |
continue; | |
} | |
mpcnt++; | |
// search from mpidx to 0 | |
var visited = new Map(); | |
var size1 = 0; | |
var size2 = 0; | |
this.idaSearch(mpidx, mid, 1, [], this.sgsG.toKeyIdx(), this.prunTable, function(moves, permKey) { | |
var key = this.permHash(permKey); | |
size1++; | |
if (!visited.has(key)) { | |
visited.set(key, moves.slice()); | |
} | |
}.bind(this)); | |
//search from mpidx to pidx | |
this.idaSearch(mpidx, depth - mid, 1, [], this.sgsG.toKeyIdx(), prunTable2, function(moves, permKey) { | |
// mp * move2 = perm, mp * move1 = I => perm * move2' * move1 = I => move1 = move2 * perm' | |
var finalPermKey = this.sgsG.permMult(permKey, perm); | |
var key = this.permHash(finalPermKey); | |
size2++; | |
if (visited.has(key)) { | |
solution = []; | |
for (var i = 0; i < moves.length; i++) { | |
solution.push(this.genExMap[moves[moves.length - 1 - i]][2]); | |
} | |
Array.prototype.push.apply(solution, visited.get(key)); | |
return true; | |
} | |
}.bind(this)); | |
mpsizes.push([mpidx, size1, size2]); | |
s1tot += size1; | |
s2tot += size2; | |
if (solution) { | |
break; | |
} | |
} | |
DEBUG && console.log('[Subgroup Solver] dis ', s1tot + s2tot, 'node(s) checked at', depth, 'tt=', performance.now() - tt); | |
if (solution) { | |
break; | |
} | |
} | |
return solution; | |
} | |
SubgroupSolver.prototype.godsAlgo = function(depth) { | |
this.initTables(); | |
var stateCnt = 0; | |
for (var i = 0; i < this.idx2coset.length; i++) { | |
var perm = this.idx2coset[i]; | |
var visited = new Set(); | |
for (var maxl = 0; maxl <= depth; maxl++) { | |
this.idaSearch(i, maxl, 1, [], this.sgsG.toKeyIdx(), this.prunTable, function(moves, permKey) { | |
var key = this.permHash(permKey); | |
if (!visited.has(key)) { | |
stateCnt++; | |
visited.add(key); | |
} | |
}.bind(this)); | |
} | |
} | |
return stateCnt; | |
} | |
/* | |
SchreierSims.prototype.minkwitz = function() { | |
var words = []; | |
var maxl = 8; | |
var toFill = 0; | |
var newDelay = 3; | |
this.words = []; | |
this.isNew = []; | |
for (var i = 0; i < this.e.length; i++) { | |
this.words[i] = []; | |
this.words[i][i] = []; | |
this.isNew[i] = []; | |
for (var j = 0; j < i; j++) { | |
if (this.sgs[i][j] && !this.words[i][j]) { | |
this.words[i][j] = null; | |
toFill++; | |
} | |
} | |
} | |
this.invMap = {}; | |
for (var i = 0; i < this.gen.length; i++) { | |
var g = this.gen[i]; | |
for (var j = i; j < this.gen.length; j++) { | |
var isEq = true; | |
for (var k = 0; k < this.e.length; k++) { | |
if (g[this.gen[j][k]] != k) { | |
isEq = false; | |
break; | |
} | |
} | |
if (isEq) { | |
this.invMap[i] = j; | |
this.invMap[j] = i; | |
} | |
} | |
if (this.invMap[i] == undefined) { | |
this.invMap[i] = ~i; | |
this.invMap[~i] = i; | |
} | |
} | |
var addWords = function(p, words) { | |
var ret = -1; | |
for (var i = p.length - 1; i >= 0; i--) { | |
var j = p[i]; | |
if (!this.sgs[i][j]) { | |
return -2; | |
} | |
if (!this.words[i][j]) { | |
this.words[i][j] = words; | |
this.isNew[i][j] = newDelay; | |
this.sgs[i][j] = p; | |
this.sgsi[i][j] = this.permInv(p); | |
return 1; | |
} | |
if (words.length < this.words[i][j].length) { | |
var _p = this.sgs[i][j]; | |
this.sgs[i][j] = p; | |
this.sgsi[i][j] = this.permInv(p); | |
p = _p; | |
var _words = this.words[i][j]; | |
this.words[i][j] = words; | |
this.isNew[i][j] = newDelay; | |
words = _words; | |
ret = 0; | |
} | |
if (words.length + this.words[i][j].length > maxl) { | |
return ret; | |
} | |
p = this.permMult(p, this.sgsi[i][j]); | |
for (var k = this.words[i][j].length - 1; k >= 0; k--) { | |
words.push(this.invMap[this.words[i][j][k]]); | |
} | |
} | |
} | |
var iterGens = function(p, remain, func) { | |
if (remain <= 0) { | |
return func.call(this, p, words); | |
} | |
for (var i = 0; i < this.gen.length && toFill > 0; i++) { | |
words.push(i); | |
var ret = iterGens.call(this, this.permMult(p, this.gen[i]), remain - 1, func); | |
words.pop(); | |
if (ret < 0) { // no improve | |
continue; | |
} | |
words.push(this.invMap[i]); | |
iterGens.call(this, this.permMult(p, this.permInv(this.gen[i])), remain - 1, func); | |
words.pop(); | |
} | |
} | |
var improve = function() { | |
var n = 0; | |
var newCnt = 0; | |
for (var i1 = 0; i1 < this.e.length; i1++) { | |
for (var j1 = 0; j1 < i1; j1++) { | |
if (this.isNew[i1][j1] > 0) { | |
this.isNew[i1][j1]--; | |
} | |
if (this.isNew[i1][j1]) { | |
newCnt++; | |
} | |
} | |
} | |
console.log('newCnt', newCnt); | |
for (var i1 = 0; i1 < this.e.length; i1++) { | |
var isFilled = true; | |
for (var j1 = 0; j1 < i1; j1++) { | |
if (this.sgs[i1][j1] && !this.words[i1][j1]) { | |
isFilled = false; | |
break; | |
} | |
} | |
for (var j1 = 0; j1 < i1; j1++) { | |
if (!this.words[i1][j1]) { | |
continue; | |
} | |
for (var i2 = i1; i2 < this.e.length; i2++) { | |
if (isFilled && i1 != i2) { | |
continue; | |
} | |
for (var j2 = (i1 == i2 ? j1 : 0); j2 < i2; j2++) { | |
if (!this.words[i2][j2]) { | |
continue; | |
} | |
var cuml = this.words[i1][j1].length + this.words[i2][j2].length; | |
if (cuml > maxl) { | |
continue; | |
} | |
if (this.isNew[i1][j1] == 0 && this.isNew[i2][j2] == 0 && i1 == i2) { | |
continue; | |
} | |
var cc = this.sgs[i1][j1][this.sgs[i2][j2][i1]]; | |
if (this.words[i1][cc] && this.words[i1][cc].length < cuml * 1.5 && i1 != i2) { | |
continue; | |
} | |
var ret = addWords.call(this, | |
this.permMult(this.sgs[i2][j2], this.sgs[i1][j1]), | |
this.words[i2][j2].concat(this.words[i1][j1]) | |
); | |
if (ret > -1) { | |
n++; | |
} | |
if (ret > 0) { | |
toFill--; | |
} | |
// console.log(i1, i2, ret); | |
} | |
} | |
} | |
} | |
return n; | |
} | |
var start = $.now(); | |
var cnt = 0; | |
for (var i = 1; i < 100 && toFill > 0; i++) { | |
iterGens.call(this, this.e, i, function(p, words) { | |
var ret = addWords.call(this, p, words.slice()); | |
cnt++; | |
if (ret > 0) { | |
toFill--; | |
} | |
if (cnt % 1000 == 0) { | |
var ret2 = improve.call(this); | |
maxl = Math.round(maxl * 1.25); | |
console.log(ret2, toFill, maxl); | |
} | |
return ret; | |
}); | |
} | |
console.log('final', $.now() - start); | |
improve.call(this); | |
console.log('init minkwitz', $.now() - start); | |
window.sgs1 = this; | |
} | |
SchreierSims.prototype.getGen = function(p) { | |
var ret = []; | |
for (var i = p.length - 1; i >= 0; i--) { | |
var j = p[i]; | |
if (!this.sgs[i][j]) { | |
return null; | |
} | |
if (j !== i) { | |
p = this.permMult(p, this.sgsi[i][j]); | |
ret.push(this.words[i][j]); | |
} | |
} | |
return ret.reverse(); | |
} | |
SchreierSims.prototype.intersect = function(other, thres) { | |
if (this.size() > other.size()) { | |
return other.intersect(this, thres); | |
} | |
thres = thres || 100000; | |
var ret = new SchreierSims([this.sgs[0][0]]); | |
var n = this.sgs.length; | |
ret.cnt = 0; | |
for (var i = 0; i < n; i++) { | |
for (var j = 0; j < i; j++) { | |
if (!this.sgs[i][j] || ret.sgs[i][j]) { | |
continue; | |
} | |
// console.log(i, j); | |
this.enumDFS(i - 1, this.sgs[i][j], function(perm) { | |
ret.knutha(n - 1, perm); | |
// console.log(i, j, ret.size(), perm); | |
return true; | |
}, function(depth, perm) { | |
if (ret.cnt > thres || ret.cnt == -1) { | |
ret.cnt = -1; | |
return false; | |
} | |
ret.cnt++; | |
var mchk = other.isMember(perm, depth); | |
if (!mchk) { | |
return false; | |
} | |
for (var i = 0; i < ret.sgs[depth].length - 1; i++) { | |
if (ret.sgs[depth][i]) { | |
var pp = ret.permMult(perm, ret.sgs[depth][i]); | |
if (pp[depth] < perm[depth]) { | |
return false; | |
} | |
} | |
} | |
return true; | |
}); | |
if (ret.cnt == -1) { | |
return ret; | |
} | |
} | |
} | |
return ret; | |
} | |
SchreierSims.prototype.enumDFS = function(depth, perm, callback, checkFunc) { | |
if (checkFunc && !checkFunc(depth + 1, perm)) { | |
return; | |
} | |
if (depth == 0) { | |
return callback(perm); | |
} | |
for (var j = 0; j <= depth; j++) { | |
if (this.sgs[depth][j]) { | |
var ret = this.enumDFS(depth - 1, this.permMult(this.sgs[depth][j], perm), callback, checkFunc); | |
if (ret) { | |
// console.log(depth, j, this.sgs[depth][j]) | |
return ret; | |
} | |
} | |
} | |
} | |
SchreierSims.prototype.enum = function(callback) { | |
this.enumDFS(this.sgs.length - 1, this.sgs[0][0], callback); | |
} | |
*/ | |
function createPrun(prun, init, size, maxd, doMove, N_MOVES, N_POWER, N_INV) { | |
var isMoveTable = $.isArray(doMove); | |
N_MOVES = N_MOVES || 6; | |
N_POWER = N_POWER || 3; | |
N_INV = N_INV || 256; | |
maxd = maxd || 256; | |
for (var i = 0, len = (size + 7) >>> 3; i < len; i++) { | |
prun[i] = -1; | |
} | |
prun[init >> 3] ^= 15 << ((init & 7) << 2); | |
var val = 0; | |
// var t = +new Date; | |
for (var l = 0; l <= maxd; l++) { | |
var done = 0; | |
var inv = l >= N_INV; | |
var fill = (l + 1) ^ 15; | |
var find = inv ? 0xf : l; | |
var check = inv ? l : 0xf; | |
out: for (var p = 0; p < size; p++, val >>= 4) { | |
if ((p & 7) == 0) { | |
val = prun[p >> 3]; | |
if (!inv && val == -1) { | |
p += 7; | |
continue; | |
} | |
} | |
if ((val & 0xf) != find) { | |
continue; | |
} | |
for (var m = 0; m < N_MOVES; m++) { | |
var q = p; | |
for (var c = 0; c < N_POWER; c++) { | |
q = isMoveTable ? doMove[m][q] : doMove(q, m); | |
if (getPruning(prun, q) != check) { | |
continue; | |
} | |
++done; | |
if (inv) { | |
prun[p >> 3] ^= fill << ((p & 7) << 2); | |
continue out; | |
} | |
prun[q >> 3] ^= fill << ((q & 7) << 2); | |
} | |
} | |
} | |
if (done == 0) { | |
break; | |
} | |
DEBUG && console.log('[prun]', done); | |
} | |
} | |
//state_params: [[init, doMove, size, [maxd], [N_INV]], [...]...] | |
function Solver(N_MOVES, N_POWER, state_params) { | |
this.N_STATES = state_params.length; | |
this.N_MOVES = N_MOVES; | |
this.N_POWER = N_POWER; | |
this.state_params = state_params; | |
this.inited = false; | |
} | |
var _ = Solver.prototype; | |
_.search = function(state, minl, MAXL) { | |
MAXL = (MAXL || 99) + 1; | |
if (!this.inited) { | |
this.move = []; | |
this.prun = []; | |
for (var i = 0; i < this.N_STATES; i++) { | |
var state_param = this.state_params[i]; | |
var init = state_param[0]; | |
var doMove = state_param[1]; | |
var size = state_param[2]; | |
var maxd = state_param[3]; | |
var N_INV = state_param[4]; | |
this.move[i] = []; | |
this.prun[i] = []; | |
createMove(this.move[i], size, doMove, this.N_MOVES); | |
createPrun(this.prun[i], init, size, maxd, this.move[i], this.N_MOVES, this.N_POWER, N_INV); | |
} | |
this.inited = true; | |
} | |
this.sol = []; | |
for (var maxl = minl; maxl < MAXL; maxl++) { | |
if (this.idaSearch(state, maxl, -1)) { | |
break; | |
} | |
} | |
return maxl == MAXL ? null : this.sol.reverse(); | |
}; | |
_.toStr = function(sol, move_map, power_map) { | |
var ret = []; | |
for (var i = 0; i < sol.length; i++) { | |
ret.push(move_map[sol[i][0]] + power_map[sol[i][1]]); | |
} | |
return ret.join(' ').replace(/ +/g, ' '); | |
}; | |
_.idaSearch = function(state, maxl, lm) { | |
var N_STATES = this.N_STATES; | |
for (var i = 0; i < N_STATES; i++) { | |
if (getPruning(this.prun[i], state[i]) > maxl) { | |
return false; | |
} | |
} | |
if (maxl == 0) { | |
return true; | |
} | |
var offset = state[0] + maxl + lm + 1; | |
for (var move0 = 0; move0 < this.N_MOVES; move0++) { | |
var move = (move0 + offset) % this.N_MOVES; | |
if (move == lm) { | |
continue; | |
} | |
var cur_state = state.slice(); | |
for (var power = 0; power < this.N_POWER; power++) { | |
for (var i = 0; i < N_STATES; i++) { | |
cur_state[i] = this.move[i][move][cur_state[i]]; | |
} | |
if (this.idaSearch(cur_state, maxl - 1, move)) { | |
this.sol.push([move, power]); | |
return true; | |
} | |
} | |
} | |
return false; | |
}; | |
// state: string not null | |
// solvedStates: [solvedstate, solvedstate, ...], string not null | |
// moveFunc: function(state, move); | |
// moves: {move: face0 | axis0}, face0 | axis0 = 4 + 4 bits | |
function gSolver(solvedStates, doMove, moves) { | |
this.solvedStates = solvedStates; | |
this.doMove = doMove; | |
this.movesList = []; | |
for (var move in moves) { | |
this.movesList.push([move, moves[move]]); | |
} | |
this.prunTable = {}; | |
this.toUpdateArr = null; | |
this.prunTableSize = 0; | |
this.prunDepth = -1; | |
this.cost = 0; | |
this.MAX_PRUN_SIZE = 100000; | |
} | |
_ = gSolver.prototype; | |
/* | |
_.calcNumOfStates = function() { | |
var len = this.solvedStates[0].length; | |
var genMove = []; | |
for (var moveIdx = 0; moveIdx < this.movesList.length; moveIdx++) { | |
var state = []; | |
for (var i = 0; i < len; i++) { | |
state.push(i + 32); | |
} | |
var newState = this.doMove(String.fromCharCode.apply(null, state), this.movesList[moveIdx][0]); | |
if (!newState) { | |
continue; | |
} | |
for (var i = 0; i < len; i++) { | |
state[i] = newState.charCodeAt(i) - 32; | |
} | |
genMove.push(state); | |
} | |
console.log(genMove); | |
var sgsObj = new SchreierSims(genMove); | |
console.log(sgsObj.size()); | |
return sgsObj; | |
var genColor = []; | |
var state = this.solvedStates[0]; | |
var e = []; | |
for (var i = 0; i < len; i++) { | |
e[i] = i; | |
} | |
var checked = []; | |
for (var i = 0; i < len; i++) { | |
if (checked[i]) { | |
continue; | |
} | |
for (var j = i + 1; j < len; j++) { | |
if (state[i] == state[j] && (i % 9 % 2) == (j % 9 % 2)) { | |
var perm = e.slice(); | |
perm[i] = j; | |
perm[j] = i; | |
checked[j] = 1; | |
genColor.push(perm); | |
} | |
} | |
} | |
var sgsObj = new SchreierSims(genMove); | |
sgsObj.minkwitz(); | |
var perm = e.slice(); | |
var initMv = []; | |
for (var i = 0; i < 50; i++) { | |
var mv = rn(genMove.length); | |
perm = sgsObj.permMult(genMove[mv], perm); | |
initMv.push(sgsObj.invMap[mv]); | |
} | |
var sol = sgsObj.getGen(perm); | |
var move2str = function(v) { return "URFDLB"[~~(v/3)] + " 2'"[v%3]; }; | |
sol = $.map(Array.prototype.concat.apply([], sol).reverse(), move2str).join(' '); | |
console.log($.map(initMv.reverse(), move2str).join(' '), '\n', sol); | |
var sgs0, sgs1, sgs01; | |
for (var r = 0; r < 100; r++) { | |
var shuffle = []; | |
for (var i = 0; i < len; i++) { | |
shuffle[i] = i; | |
} | |
for (var i = 0; i < len; i++) { | |
var j = ~~(Math.random() * (len - i)) + i; | |
var tmp = shuffle[i]; | |
shuffle[i] = shuffle[j]; | |
shuffle[j] = tmp; | |
} | |
sgs0 = new SchreierSims(genColor, shuffle); | |
sgs1 = new SchreierSims(genMove, shuffle); | |
sgs01 = sgs0.intersect(sgs1); | |
if (sgs01.cnt != -1) { | |
console.log(r); | |
break; | |
} | |
} | |
console.log(sgs01.cnt, sgs0.size(), sgs1.size(), sgs01.size(), sgs1.size() / sgs01.size()); | |
}; | |
*/ | |
_.updatePrun = function(targetDepth) { | |
targetDepth = targetDepth === undefined ? this.prunDepth + 1 : targetDepth; | |
for (var depth = this.prunDepth + 1; depth <= targetDepth; depth++) { | |
if (this.prevSize >= this.MAX_PRUN_SIZE) { | |
DEBUG && console.log('[gSolver] skipPrun', depth, this.prunTableSize); | |
break; | |
} | |
var t = +new Date; | |
if (depth < 1) { | |
this.prevSize = 0; | |
for (var i = 0; i < this.solvedStates.length; i++) { | |
var state = this.solvedStates[i]; | |
if (!(state in this.prunTable)) { | |
this.prunTable[state] = depth; | |
this.prunTableSize++; | |
} | |
} | |
} else { | |
this.updatePrunBFS(depth - 1); | |
} | |
if (this.cost == 0) { | |
return; | |
} | |
this.prunDepth = depth; | |
DEBUG && console.log('[gSolver] updatePrun', depth, this.prunTableSize - this.prevSize, +new Date - t); | |
this.prevSize = this.prunTableSize; | |
} | |
}; | |
_.updatePrunBFS = function(fromDepth) { | |
if (this.toUpdateArr == null) { | |
this.toUpdateArr = []; | |
for (var state in this.prunTable) { | |
if (this.prunTable[state] != fromDepth) { | |
continue; | |
} | |
this.toUpdateArr.push(state); | |
} | |
} | |
while (this.toUpdateArr.length != 0) { | |
var state = this.toUpdateArr.pop(); | |
for (var moveIdx = 0; moveIdx < this.movesList.length; moveIdx++) { | |
var newState = this.doMove(state, this.movesList[moveIdx][0]); | |
if (!newState || newState in this.prunTable) { | |
continue; | |
} | |
this.prunTable[newState] = fromDepth + 1; | |
this.prunTableSize++; | |
} | |
if (this.cost >= 0) { | |
if (this.cost == 0) { | |
return; | |
} | |
this.cost--; | |
} | |
} | |
this.toUpdateArr = null; | |
}; | |
_.search = function(state, minl, MAXL) { | |
this.sol = []; | |
this.subOpt = false; | |
this.state = state; | |
this.visited = {}; | |
this.maxl = minl = minl || 0; | |
return this.searchNext(MAXL); | |
}; | |
_.searchNext = function(MAXL, cost) { | |
MAXL = (MAXL + 1) || 99; | |
this.prevSolStr = this.solArr ? this.solArr.join(',') : null; | |
this.solArr = null; | |
this.cost = cost || -1; | |
for (; this.maxl < MAXL; this.maxl++) { | |
this.updatePrun(Math.ceil(this.maxl / 2)); | |
if (this.cost == 0) { | |
return null; | |
} | |
if (this.idaSearch(this.state, this.maxl, null, 0)) { | |
break; | |
} | |
} | |
return this.solArr; | |
} | |
_.getPruning = function(state) { | |
var prun = this.prunTable[state]; | |
return prun === undefined ? this.prunDepth + 1 : prun; | |
}; | |
_.idaSearch = function(state, maxl, lm, depth) { | |
if (this.getPruning(state) > maxl) { | |
return false; | |
} | |
if (maxl == 0) { | |
if (this.solvedStates.indexOf(state) == -1) { | |
return false; | |
} | |
var solArr = this.getSolArr(); | |
this.subOpt = true; | |
if (solArr.join(',') == this.prevSolStr) { | |
return false; | |
} | |
this.solArr = solArr; | |
return true; | |
} | |
if (!this.subOpt) { | |
if (state in this.visited && this.visited[state] < depth) { | |
return false; | |
} | |
this.visited[state] = depth; | |
} | |
if (this.cost >= 0) { | |
if (this.cost == 0) { | |
return true; | |
} | |
this.cost--; | |
} | |
var lastMove = lm == null ? '' : this.movesList[lm][0]; | |
var lastAxisFace = lm == null ? -1 : this.movesList[lm][1]; | |
for (var moveIdx = this.sol[depth] || 0; moveIdx < this.movesList.length; moveIdx++) { | |
var moveArgs = this.movesList[moveIdx]; | |
var axisface = moveArgs[1] ^ lastAxisFace; | |
var move = moveArgs[0]; | |
if (axisface == 0 || | |
(axisface & 0xf) == 0 && move <= lastMove) { | |
continue; | |
} | |
var newState = this.doMove(state, move); | |
if (!newState || newState == state) { | |
continue; | |
} | |
this.sol[depth] = moveIdx; | |
if (this.idaSearch(newState, maxl - 1, moveIdx, depth + 1)) { | |
return true; | |
} | |
this.sol.pop(); | |
} | |
return false; | |
}; | |
_.getSolArr = function() { | |
var solArr = []; | |
for (var i = 0; i < this.sol.length; i++) { | |
solArr.push(this.movesList[this.sol[i]][0]); | |
} | |
return solArr; | |
} | |
function rndPerm(n, isEven) { | |
var p = 0; | |
var arr = []; | |
for (var i = 0; i < n; i++) { | |
arr[i] = i; | |
} | |
for (var i = 0; i < n - 1; i++) { | |
var k = rn(n - i); | |
circle(arr, i, i + k); | |
p ^= k != 0; | |
} | |
if (isEven && p) { | |
circle(arr, 0, 1); | |
} | |
return arr; | |
} | |
function time2str(unix, format) { | |
if (!unix) { | |
return 'N/A'; | |
} | |
format = format || '%Y-%M-%D %h:%m:%s'; | |
var date = new Date(unix * 1000); | |
return format | |
.replace('%Y', date.getFullYear()) | |
.replace('%M', ('0' + (date.getMonth() + 1)).slice(-2)) | |
.replace('%D', ('0' + date.getDate()).slice(-2)) | |
.replace('%h', ('0' + date.getHours()).slice(-2)) | |
.replace('%m', ('0' + date.getMinutes()).slice(-2)) | |
.replace('%s', ('0' + date.getSeconds()).slice(-2)); | |
} | |
var timeRe = /^\s*(\d+)-(\d+)-(\d+) (\d+):(\d+):(\d+)\s*$/; | |
function str2time(val) { | |
var m = timeRe.exec(val); | |
if (!m) { | |
return null; | |
} | |
var date = new Date(0); | |
date.setFullYear(~~m[1]); | |
date.setMonth(~~m[2] - 1); | |
date.setDate(~~m[3]); | |
date.setHours(~~m[4]); | |
date.setMinutes(~~m[5]); | |
date.setSeconds(~~m[6]); | |
return ~~(date.getTime() / 1000); | |
} | |
function obj2str(val) { | |
if (typeof val == 'string') { | |
return val; | |
} | |
return JSON.stringify(val); | |
} | |
function str2obj(val) { | |
if (typeof val != 'string') { | |
return val; | |
} | |
return JSON.parse(val); | |
} | |
function valuedArray(len, val) { | |
var ret = []; | |
for (var i = 0; i < len; i++) { | |
ret[i] = val; | |
} | |
return ret; | |
} | |
function idxArray(arr, idx) { | |
var ret = []; | |
for (var i = 0; i < arr.length; i++) { | |
ret.push(arr[i][idx]); | |
} | |
return ret; | |
} | |
Math.TAU = Math.PI * 2; | |
return { | |
Cnk: Cnk, | |
fact: fact, | |
getPruning: getPruning, | |
setNPerm: setNPerm, | |
getNPerm: getNPerm, | |
getNParity: getNParity, | |
get8Perm: get8Perm, | |
set8Perm: set8Perm, | |
coord: coord, | |
createMove: createMove, | |
edgeMove: edgeMove, | |
circle: circle, | |
circleOri: circleOri, | |
acycle: acycle, | |
SchreierSims: SchreierSims, | |
SubgroupSolver: SubgroupSolver, | |
createPrun: createPrun, | |
CubieCube: CubieCube, | |
minx: minx, | |
SOLVED_FACELET: "UUUUUUUUURRRRRRRRRFFFFFFFFFDDDDDDDDDLLLLLLLLLBBBBBBBBB", | |
fillFacelet: fillFacelet, | |
time2str: time2str, | |
str2time: str2time, | |
obj2str: obj2str, | |
str2obj: str2obj, | |
valuedArray: valuedArray, | |
idxArray: idxArray, | |
Solver: Solver, | |
gSolver: gSolver | |
}; | |
})(); | |
export default mathlib; |