UNPKG

@piltoverarchive/riftbound-deck-codes

Version:

Encode and decode Riftbound TCG decks to/from shareable strings

227 lines 8.3 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.getCodeFromDeck = getCodeFromDeck; exports.getDeckFromCode = getDeckFromCode; const mappings_1 = require("./mappings"); const VarintTranslator_1 = __importDefault(require("./VarintTranslator")); const FORMAT = 1; const VERSION = 2; const BASE32_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"; /** * Encodes a byte array to base32 string */ function base32Encode(bytes) { let result = ""; let buffer = 0; let bitsLeft = 0; for (const byte of bytes) { buffer = (buffer << 8) | byte; bitsLeft += 8; while (bitsLeft >= 5) { bitsLeft -= 5; result += BASE32_ALPHABET[(buffer >> bitsLeft) & 0x1f]; } } if (bitsLeft > 0) { buffer <<= 5 - bitsLeft; result += BASE32_ALPHABET[buffer & 0x1f]; } return result; } /** * Decodes a base32 string to byte array */ function base32Decode(str) { const bytes = []; let buffer = 0; let bitsLeft = 0; for (const char of str) { const value = BASE32_ALPHABET.indexOf(char.toUpperCase()); if (value === -1) { throw new Error(`Invalid character in deck code: '${char}'`); } buffer = (buffer << 5) | value; bitsLeft += 5; while (bitsLeft >= 8) { bitsLeft -= 8; bytes.push((buffer >> bitsLeft) & 0xff); } } return new Uint8Array(bytes); } /** * Parses a Riftbound card code into its components * @param cardCode - Card code in format "SET-NUMBERvariant" (e.g., "OGN-007a") * @returns Object containing set, number, and variant * @throws Error if card code format is invalid */ function parseCardCode(cardCode) { const parts = cardCode.split("-"); if (parts.length !== 2) { throw new Error(`Invalid card code format: ${cardCode}. Expected format: SET-NUMBERvariant`); } const set = parts[0]; const rest = parts[1]; if (!set || !rest) { throw new Error(`Invalid card code format: ${cardCode}. Missing set or card number.`); } const match = rest.match(/^(\d+)([a-z]?)$/); if (!match) { throw new Error(`Invalid card code format: ${cardCode}. Expected format: SET-NUMBERvariant`); } return { set, number: match[1] ?? "", variant: match[2] ?? "", }; } /** * Groups cards by set and variant for efficient encoding */ function groupBySetAndVariant(cards) { const groups = new Map(); for (const card of cards) { const { set, number, variant } = parseCardCode(card.cardCode); const key = `${set}-${variant}`; if (!groups.has(key)) { const setValue = mappings_1.SET_MAP[set]; if (setValue === undefined) { throw new Error(`Unknown set: ${set}. Valid sets: ${Object.keys(mappings_1.SET_MAP).join(", ")}`); } const variantValue = mappings_1.VARIANT_MAP[variant]; if (variantValue === undefined) { throw new Error(`Unknown variant: '${variant}'. Valid variants: ${Object.keys(mappings_1.VARIANT_MAP).join(", ")}`); } groups.set(key, { set: setValue, variant: variantValue, cardNumbers: [], }); } groups.get(key).cardNumbers.push(number); } return Array.from(groups.values()) .sort((a, b) => { if (a.set !== b.set) return a.set - b.set; if (a.variant !== b.variant) return a.variant - b.variant; return 0; }) .map((group) => ({ ...group, cardNumbers: group.cardNumbers.sort((a, b) => a.localeCompare(b, undefined, { numeric: true })), })); } /** * Encodes a deck section into bytes * @param deck - The deck to encode * @param maxCount - Maximum count to process (12 for main deck, 3 for sideboard) */ function encodeDeckSection(deck, maxCount = 12) { const bytes = []; // Process counts from maxCount down to 1 for (let count = maxCount; count >= 1; count--) { const cards = deck.filter((card) => card.count === count); const setVariantGroups = groupBySetAndVariant(cards); // Write number of set/variant groups bytes.push(...VarintTranslator_1.default.GetVarint(setVariantGroups.length)); // Write each set/variant group for (const group of setVariantGroups) { bytes.push(...VarintTranslator_1.default.GetVarint(group.cardNumbers.length)); bytes.push(group.set); bytes.push(group.variant); // Write card numbers for (const cardNumber of group.cardNumbers) { bytes.push(...VarintTranslator_1.default.GetVarint(parseInt(cardNumber))); } } } return bytes; } /** * Decodes a deck section from bytes * @param translator - The varint translator * @param maxCount - Maximum count to process (12 for main deck, 3 for sideboard) */ function decodeDeckSection(translator, maxCount = 12) { const deck = []; // Process counts from maxCount down to 1 for (let count = maxCount; count >= 1; count--) { const numGroups = translator.PopVarint(); for (let i = 0; i < numGroups; i++) { const numCards = translator.PopVarint(); const set = translator.get(0); const variant = translator.get(1); translator.sliceAndSet(2); const setCode = Object.entries(mappings_1.SET_MAP).find(([_, value]) => value === set)?.[0]; const variantCode = Object.entries(mappings_1.VARIANT_MAP).find(([_, value]) => value === variant)?.[0]; if (!setCode) { throw new Error(`Unknown set code: ${set}`); } for (let j = 0; j < numCards; j++) { const cardNumber = translator.PopVarint(); deck.push({ cardCode: `${setCode}-${cardNumber.toString().padStart(3, "0")}${variantCode || ""}`, count, }); } } } return deck; } /** * Encodes a Riftbound deck into a shareable deck code * @param mainDeck - The main deck cards * @param sideboard - Optional sideboard cards (defaults to empty array) * @returns Base32-encoded deck code string * @throws Error if deck format is invalid */ function getCodeFromDeck(mainDeck, sideboard = []) { const bytes = []; // Write format and version (always version 2) bytes.push((FORMAT << 4) | VERSION); // Encode main deck (counts 1-12) bytes.push(...encodeDeckSection(mainDeck, 12)); // Encode sideboard (counts 1-3 only, since sideboards can't have runes/battlefields) // Empty sideboard will encode as three 0-byte varints (3 bytes total) bytes.push(...encodeDeckSection(sideboard, 3)); return base32Encode(new Uint8Array(bytes)); } /** * Decodes a Riftbound deck code into deck and sideboard * @param code - Base32-encoded deck code string * @returns Object containing mainDeck and sideboard arrays * @throws Error if code is invalid or unsupported */ function getDeckFromCode(code) { const bytes = base32Decode(code); const translator = new VarintTranslator_1.default(bytes); // Read format and version const formatVersion = translator.get(0); translator.sliceAndSet(1); const format = (formatVersion >> 4) & 0x0f; const version = formatVersion & 0x0f; if (format !== FORMAT) { throw new Error(`Unsupported format: ${format}. Expected format: ${FORMAT}`); } if (version > VERSION) { throw new Error(`Unsupported version: ${version}. Maximum supported version: ${VERSION}`); } // Decode main deck (counts 1-12) const mainDeck = decodeDeckSection(translator, 12); // Decode sideboard (counts 1-3 only) // Version 1 codes don't have sideboard section, version 2+ do let sideboard = []; if (version >= 2) { sideboard = decodeDeckSection(translator, 3); } return { mainDeck, sideboard, }; } //# sourceMappingURL=deckCode.js.map