Compare commits

...

20 commits

Author SHA1 Message Date
ferricles
9aa9d871b8 Merge branch 'master' into sock_encoding 2022-12-20 15:09:18 -08:00
ferricles
f9b98ef7e4 BitVector fixes w/ unit tests + unit testing page (/unit_tests/) 2022-12-20 15:08:55 -08:00
ferricles
420e66842d parse hash 2022-12-20 11:46:40 -08:00
ferricles
7b295e4e46 merge conflict 2022-12-20 11:11:30 -08:00
ferricles
9b8564261a pre-merge commit 2022-12-20 11:05:38 -08:00
ferricles
b5e950b8a5 preparing to merge 2022-12-20 11:04:19 -08:00
ferricles
07ad16934b Merge branch 'atree' into sock_encoding 2022-07-26 11:37:25 -07:00
ferricles
31fd1a2f01 change Build internal item repr to include separate tomes array, allow bitvec to append using other bitvec (untested) 2022-07-26 11:37:08 -07:00
ferricles
d76ad45b9c Merge branch 'atree' into sock_encoding 2022-07-21 16:24:24 -07:00
ferricles
c65da33ccd sock encoding build.encode started, bitvec append allow appending bitvecs started 2022-07-21 16:24:03 -07:00
ferricles
99d2b8273a fix merge conflicts 2022-07-20 14:53:35 -07:00
ferricles
ec241891fc append bug fix + unit tests 2022-07-06 21:40:50 -07:00
ferricles
80b65eb0e4 continue bitvec unit testing, changed all asserts to console.trace instead of errors so that future tests are run too and TEST_FAIL constant can be returned 2022-07-06 21:25:04 -07:00
ferricles
c6cc5bfc24 begin writing bitvec unit tests + return constants for unit tests 2022-07-06 20:27:04 -07:00
ferricles
e8a9439b64 Merge branch 'atree' into sock_encoding 2022-07-04 22:25:37 -07:00
ferricles
73a7cec2ed Merge branch 'atree' into sock_encoding 2022-07-04 16:29:21 -07:00
ferricles
458c18297d array fill bug fix 2022-07-04 10:08:44 -07:00
ferricles
e956d28c56 fixed merge conflict 2022-07-04 00:21:38 -07:00
ferricles
1486a8befa add recursion in size check to reduce code duplication 2022-07-04 00:16:15 -07:00
ferricles
1eeabc2c08 updated append(), not tested 2022-07-03 23:42:29 -07:00
8 changed files with 356 additions and 110 deletions

View file

@ -20,7 +20,6 @@ class Build{
* @param {Item} weapon: Weapon that this build is using.
*/
constructor(level, items, weapon){
if (level < 1) { //Should these be constants?
this.level = 1;
} else if (level > 106) {

View file

@ -232,79 +232,119 @@ async function parse_hash(url_tag) {
/* Stores the entire build in a string using B64 encoding and adds it to the URL.
*/
function encodeBuild(build, powders, skillpoints, atree, atree_state) {
//currently on version 8 - a unified version for all build types using bit-level encoding
if (build) {
let build_string;
//V6 encoding - Tomes
//V7 encoding - ATree
//V8 encoding - wynn version
//final link will be [build_vers]_[len_string]_[build_string]
build_version = 8;
build_string = "";
tome_string = "";
let len_string = "";
let build_string = "";
let build_bits = new BitVector(0, 0);
//ITEMS
for (const item of build.items) {
if (item.statMap.get("custom")) {
let custom = "CI-"+encodeCustom(item, true);
build_string += Base64.fromIntN(custom.length, 3) + custom;
if (item.statMap.get("NONE") && item.statMap.get("NONE") === true) {
build_bits.append(0, 2); //00
} else if (item.statMap.get("custom")) {
build_bits.append(3, 2); //11
//BitVector CI encoding TODO
// let custom = "CI-"+encodeCustom(item, true);
// build_string += Base64.fromIntN(custom.length, 3) + custom;
// build_version = Math.max(build_version, 5);
} else if (item.statMap.get("crafted")) {
build_string += "CR-"+encodeCraft(item);
} else if (item.statMap.get("category") === "tome") {
let tome_id = item.statMap.get("id");
if (tome_id <= 60) {
// valid normal tome. ID 61-63 is for NONE tomes.
//build_version = Math.max(build_version, 6);
}
tome_string += Base64.fromIntN(tome_id, 2);
build_bits.append(2, 2); //10
//BitVector CR encoding TODO
// build_string += "CR-"+encodeCraft(item);
} else {
build_string += Base64.fromIntN(item.statMap.get("id"), 3);
if (item.statMap.get("category") === "tome") {
//we will encode tomes later
continue;
} else {
build_bits.append(1, 2); //01
build_bits.append(item.statMap.get("id"), 13);
//powderable
if (powderable_keys.includes(item.statMap.get("type"))) {
if (item.statMap.get("powders") && item.statMap.get("powders").length !== 0) {
//has powders
build_bits.append(1, 1);
//num of powders in 8 bits, then each powder (6 bits)
//Having more than 256 powders on a vanilla item is NOT HANDLED.
build_bits.append(item.statMap.get("powders").length, 8);
for (const powder of item.statMap.get("powders")) {
build_bits.append(powder, 6);
}
} else {
//no powders
build_bits.append(0, 1);
}
}
}
}
}
for (const skp of skillpoints) {
build_string += Base64.fromIntN(skp, 2); // Maximum skillpoints: 2048
}
build_string += Base64.fromIntN(build.level, 2);
for (const _powderset of powders) {
let n_bits = Math.ceil(_powderset.length / 6);
build_string += Base64.fromIntN(n_bits, 1); // Hard cap of 378 powders.
// Slice copy.
let powderset = _powderset.slice();
while (powderset.length != 0) {
let firstSix = powderset.slice(0,6).reverse();
let powder_hash = 0;
for (const powder of firstSix) {
powder_hash = (powder_hash << 5) + 1 + powder; // LSB will be extracted first.
}
build_string += Base64.fromIntN(powder_hash, 5);
powderset = powderset.slice(6);
}
}
build_string += tome_string;
//SKILL POINTS
//the original schema included a flag to indicate whether or not skill points are included.
//any reason for having a flag isn't implemented yet, so for now every build will have the skill point flag set.
build_bits.append(1, 1);
for (const skp of build.base_skillpoints) {
build_bits.append(skp, 8); // Maximum skillpoints: 255 (allows for manual assign up to 150)
}
//BUILD LEVEL
// [flag to indicate if level is not 106 (0/1)]
// [else: level (7 bits, allows for lv 1->127)]
if (player_build.level != 106) {
build_bits.append(1, 1);
build_bits.append(player_build.level, 7);
} else {
build_bits.append(0, 1);
}
// TOMES
// [flag to indicate if tomes are included (0/1)]
// [if set: 7 sequential tome IDs, each 6 bits unsigned]
if (build.tomes.length > 0) {
build_bits.append(1, 1);
//decoding will assume that tomes has length of 7.
for (const tome of build.tomes) {
build_bits.append(tome.id, 6);
}
} else {
build_bits.append(0, 1);
}
// ATREE
// [flag to indicate if atree data is present]
// [atree data: see existing encoding impl] //idk the impl
if (atree.length > 0 && atree_state.get(atree[0].ability.id).active) {
//build_version = Math.max(build_version, 7);
const bitvec = encode_atree(atree, atree_state);
build_string += bitvec.toB64();
build_bits.append(1, 1);
const atree_bitvec = encode_atree(atree, atree_state);
build_bits.append(atree_bitvec);
} else {
build_bits.append(0, 1);
}
return build_version.toString() + "_" + build_string;
//compute length and return final build hash
return build_version.toString() + "_" + len_string + "_" + build_string;
}
}
function get_full_url() {
return `${url_base}?v=${wynn_version_id.toString()}${location.hash}`
}
function copyBuild() {
copyTextToClipboard(get_full_url());
copyTextToClipboard(url_base+location.hash);
document.getElementById("copy-button").textContent = "Copied!";
}
function shareBuild(build) {
if (build) {
let text = get_full_url()+"\n"+
let text = url_base+location.hash+"\n"+
"WynnBuilder build:\n"+
"> "+build.items[0].statMap.get("displayName")+"\n"+
"> "+build.items[1].statMap.get("displayName")+"\n"+
@ -314,13 +354,7 @@ function shareBuild(build) {
"> "+build.items[5].statMap.get("displayName")+"\n"+
"> "+build.items[6].statMap.get("displayName")+"\n"+
"> "+build.items[7].statMap.get("displayName")+"\n"+
"> "+build.items[15].statMap.get("displayName")+" ["+build_powders[4].map(x => powderNames.get(x)).join("")+"]\n";
for (let tomeslots = 8; tomeslots < 15; tomeslots++) {
if (!build.items[tomeslots].statMap.has('NONE')) {
text += ">"+' (Has Tomes)' ;
break;
}
}
"> "+build.items[15].statMap.get("displayName")+" ["+build_powders[4].map(x => powderNames.get(x)).join("")+"]";
copyTextToClipboard(text);
document.getElementById("share-button").textContent = "Copied!";
}

View file

@ -428,7 +428,7 @@ class BuildAssembleNode extends ComputeNode {
if (all_none && !location.hash) {
return null;
}
return new Build(level, equipments, weapon);
return new Build(level, equipments, tomes, weapon);
}
}

View file

@ -172,7 +172,13 @@ Base64 = (function () {
}
}
} else if (typeof data === "number") {
if (typeof length === "undefined")
if (typeof length === "undefined") {
if (data == 0) {
length = 0;
} else {
length = Math.ceil(Math.log(data + 1) / Math.log(2)); //+1 to account for powers of 2
}
}
if (length < 0) {
throw new RangeError("BitVector must have nonnegative length.");
}
@ -189,6 +195,10 @@ Base64 = (function () {
throw new TypeError("BitVector must be instantiated with a Number or a B64 String");
}
if (bit_vec.length == 0) {
bit_vec = [0];
}
this.length = length;
this.bits = new Uint32Array(bit_vec);
}
@ -201,7 +211,7 @@ Base64 = (function () {
*/
read_bit(idx) {
if (idx < 0 || idx >= this.length) {
throw new RangeError("Cannot read bit outside the range of the BitVector. ("+idx+" > "+this.length+")");
throw new RangeError("Cannot read bit outside the range of the BitVector. ("+idx+" >= "+this.length+")");
}
return ((this.bits[Math.floor(idx / 32)] & (1 << idx)) == 0 ? 0 : 1);
}
@ -311,46 +321,51 @@ Base64 = (function () {
/** Appends data to the BitVector.
*
* @param {Number | String} data - The data to append.
* @param {Number} length - The length, in bits, of the new data. This is ignored if data is a string.
* @param {Number | String | BitVector} data - The data to append.
* @param {Number} length - The length, in bits, of the new data. This is ignored if data is a string. Defaults to 32 for numbers.
*/
append(data, length) {
append(data, length = 32) {
if (length < 0) {
throw new RangeError("BitVector length must increase by a nonnegative number.");
}
let bit_vec = [];
for (const uint of this.bits) {
bit_vec.push(uint);
}
//actual new data length is needed for resizing purposes
if (typeof data === "string") {
let int = bit_vec[bit_vec.length - 1];
let bv_idx = this.length;
length = data.length * 6;
let updated_curr = false;
for (let i = 0; i < data.length; i++) {
let char = Base64.toInt(data[i]);
let pre_pos = bv_idx % 32;
int |= (char << bv_idx);
bv_idx += 6;
let post_pos = bv_idx % 32;
if (post_pos < pre_pos) { //we have to have filled up the integer
if (bit_vec.length == this.bits.length && !updated_curr) {
bit_vec[bit_vec.length - 1] = int;
updated_curr = true;
} else {
bit_vec.push(int);
}
int = (char >>> (6 - post_pos));
} else if (data instanceof BitVector) {
length = data.length;
}
if (i == data.length - 1) {
if (bit_vec.length == this.bits.length && !updated_curr) {
bit_vec[bit_vec.length - 1] = int;
} else if (post_pos != 0) {
bit_vec.push(int);
let new_length = this.length + length;
if (this.bits.length * this.bits.BYTES_PER_ELEMENT * 8 < new_length) {
//resize the internal repr by a factor of 2 before recursive calling
let bit_vec = Array(2 * this.bits.length).fill(0);
for (let i = 0; i < this.bits.length; i++) {
bit_vec[i] = this.bits[i];
}
this.bits = new Uint32Array(bit_vec);
return this.append(data, length);
}
//just write to the original bitvec
let curr_idx = Math.floor(this.length / 32);
let pos = this.length;
if (typeof data === "string") {
//daily reminder that shifts are modded by 32
for (const character of data) {
let char = Base64.toInt(character);
this.bits[curr_idx] |= (char << pos);
//if we go to the "next" char, update it
if (Math.floor(pos / 32) < Math.floor((pos + 5) / 32)) {
this.bits[curr_idx + 1] |= (char >>> (6 - (pos + 6) % 32));
}
//update counters
pos += 6;
curr_idx = Math.floor(pos / 32);
}
} else if (typeof data === "number") {
//convert to int just in case
@ -362,15 +377,36 @@ Base64 = (function () {
}
//could be split between multiple new ints
//reminder that shifts implicitly mod 32
bit_vec[bit_vec.length - 1] |= ((int & ~((~0) << length)) << (this.length));
if (((this.length - 1) % 32 + 1) + length > 32) {
bit_vec.push(int >>> (32 - this.length));
if (length == 32) {
this.bits[curr_idx] |= int << (this.length);
} else {
this.bits[curr_idx] |= ((int & ~((~0) << length)) << (this.length));
}
//overflow part
if ((pos % 32) + length > 32) {
this.bits[curr_idx + 1] = (int >>> (32 - this.length));
}
} else if (data instanceof BitVector) {
//fill to end of curr int of existing bv
let other_pos = (32 - (pos % 32));
this.bits[curr_idx] |= data.slice(0, other_pos);
curr_idx += 1;
//fill full ints
while (other_pos + 32 < data.length) {
this.bits[curr_idx] = data.slice(other_pos, other_pos + 32);
curr_idx += 1;
other_pos += 32;
}
//fill from "rest of" length/bv
this.bits[curr_idx] = data.slice(other_pos, data.length);
} else {
throw new TypeError("BitVector must be appended with a Number or a B64 String");
}
this.bits = new Uint32Array(bit_vec);
//update length
this.length += length;
}
};
@ -661,6 +697,9 @@ const getScript = url => new Promise((resolve, reject) => {
/*
GENERIC TEST FUNCTIONS
*/
const TEST_SUCCESS = 1;
const TEST_FAIL = 0;
/** The generic assert function. Fails on all "false-y" values. Useful for non-object equality checks, boolean value checks, and existence checks.
*
* @param {*} arg - argument to assert.
@ -668,8 +707,10 @@ GENERIC TEST FUNCTIONS
*/
function assert(arg, msg) {
if (!arg) {
throw new Error(msg ? msg : "Assert failed.");
console.trace(msg ? msg : "Assert failed.");
return TEST_FAIL;
}
return TEST_SUCCESS;
}
/** Asserts object equality of the 2 parameters. For loose and strict asserts, use assert().
@ -680,8 +721,10 @@ GENERIC TEST FUNCTIONS
*/
function assert_equals(arg1, arg2, msg) {
if (!Object.is(arg1, arg2)) {
throw new Error(msg ? msg : "Assert Equals failed. " + arg1 + " is not " + arg2 + ".");
console.trace(msg ? msg : "Assert Equals failed. " + arg1 + " is not " + arg2 + ".");
return TEST_FAIL;
}
return TEST_SUCCESS;
}
/** Asserts object inequality of the 2 parameters. For loose and strict asserts, use assert().
@ -692,8 +735,10 @@ function assert_equals(arg1, arg2, msg) {
*/
function assert_not_equals(arg1, arg2, msg) {
if (Object.is(arg1, arg2)) {
throw new Error(msg ? msg : "Assert Not Equals failed. " + arg1 + " is " + arg2 + ".");
console.trace(msg ? msg : "Assert Not Equals failed. " + arg1 + " is " + arg2 + ".");
return TEST_FAIL;
}
return TEST_SUCCESS;
}
/** Asserts proximity between 2 arguments. Should be used for any floating point datatype.
@ -705,8 +750,10 @@ function assert_equals(arg1, arg2, msg) {
*/
function assert_near(arg1, arg2, epsilon = 1E-5, msg) {
if (Math.abs(arg1 - arg2) > epsilon) {
throw new Error(msg ? msg : "Assert Near failed. " + arg1 + " is not within " + epsilon + " of " + arg2 + ".");
console.trace(msg ? msg : "Assert Near failed. " + arg1 + " is not within " + epsilon + " of " + arg2 + ".");
return TEST_FAIL;
}
return TEST_SUCCESS;
}
/** Asserts that the input argument is null.
@ -716,8 +763,10 @@ function assert_near(arg1, arg2, epsilon = 1E-5, msg) {
*/
function assert_null(arg, msg) {
if (arg !== null) {
throw new Error(msg ? msg : "Assert Near failed. " + arg + " is not null.");
console.trace(msg ? msg : "Assert Near failed. " + arg + " is not null.");
return TEST_FAIL;
}
return TEST_SUCCESS;
}
/** Asserts that the input argument is undefined.
@ -727,8 +776,10 @@ function assert_null(arg, msg) {
*/
function assert_undefined(arg, msg) {
if (arg !== undefined) {
throw new Error(msg ? msg : "Assert Near failed. " + arg + " is not undefined.");
console.trace(msg ? msg : "Assert Near failed. " + arg + " is not undefined.");
return TEST_FAIL;
}
return TEST_SUCCESS;
}
/** Asserts that there is an error when a callback function is run.
@ -740,9 +791,10 @@ function assert_error(func_binding, msg) {
try {
func_binding();
} catch (err) {
return;
return TEST_SUCCESS;
}
throw new Error(msg ? msg : "Function didn't throw an error.");
console.trace(msg ? msg : "Function didn't throw an error.");
return TEST_FAIL;
}
/**
@ -779,8 +831,6 @@ function deepcopy(obj, refs=undefined) {
}
return ret;
}
/**
*
*/

11
temp.py Normal file
View file

@ -0,0 +1,11 @@
import json
import sys
import os
import base64
import argparse
parser = argparse.ArgumentParser(description="Do a little trolling.")
parser.add_argument('infile', help='input file to read data from')
parser.add_argument('outfile', help='output file to dump clean data into')
args = parser.parse_args()
infile, outfile = args.infile, args.outfile

32
testing/index.html Normal file
View file

@ -0,0 +1,32 @@
<!DOCTYPE html>
<!-- This is supposed to be a testing page. Modify it as you need. -->
<html scroll-behavior="smooth">
<head>
<title>WynnBuilder Dev</title>
<link rel="icon" href="../media/icons/new/atlas64.png">
<link rel="manifest" href="manifest.json">
<meta name="viewport" content="width=device-width, initial-scale=.45, user-scalable=no">
<!-- nunito font, copying wynnbuilder, which is copying wynndata -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Nunito&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Nunito&display=swap" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet"
integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous">
<link rel="stylesheet" href="../css/sq2bs.css">
</head>
<body id = "body" class = "all">
<!-- add DOM elements as desired -->
<!-- add scripts -->
<script type="text/javascript" src="../js/utils.js"></script>
</body>
</html>

26
unit_tests/index.html Normal file
View file

@ -0,0 +1,26 @@
<!DOCTYPE html>
<body>
<head>
<meta name="HandheldFriendly" content="true" />
<meta name="MobileOptimized" content="320" />
<meta name="viewport" content="initial-scale=1.0, maximum-scale=1.0, width=device-width, user-scalable=no" />
<!-- nunito font, copying wynndata -->
<meta name="viewport" content="width=device-width, initial-scale=.45, user-scalable=no">
<link href="https://fonts.googleapis.com/css2?family=Nunito&display=swap" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous">
<link rel="stylesheet" href="../css/sq2bs.css">
<link rel="icon" href="../media/icons/new/builder.png">
<link rel="manifest" href="manifest.json">
<title>WynnBuilder</title>
</head>
<div>
Scram kid this page ain't for you
</div>
<script type="text/javascript" src = "./test_main.js"></script>
<!--Import other needed scripts here-->
<script type="text/javascript" src="../js/utils.js"></script>
<!--script type="text/javascript" src="../js/builder/optimize.js"></script-->
</body>

94
unit_tests/test_main.js Normal file
View file

@ -0,0 +1,94 @@
//requires js/utils.js
function test_bv() {
/* BASIC TESTS - NO EDGE CASES */
//empty array
let bv = new BitVector(0);
console.log(bv.toB64()); //0
bv.append(10, 4);
console.log(bv.toB64()); //A
bv.append(10, 4);
console.log(bv.toB64(), bv.bits); //g2, 170 (0b10 101010)
bv = new BitVector("");
console.log(bv.toB64(), bv.bits, bv.length);
bv.append("A");
console.log(bv.toB64(), bv.bits); //A
bv.append("102");
console.log(bv.toB64(), bv.bits); //A102
//make sure extra length doesn't do anything
bv = new BitVector(0);
bv.append(10, 5);
console.log(bv.toB64()); //A
bv.append(10, 4);
console.log(bv.toB64(), bv.bits); //a5, 330 (0b101 001010)
//non-empty array
bv = new BitVector(32); //100000
console.log(bv.toB64(), bv.bits); //W, 32 (0b 100000)
bv.append(1, 1);
console.log(bv.toB64(), bv.bits); //W1, 96 (0b 1 100000)
bv.append(10, 4);
console.log(bv.toB64(), bv.bits); //WL, 1376 (0b 10101 100000)
bv = new BitVector(7, 2);
bv.append("ABCDE");
console.log(bv.toB64(), bv.bits, bv.length); // limqu0, 0b 00 111000 110100 110000 101100 101111
//bit ops
console.log(bv.read_bit(7)); //0
bv.set_bit(7);
console.log(bv.read_bit(7)); //1
console.log(bv.toB64(), bv.bits); //WN , 1504 (0b 10111 100000)
bv.clear_bit(7);
console.log(bv.read_bit(7)); //0
/* SIMPLE EDGE CASE TESTS*/
// string -> 3 ints
bv = new BitVector("a1s2d3f4A9XJKw-m");
console.log(bv.toB64(), bv.bits, bv.length);
bv.append("M+LKeoxZJ0JELW0x");
console.log(bv.toB64(), bv.bits, bv.length);
//full int
bv = new BitVector(4294967295);
console.log(bv.toB64(), bv.bits, bv.length); //-----3, 4294967295, (0b 11 111111 111111 111111 111111 111111)
//append single bit to full int
bv.append(1, 1);
console.log(bv.toB64(), bv.bits, bv.length); //-----7, [4294967295, 1], (0b 111 1s...)
//append full int to full int to full int
bv = new BitVector(4294967295);
bv.append(4294967280);
console.log(bv.toB64(), bv.bits, bv.length); // -----3----F, [4294967295, 4294967280], (0b 1111 1....1 000011 111111 ...)
bv.append(4294967167);
console.log(bv.toB64(), bv.bits, bv.length); // -----3-----V----, [4294967295, 4294967280, 4294967167, 0] (0b 1...1 011111 1....1 000011 111111)
/* BIG TESTS */
bv = new BitVector(12341234); //(0b 101111 000100 111111 110010)
console.log(bv.toB64(), bv.bits, bv.length); // o-4l, 12341234 (0b 101111 000100 111111 110010)
console.log(bv.slice(10, 15)); //19 (100 11)
bv.append(4113241323); //(0b 11 110101 001010 110001 010011 101011)
bv.append(2213461274); //(0b 1000 001111 101110 101111 010001 1010)
bv.append(1273491384); //(0b 10010 111110 011111 101111 101110 00)
bv.append(1828394744); //(0b 110110 011111 011000 101101 111100 0)
bv.append(1938417329); //(0b 1 110011 100010 011110 011010 110001)
console.log(bv.toB64(), bv.bits, bv.length); //o -4lhJn ArhHlk F8klV+ IuRn+i 5hv9E7
bv = new BitVector("");
bv_2 = new BitVector("a1s2d3f4A9XJKw-m");
console.log(bv_2.toB64(), bv_2.bits, bv_2.length);
bv.append(bv_2);
console.log(bv.toB64(), bv.bits, bv.length);
}