"""
Logic for encoding deck into a deck code string.
Encoding is done by writing a few things into a bytearray thanks to the magic of bitwise operations.
That is then encoded to base64 and sanitized for url usage.
"""
import re
from base64 import b64encode
from ..exceptions import DeckEncodeException
from ..types.deck_types import DeckContents
# Version 1: Heroes and Cards
# Version 2: Name, Heroes and Cards
SUPPORTED_VERSIONS = [2]
[docs]def encode_deck(deck_contents: DeckContents, version: int = SUPPORTED_VERSIONS[-1]) -> str:
"""
Encodes deck content into a deck code string.
:param deck_contents: A dictionary with name, heroes and cards (without those included automatically)
:param version: Deck code version, atm only 2 and higher is supported
:return: Deck code
"""
return Encoder(deck_contents, version=version).deck_code
[docs]class Encoder:
"""
Main purpose of this class is to hold shared data across the encoding process.
There shouldn't be a need to use this part of the library, It offers a more low level access
to the encoding process, but doesn't offer anything more practical than :py:func:`pyartifact.encode_deck` does.
"""
header_size = 3
prefix = 'ADC'
def __init__(self, deck_contents: DeckContents, version: int = SUPPORTED_VERSIONS[-1]) -> None:
"""
:param deck_contents: The deck contents.
:param version: Version under which to encode, by default the newest version is used.
Must be on of the supported versions for encoding (atm only V2).
"""
self.version = version
self.heroes = sorted(deck_contents['heroes'], key=lambda x: x['id'])
self.cards = sorted(deck_contents['cards'], key=lambda x: x['id'])
name = deck_contents.get('name', '')[:63] # name has a hard limit of 63 characters (V2)
self.name = re.sub('<[^<]+?>', '', name)
self._binary = bytearray() # This where all our hard work will end in, before being interpreted as string
@property
def deck_code(self) -> str:
"""Returns the deck code for the deck contents provided."""
self._encode_to_binary()
encoded = b64encode(bytes(self._binary)).decode()
deck_code = f'{self.prefix}{encoded}'
# url safe (no, base64.urlsafe_b64encode wouldn't do the trick, different replacements in this case)
deck_code = deck_code.replace('/', '-').replace('=', '_')
return deck_code
def _encode_to_binary(self) -> bytearray:
"""Heavy lifting for the encoding."""
# First byte is deck code version and number of heroes
version = (self.version << 4) | _extract_n_bits(len(self.heroes), 3)
self._binary.append(version)
# Put placeholder for checksum as the second byte
placeholder_checksum = 0
checksum_index = len(self._binary)
self._binary.append(placeholder_checksum)
# Length of the name string is the 3rd byte
self._binary.append(len(self.name))
# Add remaining number of heroes to the next byte if needed (happens with 8+ heroes)
self._add_remaining_bits_from_number(len(self.heroes), 3)
# Serialize and append heroes
previous_card_id = 0
for hero in self.heroes:
self._add_card(hero['turn'], hero['id'] - previous_card_id)
previous_card_id = hero['id']
# Serialize and append cards
previous_card_id = 0
for card in self.cards:
self._add_card(card['count'], card['id'] - previous_card_id)
previous_card_id = card['id']
# We'll finish of the binary data with the name,
# note down at which index we'll be starting to not include it in the checksum
name_start_index = len(self._binary)
name_bytes = bytearray(self.name.encode())
self._binary += name_bytes
# Compute the checksum and replace the placeholder
self._binary[checksum_index] = sum(b for b in self._binary[self.header_size:name_start_index]) & 255
return self._binary
def _add_remaining_bits_from_number(self, value: int, already_written_bits: int) -> None:
"""Adds the remaining bits from the number we extracted some bits from previously."""
value >>= already_written_bits
while value > 0:
next_byte = _extract_n_bits(value, 7)
value >>= 7
self._binary.append(next_byte)
def _add_card(self, count_or_turn: int, value: int) -> None:
"""Adds a card to the bytearray"""
# Note down the index we start at
bytes_start = len(self._binary)
# max count in the first byte
first_byte_max_count = 3
# Whether the count (or turn) exceeds the max
extended_count = ((count_or_turn - 1) >= first_byte_max_count)
# If up to number 3 was provided as first argument, we us that -1, otherwise we use the maximum - 3
first_byte_count = first_byte_max_count if extended_count else (count_or_turn - 1)
# This ends up being either 64, 128 or 192 depending on the count
first_byte = first_byte_count << 6
# We bitwise or the number we got with the first 5 bits of the value (card id difference)
first_byte |= _extract_n_bits(value, 5)
# And write the first byte into our array
self._binary.append(first_byte)
# After the first byte we add the value (difference in card_id from the previous card_id)
self._add_remaining_bits_from_number(value, 5)
# If we couldn't fit the count (or turn) into the first byte and we used 3, we need to add it here
if extended_count:
self._add_remaining_bits_from_number(count_or_turn, 0)
count_bytes_end = len(self._binary)
# If we exceeded 11 bytes, we are doomed, probably api version v3 will be needed by then, no instructions now
if count_bytes_end - bytes_start > 11:
raise DeckEncodeException("Something went horribly wrong")
def _extract_n_bits(value: int, num_bits: int) -> int:
"""Extracts n bits from a number"""
limit_bit = 1 << num_bits
result = value & (limit_bit - 1)
if value >= limit_bit:
result |= limit_bit
return result