Commit 804ad568 authored by nanahira's avatar nanahira

add isEqual and constructor

parent 2246aab3
import { fromBase64Url, toBase64Url } from './src/base64'; import { fromBase64Url, toBase64Url } from './src/base64';
import { BufferReader, BufferWriter, countConsecutiveItems, countItems } from './src/utils'; import {
arrayEquals,
BufferReader,
BufferWriter,
countConsecutiveItems,
countItems,
} from './src/utils';
import { fromYdkeURL, toYdkeURL } from './src/ydke'; import { fromYdkeURL, toYdkeURL } from './src/ydke';
import { fromYGOMobileDeckURL, toYGOMobileDeckURL } from './src/ygom'; import { fromYGOMobileDeckURL, toYGOMobileDeckURL } from './src/ygom';
export default class YGOProDeck { export interface YGOProDeckLike {
main: number[];
extra: number[];
side: number[];
name?: string;
}
export default class YGOProDeck implements YGOProDeckLike {
main: number[] = []; main: number[] = [];
extra: number[] = []; extra: number[] = [];
side: number[] = []; side: number[] = [];
name?: string; name?: string;
constructor(init: Partial<YGOProDeckLike> = {}) {
Object.assign(this, init);
}
bufferLength(noCompact = false) { bufferLength(noCompact = false) {
const counted = [this.main, this.extra, this.side].map(noCompact ? countConsecutiveItems : countItems); const counted = [this.main, this.extra, this.side].map(
noCompact ? countConsecutiveItems : countItems,
);
return counted.reduce((a, b) => a + b.length * 4, 0); return counted.reduce((a, b) => a + b.length * 4, 0);
} }
toUint8Array(noCompact = false) { toUint8Array(noCompact = false) {
const counted = [this.main, this.extra, this.side].map(noCompact ? countConsecutiveItems : countItems); const counted = [this.main, this.extra, this.side].map(
noCompact ? countConsecutiveItems : countItems,
);
const writer = new BufferWriter( const writer = new BufferWriter(
counted.reduce((a, b) => a + b.length * 4, 0), counted.reduce((a, b) => a + b.length * 4, 0),
); );
const writeCards = (countMap: { item: number, count: number }[], type: number) => { const writeCards = (
countMap: { item: number; count: number }[],
type: number,
) => {
// each card: 28 bits for id, 2 bits(0, 1, 2, 3) for type(0: main, 1: extra, 2: side, 3: unknown), 2 bits for count (0: 1, 1: 2, 2: 3, 3: 4) // each card: 28 bits for id, 2 bits(0, 1, 2, 3) for type(0: main, 1: extra, 2: side, 3: unknown), 2 bits for count (0: 1, 1: 2, 2: 3, 3: 4)
for (const { item, count } of countMap) { for (const { item, count } of countMap) {
if (count > 4) { if (count > 4) {
...@@ -105,14 +129,18 @@ export default class YGOProDeck { ...@@ -105,14 +129,18 @@ export default class YGOProDeck {
return new YGOProDeck().fromYdkString(str); return new YGOProDeck().fromYdkString(str);
} }
fromUpdateDeckPayload(buf: Uint8Array, isExtraDeckCard: (code: number, i: number, mainc: number) => boolean = () => false) { fromUpdateDeckPayload(
buf: Uint8Array,
isExtraDeckCard: (code: number, i: number, mainc: number) => boolean = () =>
false,
) {
const reader = new BufferReader(buf); const reader = new BufferReader(buf);
const mainc = reader.readUint32LE(); const mainc = reader.readUint32LE();
const sidec = reader.readUint32LE(); const sidec = reader.readUint32LE();
this.main = []; this.main = [];
this.extra = []; this.extra = [];
this.side = []; this.side = [];
for(let i = 0; i < mainc; i++) { for (let i = 0; i < mainc; i++) {
const id = reader.readUint32LE(); const id = reader.readUint32LE();
if (isExtraDeckCard(id, i, mainc)) { if (isExtraDeckCard(id, i, mainc)) {
this.extra.push(id); this.extra.push(id);
...@@ -120,14 +148,18 @@ export default class YGOProDeck { ...@@ -120,14 +148,18 @@ export default class YGOProDeck {
this.main.push(id); this.main.push(id);
} }
} }
for(let i = 0; i < sidec; i++) { for (let i = 0; i < sidec; i++) {
const id = reader.readUint32LE(); const id = reader.readUint32LE();
this.side.push(id); this.side.push(id);
} }
return this; return this;
} }
static fromUpdateDeckPayload(buf: Uint8Array, isExtraDeckCard: (code: number, i: number, mainc: number) => boolean = () => false) { static fromUpdateDeckPayload(
buf: Uint8Array,
isExtraDeckCard: (code: number, i: number, mainc: number) => boolean = () =>
false,
) {
return new YGOProDeck().fromUpdateDeckPayload(buf, isExtraDeckCard); return new YGOProDeck().fromUpdateDeckPayload(buf, isExtraDeckCard);
} }
...@@ -154,9 +186,14 @@ export default class YGOProDeck { ...@@ -154,9 +186,14 @@ export default class YGOProDeck {
} }
toYGOMobileDeckURL(): string { toYGOMobileDeckURL(): string {
return toYGOMobileDeckURL(this.main, this.extra, this.side, this.name && { return toYGOMobileDeckURL(
name: this.name, this.main,
}); this.extra,
this.side,
this.name && {
name: this.name,
},
);
} }
fromYdkeURL(uri: string): YGOProDeck { fromYdkeURL(uri: string): YGOProDeck {
...@@ -178,4 +215,19 @@ export default class YGOProDeck { ...@@ -178,4 +215,19 @@ export default class YGOProDeck {
side: this.side, side: this.side,
}); });
} }
isEqual(
other: YGOProDeckLike,
options: {
ignoreOrder?: boolean;
} = {},
): boolean {
const { ignoreOrder = false } = options;
const normalize = (arr: number[]) =>
ignoreOrder ? [...arr].sort((a, b) => a - b) : arr;
const fields = ['main', 'extra', 'side'] as const;
return fields.every((field) =>
arrayEquals(normalize(this[field]), normalize(other[field] || [])),
);
}
} }
...@@ -3,7 +3,7 @@ export function countItems<T>(arr: T[]) { ...@@ -3,7 +3,7 @@ export function countItems<T>(arr: T[]) {
for (const item of arr) { for (const item of arr) {
map.set(item, (map.get(item) || 0) + 1); map.set(item, (map.get(item) || 0) + 1);
} }
const blocks: { item: T, count: number }[] = []; const blocks: { item: T; count: number }[] = [];
for (const [item, count] of map.entries()) { for (const [item, count] of map.entries()) {
blocks.push({ item, count }); blocks.push({ item, count });
} }
...@@ -57,7 +57,7 @@ abstract class BufferCursor { ...@@ -57,7 +57,7 @@ abstract class BufferCursor {
const next = old + bytes; const next = old + bytes;
if (next > this.buffer.length) { if (next > this.buffer.length) {
throw new RangeError( throw new RangeError(
`Pointer overflow: tried to move to ${next}, but buffer length is ${this.buffer.length}` `Pointer overflow: tried to move to ${next}, but buffer length is ${this.buffer.length}`,
); );
} }
this.pointer = next; this.pointer = next;
...@@ -72,8 +72,8 @@ export class BufferWriter extends BufferCursor { ...@@ -72,8 +72,8 @@ export class BufferWriter extends BufferCursor {
writeUint32LE(value: number) { writeUint32LE(value: number) {
const idx = this.increasePointer(4); const idx = this.increasePointer(4);
this.buffer[idx ] = value & 0xff; this.buffer[idx] = value & 0xff;
this.buffer[idx + 1] = (value >>> 8) & 0xff; this.buffer[idx + 1] = (value >>> 8) & 0xff;
this.buffer[idx + 2] = (value >>> 16) & 0xff; this.buffer[idx + 2] = (value >>> 16) & 0xff;
this.buffer[idx + 3] = (value >>> 24) & 0xff; this.buffer[idx + 3] = (value >>> 24) & 0xff;
return this; return this;
...@@ -81,24 +81,24 @@ export class BufferWriter extends BufferCursor { ...@@ -81,24 +81,24 @@ export class BufferWriter extends BufferCursor {
writeUint32BE(value: number) { writeUint32BE(value: number) {
const idx = this.increasePointer(4); const idx = this.increasePointer(4);
this.buffer[idx ] = (value >>> 24) & 0xff; this.buffer[idx] = (value >>> 24) & 0xff;
this.buffer[idx + 1] = (value >>> 16) & 0xff; this.buffer[idx + 1] = (value >>> 16) & 0xff;
this.buffer[idx + 2] = (value >>> 8) & 0xff; this.buffer[idx + 2] = (value >>> 8) & 0xff;
this.buffer[idx + 3] = value & 0xff; this.buffer[idx + 3] = value & 0xff;
return this; return this;
} }
writeUint16LE(value: number) { writeUint16LE(value: number) {
const idx = this.increasePointer(2); const idx = this.increasePointer(2);
this.buffer[idx ] = value & 0xff; this.buffer[idx] = value & 0xff;
this.buffer[idx + 1] = (value >>> 8) & 0xff; this.buffer[idx + 1] = (value >>> 8) & 0xff;
return this; return this;
} }
writeUint16BE(value: number) { writeUint16BE(value: number) {
const idx = this.increasePointer(2); const idx = this.increasePointer(2);
this.buffer[idx ] = (value >>> 8) & 0xff; this.buffer[idx] = (value >>> 8) & 0xff;
this.buffer[idx + 1] = value & 0xff; this.buffer[idx + 1] = value & 0xff;
return this; return this;
} }
...@@ -127,34 +127,30 @@ export class BufferReader extends BufferCursor { ...@@ -127,34 +127,30 @@ export class BufferReader extends BufferCursor {
const b = this.buffer; const b = this.buffer;
// >>>0 保证无符号输出 // >>>0 保证无符号输出
return ( return (
(b[idx ] ) | (b[idx] | (b[idx + 1] << 8) | (b[idx + 2] << 16) | (b[idx + 3] << 24)) >>>
(b[idx + 1] << 8) | 0
(b[idx + 2] << 16) | );
(b[idx + 3] << 24)
) >>> 0;
} }
readUint32BE(): number { readUint32BE(): number {
const idx = this.increasePointer(4); const idx = this.increasePointer(4);
const b = this.buffer; const b = this.buffer;
return ( return (
(b[idx ] << 24) | ((b[idx] << 24) | (b[idx + 1] << 16) | (b[idx + 2] << 8) | b[idx + 3]) >>>
(b[idx + 1] << 16) | 0
(b[idx + 2] << 8) | );
(b[idx + 3] )
) >>> 0;
} }
readUint16LE(): number { readUint16LE(): number {
const idx = this.increasePointer(2); const idx = this.increasePointer(2);
const b = this.buffer; const b = this.buffer;
return (b[idx] | (b[idx + 1] << 8)); return b[idx] | (b[idx + 1] << 8);
} }
readUint16BE(): number { readUint16BE(): number {
const idx = this.increasePointer(2); const idx = this.increasePointer(2);
const b = this.buffer; const b = this.buffer;
return ((b[idx] << 8) | b[idx + 1]); return (b[idx] << 8) | b[idx + 1];
} }
readUint8(): number { readUint8(): number {
...@@ -177,3 +173,11 @@ export class BufferReader extends BufferCursor { ...@@ -177,3 +173,11 @@ export class BufferReader extends BufferCursor {
return this.buffer.subarray(idx, this.buffer.length); return this.buffer.subarray(idx, this.buffer.length);
} }
} }
export const arrayEquals = <T>(a: T[], b: T[]): boolean => {
if (a.length !== b.length) return false;
for (let i = 0; i < a.length; i++) {
if (a[i] !== b[i]) return false;
}
return true;
};
...@@ -51,10 +51,11 @@ describe('Sample test.', () => { ...@@ -51,10 +51,11 @@ describe('Sample test.', () => {
expect(decoded.main).toStrictEqual(deck.main); expect(decoded.main).toStrictEqual(deck.main);
expect(decoded.extra).toStrictEqual(deck.extra); expect(decoded.extra).toStrictEqual(deck.extra);
expect(decoded.side).toStrictEqual(deck.side); expect(decoded.side).toStrictEqual(deck.side);
}) });
it('should encode and decode ygomobile', () => { it('should encode and decode ygomobile', () => {
const uri = 'http://deck.ourygo.top?name=%E7%99%BD%E9%93%B6%E5%9F%8E%E7%A0%81&ygotype=deck&v=1&d=J-xK4Mka02AuEAMf2dV6mj7aRemuJNQJK8BwrcEYh0MqOJqBJSgYqUwxPEVhOG8R18WWVKmzkT-xEdwxbmGVkkOdrVIpufaYI3Hs8oOrcya8Bi40h9G79iFW80rq-o6P-AHsusPY5nmvHLol0DIqEykESVlf6VSbxVJp-j7XZtTE0XvmJW80rqH28R4rgyRovOusVJzbutenYFBA_cyK6d3UWcQkJQlLjaroWavH-INFA56k5DQNWOQ1gpvxrKVBLgEk1olpolKmSgriramLlgtBK1EQ6C6oi94ZyHe7N6T7mqE6peds7mahrORP6A'; const uri =
'http://deck.ourygo.top?name=%E7%99%BD%E9%93%B6%E5%9F%8E%E7%A0%81&ygotype=deck&v=1&d=J-xK4Mka02AuEAMf2dV6mj7aRemuJNQJK8BwrcEYh0MqOJqBJSgYqUwxPEVhOG8R18WWVKmzkT-xEdwxbmGVkkOdrVIpufaYI3Hs8oOrcya8Bi40h9G79iFW80rq-o6P-AHsusPY5nmvHLol0DIqEykESVlf6VSbxVJp-j7XZtTE0XvmJW80rqH28R4rgyRovOusVJzbutenYFBA_cyK6d3UWcQkJQlLjaroWavH-INFA56k5DQNWOQ1gpvxrKVBLgEk1olpolKmSgriramLlgtBK1EQ6C6oi94ZyHe7N6T7mqE6peds7mahrORP6A';
const correctYdk = `#created by OURYGO const correctYdk = `#created by OURYGO
#main #main
22812963 22812963
...@@ -146,7 +147,7 @@ describe('Sample test.', () => { ...@@ -146,7 +147,7 @@ describe('Sample test.', () => {
30748475 30748475
82732705 82732705
94145021 94145021
94145021` 94145021`;
const deck = new YGOProDeck().fromYGOMobileDeckURL(uri); const deck = new YGOProDeck().fromYGOMobileDeckURL(uri);
expect(deck.main).toHaveLength(58); expect(deck.main).toHaveLength(58);
expect(deck.extra).toHaveLength(15); expect(deck.extra).toHaveLength(15);
...@@ -163,7 +164,7 @@ describe('Sample test.', () => { ...@@ -163,7 +164,7 @@ describe('Sample test.', () => {
const uri2 = deck.toYGOMobileDeckURL(); const uri2 = deck.toYGOMobileDeckURL();
expect(uri2).toBe(uri); expect(uri2).toBe(uri);
}) });
it('should encode and decode ydke URL', () => { it('should encode and decode ydke URL', () => {
const deck = new YGOProDeck(); const deck = new YGOProDeck();
...@@ -175,19 +176,20 @@ describe('Sample test.', () => { ...@@ -175,19 +176,20 @@ describe('Sample test.', () => {
deck.extra.push(444444); deck.extra.push(444444);
deck.extra.push(555555); deck.extra.push(555555);
deck.side.push(666666); deck.side.push(666666);
const uri = deck.toYdkeURL(); const uri = deck.toYdkeURL();
expect(uri.startsWith('ydke://')).toBe(true); expect(uri.startsWith('ydke://')).toBe(true);
const decoded = new YGOProDeck().fromYdkeURL(uri); const decoded = new YGOProDeck().fromYdkeURL(uri);
expect(decoded.main).toStrictEqual(deck.main); expect(decoded.main).toStrictEqual(deck.main);
expect(decoded.extra).toStrictEqual(deck.extra); expect(decoded.extra).toStrictEqual(deck.extra);
expect(decoded.side).toStrictEqual(deck.side); expect(decoded.side).toStrictEqual(deck.side);
}); });
it('should encode and decode update deck payload', () => { it('should encode and decode update deck payload', () => {
const uri = 'http://deck.ourygo.top?name=%E7%99%BD%E9%93%B6%E5%9F%8E%E7%A0%81&ygotype=deck&v=1&d=J-xK4Mka02AuEAMf2dV6mj7aRemuJNQJK8BwrcEYh0MqOJqBJSgYqUwxPEVhOG8R18WWVKmzkT-xEdwxbmGVkkOdrVIpufaYI3Hs8oOrcya8Bi40h9G79iFW80rq-o6P-AHsusPY5nmvHLol0DIqEykESVlf6VSbxVJp-j7XZtTE0XvmJW80rqH28R4rgyRovOusVJzbutenYFBA_cyK6d3UWcQkJQlLjaroWavH-INFA56k5DQNWOQ1gpvxrKVBLgEk1olpolKmSgriramLlgtBK1EQ6C6oi94ZyHe7N6T7mqE6peds7mahrORP6A'; const uri =
'http://deck.ourygo.top?name=%E7%99%BD%E9%93%B6%E5%9F%8E%E7%A0%81&ygotype=deck&v=1&d=J-xK4Mka02AuEAMf2dV6mj7aRemuJNQJK8BwrcEYh0MqOJqBJSgYqUwxPEVhOG8R18WWVKmzkT-xEdwxbmGVkkOdrVIpufaYI3Hs8oOrcya8Bi40h9G79iFW80rq-o6P-AHsusPY5nmvHLol0DIqEykESVlf6VSbxVJp-j7XZtTE0XvmJW80rqH28R4rgyRovOusVJzbutenYFBA_cyK6d3UWcQkJQlLjaroWavH-INFA56k5DQNWOQ1gpvxrKVBLgEk1olpolKmSgriramLlgtBK1EQ6C6oi94ZyHe7N6T7mqE6peds7mahrORP6A';
const deck = new YGOProDeck().fromYGOMobileDeckURL(uri); const deck = new YGOProDeck().fromYGOMobileDeckURL(uri);
const updateDeckPayload = deck.toUpdateDeckPayload(); const updateDeckPayload = deck.toUpdateDeckPayload();
const bufferReader = new BufferReader(updateDeckPayload); const bufferReader = new BufferReader(updateDeckPayload);
...@@ -195,12 +197,40 @@ describe('Sample test.', () => { ...@@ -195,12 +197,40 @@ describe('Sample test.', () => {
const sidec = bufferReader.readUint32LE(); const sidec = bufferReader.readUint32LE();
expect(mainc).toBe(58 + 15); expect(mainc).toBe(58 + 15);
expect(sidec).toBe(15); expect(sidec).toBe(15);
const deckAgain = new YGOProDeck().fromUpdateDeckPayload(updateDeckPayload, (id, i, mainc) => i >= 58); const deckAgain = new YGOProDeck().fromUpdateDeckPayload(
updateDeckPayload,
(id, i, mainc) => i >= 58,
);
expect(deckAgain.main).toHaveLength(58); expect(deckAgain.main).toHaveLength(58);
expect(deckAgain.extra).toHaveLength(15); expect(deckAgain.extra).toHaveLength(15);
expect(deckAgain.side).toHaveLength(15); expect(deckAgain.side).toHaveLength(15);
expect(deckAgain.main).toStrictEqual(deck.main); expect(deckAgain.main).toStrictEqual(deck.main);
expect(deckAgain.extra).toStrictEqual(deck.extra); expect(deckAgain.extra).toStrictEqual(deck.extra);
expect(deckAgain.side).toStrictEqual(deck.side); expect(deckAgain.side).toStrictEqual(deck.side);
}) });
it('should compare decks correctly', () => {
const a = new YGOProDeck({
main: [1, 2, 3, 4, 5],
extra: [11, 12, 13],
side: [21, 22],
});
const b = new YGOProDeck({
// different order
main: [5, 4, 3, 2, 1],
extra: [13, 12, 11],
side: [22, 21],
});
const c = new YGOProDeck({
// different content
main: [1, 2, 3, 4, 6],
extra: [11, 12, 13],
side: [21, 22],
});
const aa = new YGOProDeck(a);
expect(a.isEqual(aa)).toBe(true);
expect(a.isEqual(b)).toBe(false);
expect(a.isEqual(b, { ignoreOrder: true })).toBe(true);
expect(a.isEqual(c)).toBe(false);
});
}); });
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment