Skip to content

Commit

Permalink
refactor: use node >= v22.x and use new available functionalities
Browse files Browse the repository at this point in the history
  • Loading branch information
chrysomallos committed Aug 17, 2024
1 parent 13a1293 commit 903fed7
Show file tree
Hide file tree
Showing 8 changed files with 89 additions and 49 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# lor-deck-encoder - Legends of Runeterra - Deck Encoder

This is a JavaScript ES6 port of the Legends of Runeterra deck encoder/decoder. The goal of this project is to keep up-to-date with Riot's [Implementation](https://github.com/RiotGames/LoRDeckCodes), stay as close to the original result schema as possible, and use zero external runtime dependencies (instead of testing).
This is a JavaScript ES6 port of the Legends of Runeterra deck encoder/decoder. The goal of this project is to keep up-to-date with Riot's [Implementation](https://github.com/RiotGames/LoRDeckCodes), stay as close to the original result schema as possible, and use minimal runtime dependencies (instead of testing).

## Installation

Expand All @@ -21,7 +21,7 @@ The following headlines will describe the _Deck_ class interface.
Example:
```js
import createDeck from 'lor-deck-encoder';
const deck = createDeck('CEBQGAIFAMJC6BABAMCBGFJUAICAGAQRAICACBIWDQOS4AIBAM4AEAIEAUIQEBADAEHQ');
const deck = createDeck('CEBQEBADAIIQGAIFAMJC6BABAMCBGFJUAIAQCAZYAQAQKFQ4DUXAEAIEAUIQEBADAEHQ');
```

## Example usage
Expand Down
10 changes: 9 additions & 1 deletion cli.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,18 @@ import fs from 'node:fs/promises';
import minimist from 'minimist';
import stringify from 'json-stringify-pretty-compact';
import {Deck, generateDataDragon} from './index.mjs';
import Base32 from './utils/base32.mjs';

const [code, ...parameters] = process.argv.slice(2);

console.log(Deck.fromCode(code));
const deck = Deck.fromCode(code);
console.log(`${code} => ${deck.code} (${code === deck.code})`);

console.log(JSON.stringify(Base32.decode(code)));
console.log(JSON.stringify(deck.list));

console.log(JSON.stringify(Base32.decode(deck.code)));
console.log(JSON.stringify(Deck.fromCode(deck.code).list));

if (parameters?.length) {
const dragon = generateDataDragon();
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
"quibble": "^0.9.2"
},
"engines": {
"node": ">=18"
"node": ">=22"
},
"scripts": {
"cli": "node ./cli.mjs",
Expand Down
1 change: 0 additions & 1 deletion src/data-dragon.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,6 @@ export default class DataDragon {
* @returns {Promise<void>}
*/
async initialize(language) {
if (this.cardsByCode) return;
if (!LANGUAGES[language]) {
language = Object.keys(LANGUAGES).find(key => key === language || key.split('_').includes(language));
if (!language) language = 'en_us';
Expand Down
33 changes: 20 additions & 13 deletions src/encoder.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ export default class Encoder {
const version = firstByte & 0xf;

if (!skipFormatCheck && format > SUPPORTED_FORMAT) throw new SyntaxError(`Deck format ${format} is not supported (supported format ${SUPPORTED_FORMAT})`);
if (version > Factions.maxVersion) throw new ArgumentError('version', `Deck version ${version} is not supported (max supported version ${Factions.maxVersion})`);
if (version > Factions.maxVersion)
throw new ArgumentError('version', `Deck version ${version} is not supported (max supported version ${Factions.maxVersion})`);

const result = new Array();
for (let count = 3; count > 0; count -= 1) {
Expand Down Expand Up @@ -68,24 +69,25 @@ export default class Encoder {
*/
static encode(cards, version) {
if (cards.some(card => !card?.count)) throw new Error('Invalid deck');
version = Math.max(
version ??= Math.max(
cards?.reduce((l, {factionVersion: v}) => Math.max(l, v), 0),
version,
INITIAL_VERSION
);

const values = [];

// cards with count > 3 are handled separately
const grouped = cards.reduce(
(groups, card) => {
// cards with count > 3 are handled separatly
if (card.count > 3) groups.x.push(card);
else groups[card.count].push(card);
return groups;
},
{3: [], 2: [], 1: [], x: []}
);

[3, 2, 1].forEach(count => {
for (let count = 3; count > 0; count--) {
//build the map of set and faction combinations within the group of similar card counts
const factionSetsMap = grouped[count].reduce((map, card) => {
const sf = card.set * 100 + card.factionId;
Expand All @@ -98,17 +100,22 @@ export default class Encoder {
//The sorting convention of this encoding scheme is
//First by the number of set/faction combinations in each top-level list
//Second by the alphanumeric order of the card codes within those lists.
[...factionSetsMap.keys()].sort().forEach(sf => {
const cards = factionSetsMap.get(sf);
values.push(cards.length);
values.push(cards[0].set);
values.push(cards[0].factionId);
cards.sort((a, b) => a.id - b.id).forEach(card => values.push(card.id));
});
});
[...factionSetsMap.entries()]
.sort(([a], [b]) => a - b)
.forEach(([, groupCards]) => {
const [firstCard] = groupCards;
values.push(groupCards.length);
values.push(firstCard.set);
values.push(firstCard.factionId);
groupCards
.map(({id}) => id)
.sort((a, b) => a - b)
.forEach(id => values.push(id));
});
}

//Cards with 4+ are coded simply [count, set, faction, id] for each
grouped.x.sort(Card.compare).forEach(card => {
grouped.x?.sort(Card.compare).forEach(card => {
values.push(card.count);
values.push(card.set);
values.push(card.faction.id);
Expand Down
2 changes: 1 addition & 1 deletion src/factions.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ const AVAILABLE_FACTIONS = [
];

/**
* The max known version.
* The max known version, based on the available factions.
*/
const MAX_KNOWN_VERSION = AVAILABLE_FACTIONS.reduce((last, {version}) => Math.max(last, version), 0);

Expand Down
78 changes: 52 additions & 26 deletions test/encoder.spec.mjs
Original file line number Diff line number Diff line change
@@ -1,34 +1,60 @@
import Encoder from '../src/encoder.mjs';
import {ArgumentError} from '../src/errors.mjs';
import assert from 'assert';
import Base32 from '../utils/base32.mjs';

describe('[Encoder] class tests', function () {
describe('encoder', function () {
describe('encode method', function () {
it('no cards', function () {
assert.equal(Encoder.decode('CEAAAAA').length, 0);
});

it('no cards skip version', function () {
assert.equal(Encoder.decode('EUAAAAA', true).length, 0);
});

it('check version', function () {
assert.throws(() => Encoder.decode('CZAAAAA', false), ArgumentError);
});

it('check format', function () {
assert.throws(() => Encoder.decode('EUAAAAA', false), SyntaxError);
});

it('empty value', function () {
assert.throws(() => Encoder.decode(''));
});

it('invalid deck', function () {
assert.throws(() => Encoder.encode([null]));
assert.throws(() => Encoder.encode([{count: 0}]));
});
describe('decode method', function () {
it('no cards', function () {
assert.equal(Encoder.decode('CEAAAAA').length, 0);
});

it('no cards skip version', function () {
assert.equal(Encoder.decode('EUAAAAA', true).length, 0);
});

it('check version', function () {
assert.throws(() => Encoder.decode('CZAAAAA', false), ArgumentError);
});

it('check format', function () {
assert.throws(() => Encoder.decode('EUAAAAA', false), SyntaxError);
});

it('empty value', function () {
assert.throws(() => Encoder.decode(''));
});
});

describe('encode method', function () {
it('invalid deck', function () {
assert.throws(() => Encoder.encode([null]));
assert.throws(() => Encoder.encode([{count: 0}]));
});
});

describe('decode algorithm', function () {

const newGrouping = cards => Object.groupBy(cards, ({count}) => (count > 3 ? 'x' : count));
const newMapping = (grouped, count) => Map.groupBy(grouped[count] ?? [], ({set, factionId}) => set * 100 + factionId);

const code = 'CEBAIAIABEQDINIFAEBAUEATEAYAEAIBAIYQGAIAAIDSUAQCAEBCWLIDAEAAMHJN';
let cards, info;

beforeEach(function () {
cards = Encoder.decode(code);
info = Base32.decode(code);
});

it('must match old groups', function () {
const groups = newGrouping(cards);

newMapping(groups, 3);
});

it('must match lengths', function () {
assert.equal(cards.length, 18);
assert.equal(info.length, 40);
});
});
});
8 changes: 4 additions & 4 deletions utils/base32.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -88,13 +88,13 @@ export default class Base32 {
if (!bytes?.length) return padOutput ? PADDING : '';
if (bytes.length >= 1 << 28) throw new RangeError('Value is too long to encode as base32 string');

const length = Math.floor((bytes.length * 8 + SHIFT - 1) / SHIFT);
const calculatedLength = Math.floor((bytes.length * 8 + SHIFT - 1) / SHIFT);
let padding = 0;
if (padOutput) {
const rest = length % 8;
if (rest) padding = 8 - rest;
const remainder = calculatedLength % 8;
if (remainder) padding = 8 - remainder;
}
const result = Buffer.alloc(length + padding);
const result = Buffer.alloc(calculatedLength + padding);

let buffer = bytes[0];
let nextByte = 1;
Expand Down

0 comments on commit 903fed7

Please sign in to comment.