PK!#xpyartifact/.DS_StoreBud1%typesfdscboolsfdscbool  @ @ @ @ E%DSDB` @ @ @PK!ޣpyartifact/__init__.py__version__ = '0.2.0' __all__ = ['Cards', 'CardFilter', 'Hero', 'Ability', 'PassiveAbility', 'Spell', 'Creep', 'Improvement', 'Item', 'Deck', 'decode_deck_string', 'encode_deck'] from .api_sync import Cards from .deck import Deck from .filtering import CardFilter from .sets_and_cards import Hero, Ability, PassiveAbility, Spell, Creep, Improvement, Item from .deck_encoding.decode import decode_deck_string from .deck_encoding.encode import encode_deck PK!Epyartifact/api_sync.pyfrom typing import List, Optional from .sets_and_cards import CardSet, CardTypesInstanced from .filtering import CardFilter from .context import ctx class Cards: """ Synchronous API around Artifact API sets of cards """ def __init__( self, limit_sets: Optional[List[str]] = None, localize: Optional[str] = None, ) -> None: """ :param limit_sets: Whether to only fetch some sets, by default all ar used ('00', and '01') :param localize: Which language to fetch strings for, at this time only english is available :param raw_data: Whether you want to use the raw json data from the api, or the object mapping provided by this library """ self._set_numbers = limit_sets or ['00', '01'] self.sets: List[CardSet] = [CardSet(set_number) for set_number in self._set_numbers] if localize is not None: ctx.language = localize def load_all_sets(self) -> None: ctx.cards_by_id = {} ctx.cards_by_name = {} for set in self.sets: set.load() @property def filter(self) -> 'CardFilter': return CardFilter(sets=self.sets) def get(self, name: str) -> 'CardTypesInstanced': return ctx.cards_by_name[name.lower()] def find(self, name_approx: str) -> 'CardTypesInstanced': raise NotImplementedError('Card lookup with approx. names is not yet implemented.') PK!kpyartifact/context.pyfrom typing import Any, Dict class Context: def __init__(self, language: str = 'english') -> None: self.language: str = language self.cards_by_id: Dict[int, Any] = {} self.cards_by_name: Dict[str, Any] = {} ctx = Context() PK!C 66pyartifact/deck.py""" TODO this while thing needs to be re-thought and rewritten """ from typing import List, Optional from .context import ctx from .deck_encoding.decode import decode_deck_string from .deck_encoding.encode import encode_deck from .deck_encoding.types import CardDecodedType, DeckContents from .exceptions import PyArtifactException, InvalidHeroTurn, HeroTurnFull from .sets_and_cards import Hero, CardTypesInstanced class CardEnrichedType(CardDecodedType): card: CardTypesInstanced class HeroDeployment: def __init__( self, turn_one: Optional[List[Optional[Hero]]] = None, turn_two: Optional[Hero] = None, turn_three: Optional[Hero] = None ) -> None: self.turn_one = turn_one if turn_one is not None else [None, None, None] self.turn_two = turn_two self.turn_three = turn_three @classmethod def from_list(cls, list_of_heroes: List[Optional[Hero]]) -> 'HeroDeployment': new = cls() for hero in list_of_heroes: new.add(hero) return new @property def valid(self) -> bool: """ Whether the hero deployment is valid - all positions are filled """ turn_one_heroes = len([h for h in self.turn_one if isinstance(h, Hero)]) == 3 turn_two_hero = isinstance(self.turn_two, Hero) turn_three_hero = isinstance(self.turn_three, Hero) return turn_one_heroes and turn_two_hero and turn_three_hero @property def as_list(self) -> List[Optional[Hero]]: """ Reruns the hero deployments as a list, where indexes 0-2 are turn one, 1 is turn two, 2 is turn three. If no hero is in a position, None is in it's place instead. The returned list is a standalone instance, if you want to apply it, construct new HeroDeployment `from_list` """ as_list = self.turn_one.copy() as_list.append(self.turn_two) as_list.append(self.turn_three) return as_list @property def free_turns(self) -> List[int]: free_turns = [] if len([h for h in self.turn_one if h is not None]) < 3: free_turns.append(1) if self.turn_two is None: free_turns.append(2) if self.turn_three is None: free_turns.append(3) return free_turns def add(self, hero: Optional[Hero], turn: Optional[int] = None) -> None: """ Adds a hero. :param hero: Instance of the hero, or None if you so desire. :param turn: Which turn the hero will be deployed, otherwise first free will be automatically selected """ turns = { 1: self.turn_one, 2: self.turn_two, 3: self.turn_three, None: None } if turn is None: if self.free_turns: turn = self.free_turns[0] else: raise HeroTurnFull if turn not in turns.keys(): raise InvalidHeroTurn if turns[turn] is self.turn_one: if len([h for h in self.turn_one if h is not None]) >= 3: raise HeroTurnFull else: self.turn_one[self.turn_one.index(None)] = hero else: if turns[turn] is not None: raise HeroTurnFull else: turns[turn] = hero class Deck: def __init__(self, name: str, heroes: List[Optional[Hero]], cards: List[CardEnrichedType], deck_contents: DeckContents) -> None: # Take this with a big grain of salt for now self.name = name self._heroes = HeroDeployment.from_list(heroes) self._cards = cards self._deck_contents = deck_contents def __repr__(self) -> str: return f'' @classmethod def from_code(cls, deck_code: str) -> 'Deck': if not ctx.cards_by_id: raise PyArtifactException("Deck constructor `from_code` can't be used without Card sets being loaded." "To get raw data use the function `pyartifact.decode_deck_string(deck_string)`") deck_contents = decode_deck_string(deck_code) hero_deployment = HeroDeployment() for hero in deck_contents['heroes']: hero_deployment.add(ctx.cards_by_id[hero['id']], turn=hero['turn']) enriched_cards: List[CardEnrichedType] = [] for card in deck_contents['cards']: enriched_card: CardEnrichedType = dict(id=card['id'], count=card['count'], card=ctx.cards_by_id[card['id']]) enriched_cards.append(enriched_card) return cls(name=deck_contents['name'], heroes=hero_deployment.as_list, cards=enriched_cards, deck_contents=deck_contents) @property def deck_code(self) -> str: return encode_deck(self._deck_contents) @property def cards(self) -> List[CardEnrichedType]: return self._cards @property def cards_expanded(self) -> List[CardTypesInstanced]: expanded = [] for card in self._cards: for _ in range(card['count']): expanded.append(card['card']) for hero in self._heroes.as_list: if hero is not None: expanded += hero.includes return expanded def add_hero(self, hero: Hero, turn: Optional[int] = None) -> None: self._heroes.add(hero, turn) PK!$pyartifact/deck_encoding/__init__.pyPK!>"pyartifact/deck_encoding/decode.pyimport re from base64 import b64decode from typing import List, Tuple from .types import HeroDecodedType, CardDecodedType, DeckContents from ..exceptions import InvalidDeckString, DeckDecodeException ENCODED_PREFIX = 'ADC' # Version 1: Heroes and Cards # Version 2: Name, Heroes and Cards SUPPORTED_VERSIONS = [1, 2] def decode_deck_string(deck_code: str) -> DeckContents: """ Takes in deck code, e.g. 'ADCJWkTZX05uwGDCRV4XQGy3QGLmqUBg4GQJgGLGgO7AaABR3JlZW4vQmxhY2sgRXhhbXBsZQ__' and decodes it into a dict of name, heroes and cards. :param deck_code: Deck code :return: Name, heroes and cards in a dict :raises InvalidDeckString: When an invalid deck string is provided, e.g. unknown version, bad checksum etc. :raises DeckDecodeException: When something odd happens while decoding, possible an error in this library """ deck_bytes = _decode_string(deck_code) if not deck_bytes: raise InvalidDeckString("No deck bytes could be decoded from the string") name, heroes, cards = _parse_deck(deck_bytes) return DeckContents(name=name, heroes=heroes, cards=cards) def _decode_string(deck_code: str) -> bytearray: if not deck_code.startswith(ENCODED_PREFIX): raise InvalidDeckString("The provided deck string doesn't start with a known prefix") # Strip prefix deck_code = deck_code.lstrip(ENCODED_PREFIX) # Make valid base64 from url-safe string deck_code = deck_code.replace('-', '/').replace('_', '=') decoded = b64decode(deck_code) return bytearray(decoded) def _read_bits_chunk(chunk: int, numb_bits: int, curr_shift: int, out_bits: int) -> Tuple[int, bool]: continue_bit = 1 << numb_bits new_bits = chunk & (continue_bit - 1) out_bits |= (new_bits << curr_shift) return out_bits, (chunk & continue_bit) != 0 def _read_var_encoded(base_value: int, base_bits: int, data: bytearray, index_start: int, index_end: int) -> Tuple[int, int]: out_value = 0 delta_shift = 0 out_value, unknown_thing = _read_bits_chunk(base_value, base_bits, delta_shift, out_value) if (base_bits == 0) or unknown_thing: delta_shift += base_bits while True: if index_start > index_end: raise DeckDecodeException("Invalid indexes during reading variable.") next_byte = data[index_start] index_start += 1 out_value, unknown_thing = _read_bits_chunk(next_byte, 7, delta_shift, out_value) if not unknown_thing: break delta_shift += 7 return out_value, index_start def _parse_deck(deck_bytes: bytearray) -> Tuple[str, List[HeroDecodedType], List[CardDecodedType]]: total_bytes: int = len(deck_bytes) # Variables that will be overwritten in the first loop over the bytes computed_checksum: int = 0 total_card_bytes: int = 0 checksum: int = 0 version_and_heroes: int = 0 string_length: int = 0 version: int = 0 # By default, card bytes start at index 3, except in version 1, where it starts at 2, # because it doesn't carry the name of the deck (and therefore the length of the name) current_index: int = 3 for index, deck_byte in enumerate(deck_bytes): if index == 0: version_and_heroes = deck_byte version = version_and_heroes >> 4 if version not in SUPPORTED_VERSIONS: raise InvalidDeckString(f"Deck string has incompatible version '{version}', " f"supported versions are: {SUPPORTED_VERSIONS}") elif index == 1: checksum = deck_byte elif index == 2: string_length = 0 if version > 1: string_length = deck_byte else: computed_checksum += deck_byte current_index = 2 total_card_bytes = total_bytes - string_length elif index in range(3, total_card_bytes): computed_checksum += deck_byte else: break masked = computed_checksum & 0xFF if checksum != masked: raise InvalidDeckString("Checksum is wrong") number_of_heroes, current_index = _read_var_encoded(version_and_heroes, 3, deck_bytes, current_index, total_card_bytes) heroes: List[HeroDecodedType] = [] previous_card_base: int = 0 for i in range(number_of_heroes): current_index, previous_card_base, hero_turn, hero_card_id = _read_serialized_card(deck_bytes, current_index, total_card_bytes, previous_card_base) heroes.append(dict(id=hero_card_id, turn=hero_turn)) cards: List[CardDecodedType] = [] previous_card_base = 0 while current_index < total_card_bytes: current_index, previous_card_base, card_count, card_id = _read_serialized_card(deck_bytes, current_index, total_bytes, previous_card_base) cards.append(dict(id=card_id, count=card_count)) name = '' if current_index < total_bytes: some_bytes = deck_bytes[(-1 * string_length):] some_chars = map(chr, some_bytes) name = ''.join(some_chars) # WIP simple sanitizer name = re.sub('<[^<]+?>', '', name) return name, heroes, cards def _read_serialized_card(data: bytearray, index_start: int, index_end: int, prev_card_base: int) -> Tuple[int, int, int, int]: if index_start > index_end: raise DeckDecodeException("Invalid indexes during reading serialized card") # Header contains the count (2 bits), a continue flag, and 5 bits of offset data. # If we have 11 for the count bits we have the count encoded after the offset. header = data[index_start] index_start += 1 has_extended_count = (header >> 6) == 0x03 # Read in the delta, which has 5 bits in the header, then additional bytes while the value is set card_delta, index_start = _read_var_encoded(header, 5, data, index_start, index_end) out_card_id = prev_card_base + card_delta if has_extended_count: out_count, index_start = _read_var_encoded(0, 0, data, index_start, index_end) else: out_count = (header >> 6) + 1 prev_card_base = out_card_id return index_start, prev_card_base, out_count, out_card_id PK!F"pyartifact/deck_encoding/encode.pyimport re from base64 import b64encode from .types import DeckContents ENCODED_PREFIX = 'ADC' # Version 1: Heroes and Cards # Version 2: Name, Heroes and Cards SUPPORTED_VERSIONS = [2] MAX_BYTES_FOR_VAR = 5 HEADER_SIZE = 3 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 """ encoder = Encoder(deck_contents, version=version) return encoder.deck_code class Encoder: def __init__(self, deck_contents: DeckContents, version: int = SUPPORTED_VERSIONS[-1]) -> None: self.version = version self.deck_contents = deck_contents self.heroes = sorted(deck_contents['heroes'], key=lambda x: x['id']) self.cards = sorted(deck_contents['cards'], key=lambda x: x['id']) self.name = deck_contents.get('name', '') if self.name: self.name = re.sub('<[^<]+?>', '', self.name) self.byte_array = bytearray() @property def deck_code(self) -> str: # So we don't have to call encode manually every time if not self.byte_array: self.encode() encoded = b64encode(bytes(self.byte_array)).decode() deck_code = (ENCODED_PREFIX + encoded) deck_code = deck_code.replace('/', '-').replace('=', '_') return deck_code def encode(self) -> bytearray: version = (self.version << 4) | _extract_n_bits_with_carry(len(self.heroes), 3) self.byte_array.append(version) dummy_checksum = 0 checksum_byte = len(self.byte_array) self.byte_array.append(dummy_checksum) if self.name: trim_len = len(self.name) while trim_len > 63: amount_to_trim = int((trim_len - 63) / 4) amount_to_trim = amount_to_trim if (amount_to_trim > 1) else 1 self.name = self.name[:len(self.name) - amount_to_trim] self.byte_array.append(len(self.name)) self._add_remaining_number_to_buffer(len(self.heroes), 3) previous_card_id = 0 for hero in self.heroes: self._add_card_to_buffer(hero['turn'], hero['id'] - previous_card_id) previous_card_id = hero['id'] previous_card_id = 0 for card in self.cards: self._add_card_to_buffer(card['count'], card['id'] - previous_card_id) previous_card_id = card['id'] string_byte_count = len(self.byte_array) name_bytes = bytearray(self.name.encode()) for name_byte in name_bytes: self.byte_array.append(name_byte) full_checksum = self._compute_checksum(string_byte_count - HEADER_SIZE) small_checksum = full_checksum & 0x0FF self.byte_array[checksum_byte] = small_checksum return self.byte_array def _compute_checksum(self, number_of_bytes: int) -> int: checksum = 0 for index in range(HEADER_SIZE, number_of_bytes + HEADER_SIZE): single_byte = self.byte_array[index] checksum += single_byte return checksum def _add_remaining_number_to_buffer(self, value: int, already_written_bits: int) -> None: value >>= already_written_bits # num_bytes seem unused, we'll see num_bytes = 0 while value > 0: next_byte = _extract_n_bits_with_carry(value, 7) value >>= 7 self.byte_array.append(next_byte) num_bytes += 1 def _add_card_to_buffer(self, count, value) -> None: bytes_start = len(self.byte_array) first_byte_max_count = 0x03 extended_count = ((count - 1) >= first_byte_max_count) first_byte_count = first_byte_max_count if extended_count else (count - 1) first_byte = first_byte_count << 6 first_byte |= _extract_n_bits_with_carry(value, 5) self.byte_array.append(first_byte) self._add_remaining_number_to_buffer(value, 5) if extended_count: self._add_remaining_number_to_buffer(count, 0) count_bytes_end = len(self.byte_array) if count_bytes_end - bytes_start > 11: raise Exception("This library didn't work properly") def _extract_n_bits_with_carry(value: int, num_bits: int) -> int: limit_bit = 1 << num_bits result = value & (limit_bit - 1) if value >= limit_bit: result |= limit_bit return result PK!RTgg!pyartifact/deck_encoding/types.pyfrom typing import List from mypy_extensions import TypedDict class HeroDecodedType(TypedDict): id: int turn: int class CardDecodedType(TypedDict): id: int count: int class DeckContentsBase(TypedDict): cards: List[CardDecodedType] heroes: List[HeroDecodedType] class DeckContents(DeckContentsBase, total=False): name: str PK!3+*pyartifact/exceptions.pyclass PyArtifactException(Exception): pass class FilterException(PyArtifactException): pass class UnknownFilterArgument(FilterException): pass class InvalidDeck(PyArtifactException): pass class InvalidDeckString(InvalidDeck): pass class DeckDecodeException(PyArtifactException): pass class DeckBuilding(InvalidDeck): pass class InvalidHeroTurn(DeckBuilding): pass class HeroTurnFull(DeckBuilding): pass PK!,nFM!M!pyartifact/filtering.pyfrom typing import Optional, Iterable, Set, Union, List, Tuple, cast from .exceptions import UnknownFilterArgument from .sets_and_cards import CardSet, CardTypes, CardTypesInstanced, AVAILABLE_TYPES, STR_TO_CARD_TYPE, Improvement, \ Creep, Spell, Item, Hero def convert_card_type(type_: Union[str, CardTypes]) -> CardTypes: if isinstance(type_, AVAILABLE_TYPES): return type_ else: type_ = cast(str, type_) try: return STR_TO_CARD_TYPE[type_] except KeyError: raise UnknownFilterArgument(f'Unrecognized card type: {type_}') def convert_card_types(card_types: Iterable[Union[str, CardTypes]]) -> Tuple[CardTypes, ...]: return tuple([convert_card_type(ct) for ct in card_types]) class CardFilter: def __init__( self, sets: Optional[Iterable[CardSet]] = None, cards: Optional[Iterable[CardTypesInstanced]] = None ) -> None: self.cards: Set[CardTypesInstanced] = set(cards) if cards else set() if sets: for card_set in sets: for card in card_set.data.card_list: # type: ignore self.cards.add(card) self._filtered: List[CardTypesInstanced] = [] def __len__(self) -> int: return len(self.cards) def __str__(self) -> str: return f'' def __getitem__(self, item: int) -> 'CardTypesInstanced': return list(self.cards)[item] def type(self, type_: Union[str, CardTypes], filter_out=False) -> 'CardFilter': type_ = convert_card_type(type_) for card in self.cards: if filter_out: if not isinstance(card, type_): self._filtered.append(card) else: if isinstance(card, type_): self._filtered.append(card) return CardFilter(cards=self._filtered) def types_in(self, card_types: Iterable[Union[str, CardTypes]]) -> 'CardFilter': """ Filters out cards that were not passed to this filter :param card_types: Either strings of card types, or this library's classes of card types """ card_types = convert_card_types(card_types) for card in self.cards: if isinstance(card, card_types): self._filtered.append(card) return CardFilter(cards=self._filtered) def types_not_in(self, card_types: Iterable[Union[str, CardTypes]]) -> 'CardFilter': """ Filters out cards that were passed into this filter :param card_types: Either strings of card types, or this library's classes of card types """ card_types = convert_card_types(card_types) for card in self.cards: if not isinstance(card, card_types): self._filtered.append(card) return CardFilter(cards=self._filtered) def rarity(self, rarity: str) -> 'CardFilter': for card in self.cards: if hasattr(card, 'rarity'): card = cast(Union[Hero, Creep, Improvement, Improvement], card) if card.rarity == rarity: self._filtered.append(card) return CardFilter(cards=self._filtered) def rarity_in(self, rarities: List[str]) -> 'CardFilter': for card in self.cards: if hasattr(card, 'rarity'): card = cast(Union[Hero, Creep, Improvement, Improvement], card) if card.rarity in rarities: self._filtered.append(card) return CardFilter(cards=self._filtered) def rarity_not_in(self, rarities: List[str]) -> 'CardFilter': for card in self.cards: if hasattr(card, 'rarity'): card = cast(Union[Hero, Creep, Improvement, Improvement], card) if card.rarity not in rarities: self._filtered.append(card) return CardFilter(cards=self._filtered) def color(self, color: str) -> 'CardFilter': for card in self.cards: if hasattr(card, 'color'): card = cast(Union[Hero, Spell, Creep, Improvement], card) if card.color == color.lower(): self._filtered.append(card) return CardFilter(cards=self._filtered) def color_in(self, colors: Iterable[str]) -> 'CardFilter': colors = [color.lower() for color in colors] for card in self.cards: if hasattr(card, 'color'): card = cast(Union[Hero, Spell, Creep, Improvement], card) if card.color in colors: self._filtered.append(card) return CardFilter(cards=self._filtered) def color_not_in(self, colors: Iterable[str]) -> 'CardFilter': colors = [color.lower() for color in colors] for card in self.cards: if hasattr(card, 'color'): card = cast(Union[Hero, Spell, Creep, Improvement], card) if card.color not in colors: self._filtered.append(card) return CardFilter(cards=self._filtered) def mana_cost( self, gt: Optional[int] = None, gte: Optional[int] = None, lt: Optional[int] = None, lte: Optional[int] = None, eq: Optional[int] = None ) -> 'CardFilter': """ Filters out cards by their mana cost, if they have one. This will always filter out cards without mana cost. If multiple arguments are passed, every card that fits at least one will pass the filter. :param gt: Filters out cards that have higher mana cost than the number provided :param gte: Filters out cards that have higher or equal mana cost than the number provided :param lt: Filters out cards that have lower mana cost than the number provided :param lte: Filters out cards that have lower or equal mana cost than the number provided :param eq: Filters out cards that have mana cost equal to the number provided """ for card in self.cards: if hasattr(card, 'mana_cost'): card = cast(Union[Spell, Creep, Improvement], card) if gt is not None and card.mana_cost > gt: self._filtered.append(card) elif gte is not None and card.mana_cost >= gte: self._filtered.append(card) elif lt is not None and card.mana_cost < lt: self._filtered.append(card) elif lte is not None and card.mana_cost <= lte: self._filtered.append(card) elif eq is not None and card.mana_cost == eq: self._filtered.append(card) return CardFilter(cards=self._filtered) def gold_cost( self, gt: Optional[int] = None, gte: Optional[int] = None, lt: Optional[int] = None, lte: Optional[int] = None, eq: Optional[int] = None ) -> 'CardFilter': """ Filters out cards by their gold cost, if they have one. This will always filter out cards without gold cost. If multiple arguments are passed, every card that fits at least one will pass the filter. :param gt: Filters out cards that have higher gold cost than the number provided :param gte: Filters out cards that have higher or equal gold cost than the number provided :param lt: Filters out cards that have lower gold cost than the number provided :param lte: Filters out cards that have lower or equal gold cost than the number provided :param eq: Filters out cards that have gold cost equal to the number provided """ for card in self.cards: if hasattr(card, 'gold_cost'): card = cast(Item, card) if gt is not None and card.gold_cost > gt: self._filtered.append(card) elif gte is not None and card.gold_cost >= gte: self._filtered.append(card) elif lt is not None and card.gold_cost < lt: self._filtered.append(card) elif lte is not None and card.gold_cost <= lte: self._filtered.append(card) elif eq is not None and card.gold_cost == eq: self._filtered.append(card) return CardFilter(cards=self._filtered) PK!Ąpyartifact/sets_and_cards.pyfrom typing import Optional, List, Union, Type, Dict import requests from .context import ctx from .types.json_types import CardSetType, SetInfoType, SetDataType, ReferenceType class CardBase: """Each card has this.""" def __init__(self, **kwargs) -> None: self.id: int = kwargs['card_id'] self.base_id: int = kwargs['base_card_id'] self.name: str = kwargs['card_name'][ctx.language] self.type: str = kwargs['card_type'] self.text: str = kwargs['card_text'].get(ctx.language, '') self.mini_image: str = kwargs['mini_image'].get('default') self.large_image: str = kwargs['large_image'].get('default') self.ingame_image: str = kwargs['ingame_image'].get('default') self.__references: List[ReferenceType] = kwargs['references'] def __str__(self) -> str: return self.name def __repr__(self) -> str: return f'' @property def includes(self) -> List['CardTypesInstanced']: includes = [] for ref in self.__references: if ref['ref_type'] == 'includes': included = ctx.cards_by_id[ref['card_id']] for _ in range(ref['count']): includes.append(included) return includes @property def passive_abilities(self) -> List['PassiveAbility']: passive_abilities = [] for ref in self.__references: if ref['ref_type'] == 'passive_ability': ability = ctx.cards_by_id[ref['card_id']] passive_abilities.append(ability) return passive_abilities @property def active_abilities(self) -> List['Ability']: abilities = [] for ref in self.__references: if ref['ref_type'] == 'active_ability': ability = ctx.cards_by_id[ref['card_id']] abilities.append(ability) return abilities @property def references(self) -> List['CardTypesInstanced']: references = [] for ref in self.__references: if ref['ref_type'] == 'references': reference = ctx.cards_by_id[ref['card_id']] references.append(reference) return references class ColoredCard: def __init__(self, **kwargs) -> None: if kwargs.get('is_blue', False): self.color: str = 'blue' elif kwargs.get('is_black', False): self.color = 'black' elif kwargs.get('is_red', False): self.color = 'red' elif kwargs.get('is_green', False): self.color = 'green' else: # in case future sets introduce new colors, this way it won't break this library self.color = 'unknown' class Unit: def __init__(self, **kwargs) -> None: self.attack: int = kwargs.get('attack', 0) self.armor: int = kwargs.get('armor', 0) self.hit_points: int = kwargs.get('hit_points', 0) class Collectible: """Doesn't mean that EVERY card of that type is Collectible, all have exceptions""" def __init__(self, **kwargs) -> None: self.rarity: Optional[str] = kwargs.get('rarity') self.item_def: Optional[int] = kwargs.get('item_def') class Castable: def __init__(self, **kwargs) -> None: self.mana_cost: int = kwargs['mana_cost'] class Hero(CardBase, ColoredCard, Unit, Collectible): def __init__(self, **kwargs) -> None: CardBase.__init__(self, **kwargs) ColoredCard.__init__(self, **kwargs) Unit.__init__(self, **kwargs) Collectible.__init__(self, **kwargs) self.illustrator: str = kwargs['illustrator'] class PassiveAbility(CardBase): def __init__(self, **kwargs) -> None: CardBase.__init__(self, **kwargs) class Spell(CardBase, ColoredCard, Collectible, Castable): def __init__(self, **kwargs) -> None: CardBase.__init__(self, **kwargs) ColoredCard.__init__(self, **kwargs) Collectible.__init__(self, **kwargs) Castable.__init__(self, **kwargs) self.illustrator: str = kwargs['illustrator'] class Creep(CardBase, ColoredCard, Unit, Collectible, Castable): def __init__(self, **kwargs): CardBase.__init__(self, **kwargs) ColoredCard.__init__(self, **kwargs) Unit.__init__(self, **kwargs) Collectible.__init__(self, **kwargs) Castable.__init__(self, **kwargs) self.illustrator: Optional[str] = kwargs.get('illustrator') class Ability(CardBase): def __init__(self, **kwargs) -> None: CardBase.__init__(self, **kwargs) class Item(CardBase, Collectible): def __init__(self, **kwargs) -> None: CardBase.__init__(self, **kwargs) Collectible.__init__(self, **kwargs) self.illustrator: str = kwargs['illustrator'] self.gold_cost: int = kwargs['gold_cost'] self.sub_type: str = kwargs['sub_type'] class Improvement(CardBase, ColoredCard, Collectible, Castable): def __init__(self, **kwargs) -> None: CardBase.__init__(self, **kwargs) ColoredCard.__init__(self, **kwargs) Collectible.__init__(self, **kwargs) Castable.__init__(self, **kwargs) self.illustrator: str = kwargs['illustrator'] class SetInfo: def __init__(self, set_info: SetInfoType) -> None: self.set_id: int = set_info['set_id'] self.pack_item_def: int = set_info['pack_item_def'] self.name: str = set_info['name'][ctx.language] CardTypesInstanced = Union[Item, Hero, Ability, PassiveAbility, Improvement, Creep, Spell] CardTypes = Union[ Type[Item], Type[Hero], Type[Ability], Type[PassiveAbility], Type[Improvement], Type[Creep], Type[Spell] ] STR_TO_CARD_TYPE = { 'Hero': Hero, 'Passive Ability': PassiveAbility, 'Spell': Spell, 'Creep': Creep, 'Ability': Ability, 'Item': Item, 'Improvement': Improvement } # type: Dict[str, CardTypes] AVAILABLE_TYPES = (Item, Hero, Ability, PassiveAbility, Improvement, Creep, Spell) class CardSetData: # Stronghold and Pathing are core game mechanics, there's no need to be indexing them not_indexed = ['Stronghold', 'Pathing'] def __init__(self, data: CardSetType) -> None: self.version: int = data['version'] self.set_info = SetInfo(data['set_info']) self.card_list: List[CardTypesInstanced] = [] for card in data['card_list']: if card['card_type'] not in self.not_indexed: type_of_card = STR_TO_CARD_TYPE[card['card_type']] # type: CardTypes card_instance = type_of_card(**card) self.card_list.append(card_instance) # For fast lookups ctx.cards_by_id[card['base_card_id']] = card_instance ctx.cards_by_name[card['card_name'][ctx.language].lower()] = card_instance class CardSet: base_url = 'https://playartifact.com/cardset/' def __init__(self, set_number: str) -> None: self.url = f'{self.base_url}{set_number}' self.expire_time = None self.data: Optional[CardSetData] = None def load(self) -> None: cdn_info = requests.get(self.url).json() self.expire_time = cdn_info['expire_time'] data = requests.get(f"{cdn_info['cdn_root']}{cdn_info['url']}").json() # type: SetDataType self.data = CardSetData(data['card_set']) PK!pyartifact/types/.DS_StoreBud1_encod deck_encodingfdscbool  @ @ @ @ EDSDB ` @ @ @PK!pyartifact/types/__init__.pyPK!t))pyartifact/types/json_types.pyfrom typing import List, Dict from mypy_extensions import TypedDict # Key is language (atm only english), value is text, which includes html LocalizedText = Dict[str, str] # Key is so far only default, value is the url ImageUrl = Dict[str, str] class ReferenceTypeBase(TypedDict): card_id: int ref_type: str # includes, passive_ability, active_ability, references class ReferenceType(ReferenceTypeBase, total=False): count: int class SetInfoType(TypedDict): set_id: int pack_item_def: int name: LocalizedText class CardListDataType(TypedDict, total=False): card_id: int base_card_id: int card_type: str # Hero, Passive Ability, Spell, Creep, Ability, Item, Improvement sub_type: str gold_cost: int mana_cost: int card_name: LocalizedText card_text: LocalizedText mini_image: ImageUrl large_image: ImageUrl ingame_image: ImageUrl illustrator: str rarity: str # Common, Uncommon, Rare is_blue: bool is_red: bool is_black: bool is_green: bool item_def: int attack: int armor: int hit_points: int references: List[ReferenceType] class CardSetType(TypedDict): version: int set_info: SetInfoType card_list: List[CardListDataType] class SetDataType(TypedDict): card_set: CardSetType PK!%/88"pyartifact-0.2.0.dist-info/LICENSEThe MIT License (MIT) Copyright (c) 2018 David Jetelina Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.PK!HW"TT pyartifact-0.2.0.dist-info/WHEEL A н#J."jm)Afb~ ڡ5 G7hiޅF4+-3ڦ/̖?XPK!H.[ y #pyartifact-0.2.0.dist-info/METADATAVr6}WɃDQ$ۊVN|Q@rE! 0{d$qwsv`Y,k߀6\#ң] `DTɴ3[I "˘.G䢴s,4S 4aZyBnx!28NdmRt=VQr;/"ˬMu6rOIG3 xΖ6dIRK;LuD!Xǡ` FF#)(#$ïώ\hjeڇ2Gw}U*â!^"SV@{PEH" qaAKBdRZc.D͙e̶H{LAȍexOw7WYJ͜H rƎͰB3g :La- 54Ia7A௝aJF]T%@YXtr.,}%93K@bD#ɌiÇBde1߄gJjKNLe_d4 ɒ)bj4nlqiȥۆOy"s܁ƈtA"A>丌qAtnsF5sT D%@bk\3 [ o̐8HU,.vW18`{w~Lt S$"쯒 -dfkIr6qBXl錟99h|{5Ղ(/\ RsI$>mv*21y5\=ʷuXRWAGVG)%OႳN]́"o53Ʊm#@.bdasSsiqs5 1Sl :^nklL^= }ڛ݃bݞ\?b'{NIn"zlN]곸wX>8t!"ǟX|I [.~8ms0$_ӄW5g`}V+~nԲ!}^{?nWn#y[RM\.ro |$AurcV"JlF ެD˄OnqP-6-Rkq"-7pyartifact/deck_encoding/decode.pyPK!F"KRpyartifact/deck_encoding/encode.pyPK!RTgg!dpyartifact/deck_encoding/types.pyPK!3+*Cfpyartifact/exceptions.pyPK!,nFM!M!@hpyartifact/filtering.pyPK!Ą‰pyartifact/sets_and_cards.pyPK!pyartifact/types/.DS_StorePK!pyartifact/types/__init__.pyPK!t))Xpyartifact/types/json_types.pyPK!%/88"pyartifact-0.2.0.dist-info/LICENSEPK!HW"TT 5pyartifact-0.2.0.dist-info/WHEELPK!H.[ y #pyartifact-0.2.0.dist-info/METADATAPK!HL Y!pyartifact-0.2.0.dist-info/RECORDPK