Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support sideboard deckstrings #42

Merged
merged 1 commit into from
Jun 1, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 88 additions & 13 deletions hearthstone/deckstrings.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

import base64
from io import BytesIO
from typing import IO, List, Tuple
from typing import IO, List, Optional, Sequence, Tuple

from .enums import FormatType

Expand All @@ -14,6 +14,7 @@

CardList = List[int]
CardIncludeList = List[Tuple[int, int]]
SideboardList = List[Tuple[int, int, int]]


def _read_varint(stream: IO) -> int:
Expand Down Expand Up @@ -50,42 +51,64 @@ class Deck:
@classmethod
def from_deckstring(cls, deckstring: str) -> "Deck":
instance = cls()
instance.cards, instance.heroes, instance.format = parse_deckstring(deckstring)
(
instance.cards,
instance.heroes,
instance.format,
instance.sideboard,
) = parse_deckstring(deckstring)
return instance

def __init__(self):
self.cards: CardIncludeList = []
self.sideboard: SideboardList = []
self.heroes: CardList = []
self.format: FormatType = FormatType.FT_UNKNOWN

@property
def as_deckstring(self) -> str:
return write_deckstring(self.cards, self.heroes, self.format)
return write_deckstring(self.cards, self.heroes, self.format, self.sideboard)

def get_dbf_id_list(self) -> CardIncludeList:
return sorted(self.cards, key=lambda x: x[0])

def get_sideboard_dbf_id_list(self) -> SideboardList:
return sorted(self.sideboard, key=lambda x: x[0])

def trisort_cards(cards: CardIncludeList) -> Tuple[
CardIncludeList, CardIncludeList, CardIncludeList

def trisort_cards(cards: Sequence[tuple]) -> Tuple[
List[tuple], List[tuple], List[tuple]
]:
cards_x1: CardIncludeList = []
cards_x2: CardIncludeList = []
cards_xn: CardIncludeList = []
cards_x1: List[tuple] = []
cards_x2: List[tuple] = []
cards_xn: List[tuple] = []

for card_elem in cards:
sideboard_owner = None
if len(card_elem) == 3:
# Sideboard
cardid, count, sideboard_owner = card_elem
else:
cardid, count = card_elem

for cardid, count in cards:
if count == 1:
list = cards_x1
elif count == 2:
list = cards_x2
else:
list = cards_xn
list.append((cardid, count))

if len(card_elem) == 3:
list.append((cardid, count, sideboard_owner))
else:
list.append((cardid, count))

return cards_x1, cards_x2, cards_xn


def parse_deckstring(deckstring) -> Tuple[CardIncludeList, CardList, FormatType]:
def parse_deckstring(deckstring) -> (
Tuple[CardIncludeList, CardList, FormatType, SideboardList]
):
decoded = base64.b64decode(deckstring)
data = BytesIO(decoded)

Expand Down Expand Up @@ -124,10 +147,42 @@ def parse_deckstring(deckstring) -> Tuple[CardIncludeList, CardList, FormatType]
count = _read_varint(data)
cards.append((card_id, count))

return cards, heroes, format
sideboard = []

has_sideboard = data.read(1) == b"\1"

if has_sideboard:
num_sideboard_x1 = _read_varint(data)
for i in range(num_sideboard_x1):
card_id = _read_varint(data)
sideboard_owner = _read_varint(data)
sideboard.append((card_id, 1, sideboard_owner))

num_sideboard_x2 = _read_varint(data)
for i in range(num_sideboard_x2):
card_id = _read_varint(data)
sideboard_owner = _read_varint(data)
sideboard.append((card_id, 2, sideboard_owner))

num_sideboard_xn = _read_varint(data)
for i in range(num_sideboard_xn):
card_id = _read_varint(data)
count = _read_varint(data)
sideboard_owner = _read_varint(data)
sideboard.append((card_id, count, sideboard_owner))

return cards, heroes, format, sideboard


def write_deckstring(
cards: CardIncludeList,
heroes: CardList,
format: FormatType,
sideboard: Optional[SideboardList] = None,
) -> str:
if sideboard is None:
sideboard = []

def write_deckstring(cards: CardIncludeList, heroes: CardList, format: FormatType) -> str:
data = BytesIO()
data.write(b"\0")
_write_varint(data, DECKSTRING_VERSION)
Expand All @@ -151,5 +206,25 @@ def write_deckstring(cards: CardIncludeList, heroes: CardList, format: FormatTyp
_write_varint(data, cardid)
_write_varint(data, count)

if len(sideboard) > 0:
data.write(b"\1")

sideboard_x1, sideboard_x2, sideboard_xn = trisort_cards(sideboard)

for cardlist in sideboard_x1, sideboard_x2:
_write_varint(data, len(cardlist))
for cardid, _, sideboard_owner in cardlist:
_write_varint(data, cardid)
_write_varint(data, sideboard_owner)

_write_varint(data, len(cards_xn))
for cardid, count, sideboard_owner in sideboard_xn:
_write_varint(data, cardid)
_write_varint(data, count)
_write_varint(data, sideboard_owner)

else:
data.write(b"\0")

encoded = base64.b64encode(data.getvalue())
return encoded.decode("utf-8")
105 changes: 94 additions & 11 deletions tests/test_deckstrings.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@
from hearthstone.enums import FormatType


TEST_DECKSTRING_PRE_SIDEBOARD = (
"AAEBAR8G+LEChwTmwgKhwgLZwgK7BQzquwKJwwKOwwKTwwK5tAK1A/4MqALsuwLrB86uAu0JAA=="
)
TEST_DECKSTRING = (
"AAECAR8G+LEChwTmwgKhwgLZwgK7BQzquwKJwwKOwwKTwwK5tAK1A/4MqALsuwLrB86uAu0JAA=="
"AAEBAR8GhwS7BfixAqHCAtnCAubCAgyoArUD6wftCf4Mzq4CubQC6rsC7LsCicMCjsMCk8MCAAA="
)
TEST_DECKSTRING_CARDLIST = (
TEST_DECKSTRING_CARDLIST = [
(40426, 2), # Alleycat
(41353, 2), # Jeweled Macaw
(39160, 1), # Cat Trick
Expand All @@ -24,33 +27,69 @@
(41305, 1), # Nesting Roc
(699, 1), # Tundra Rhino
(1261, 2), # Savannah Highmane
]

TEST_SIDEBOARD_DECKSTRING = (
"AAEBAZCaBgjlsASotgSX7wTvkQXipAX9xAXPxgXGxwUQvp8EobYElrcE+dsEuNwEutwE9v"
"AEhoMFopkF4KQFlMQFu8QFu8cFuJ4Gz54G0Z4GAAED8J8E/cQFuNkE/cQF/+EE/cQFAAA="
)
TEST_SIDEBOARD_DECKSTRING_CARDLIST = [
(102223, 2), # Armor Vendor
(69566, 2), # Psychic Conjurer
(102200, 2), # Shard of the Naaru
(71781, 1), # Sir Finley, Sea Guide
(77305, 2), # The Light! It Burns!
(86626, 1), # Astalor Bloodsworn
(91078, 1), # Audio Amplifier
(102225, 2), # Dirty Rat
(90644, 2), # Mind Eater
(91067, 2), # Power Chord: Synchronize
(82310, 2), # Cathedral of Atonement
(77368, 2), # Identity Theft
(90959, 1), # Love Everlasting
(85154, 2), # Nerubian Vizier
(79767, 1), # Prince Renathal
(86624, 2), # Cannibalize
(79990, 2), # Demolition Renovator
(90749, 1), # E.T.C., Band Manager
(72598, 2), # School Teacher
(77370, 2), # Clean the Scene
(90683, 2), # Harmonic Pop
(84207, 1), # Sister Svalna
(72488, 1), # Blackwater Behemoth
(72481, 2), # Whirlpool
]
TEST_SIDEBOARD_DECKSTRING_SIDEBOARD = [
(69616, 1, 90749),
(76984, 1, 90749),
(78079, 1, 90749),
]


DECKSTRING_TEST_DATA = [
{
"cards": [(1, 2), (2, 2), (3, 2), (4, 2)],
"heroes": [7], # Garrosh Hellscream
"format": FormatType.FT_STANDARD,
"deckstring": "AAECAQcABAECAwQA",
"deckstring": "AAECAQcABAECAwQAAA==",
},
{
"cards": [(8, 1), (179, 1), (2009, 1)],
"heroes": [7],
"format": FormatType.FT_STANDARD,
"deckstring": "AAECAQcDCLMB2Q8AAA==",
"deckstring": "AAECAQcDCLMB2Q8AAAA=",
},
{
"cards": [(1, 3), (2, 3), (3, 3), (4, 3)],
"heroes": [7], # Garrosh Hellscream
"format": FormatType.FT_WILD,
"deckstring": "AAEBAQcAAAQBAwIDAwMEAw==",
"deckstring": "AAEBAQcAAAQBAwIDAwMEAwA=",
},
{
"cards": [(1, 1), (2, 1), (3, 1), (4, 1)],
"heroes": [40195], # Maiev Shadowsong
"format": FormatType.FT_WILD,
"deckstring": "AAEBAYO6AgQBAgMEAAA=",
"deckstring": "AAEBAYO6AgQBAgMEAAAA",
},
{
# https://hsreplay.net/decks/mae2HTeLYbTIrSYZiALN9d/
Expand All @@ -76,7 +115,7 @@
"format": FormatType.FT_STANDARD,
"heroes": [41887], # Tyrande Whisperwind
"deckstring": (
"AAECAZ/HAgS1uwLcwQK+yALIxwIN68IC+ALyDOUE0QrYwQLRwQLXCvC7AtMKysMCmcICwsMCAA=="
"AAECAZ/HAgS1uwLcwQK+yALIxwIN68IC+ALyDOUE0QrYwQLRwQLXCvC7AtMKysMCmcICwsMCAAA="
)
},
{
Expand All @@ -103,7 +142,7 @@
"format": FormatType.FT_STANDARD,
"heroes": [31], # Rexxar
"deckstring": (
"AAECAR8GxwPJBLsFmQfZB/gIDI0B2AGoArUDhwSSBe0G6wfbCe0JgQr+DAA="
"AAECAR8GxwPJBLsFmQfZB/gIDI0B2AGoArUDhwSSBe0G6wfbCe0JgQr+DAAA"
),
}
]
Expand All @@ -116,26 +155,69 @@ def _decksorted(cards):
def test_empty_deckstring():
deck = deckstrings.Deck()
deck.heroes = [0]
assert deck.as_deckstring == "AAEAAQAAAAA="
assert deck.as_deckstring == "AAEAAQAAAAAA"


def test_decode_pre_sideboard_deckstring():
deck = deckstrings.Deck.from_deckstring(TEST_DECKSTRING_PRE_SIDEBOARD)
assert deck.get_dbf_id_list() == _decksorted(TEST_DECKSTRING_CARDLIST)
assert deck.get_sideboard_dbf_id_list() == []
assert deck.format == FormatType.FT_WILD
assert deck.heroes == [31] # Rexxar


def test_decode_deckstring():
deck = deckstrings.Deck.from_deckstring(TEST_DECKSTRING)
assert deck.get_dbf_id_list() == _decksorted(TEST_DECKSTRING_CARDLIST)
assert deck.format == FormatType.FT_STANDARD
assert deck.get_sideboard_dbf_id_list() == []
assert deck.format == FormatType.FT_WILD
assert deck.heroes == [31] # Rexxar


def test_encode_deckstring():
deck = deckstrings.Deck()
deck.cards = _decksorted(TEST_DECKSTRING_CARDLIST)
deck.sideboard = []
deck.format = FormatType.FT_WILD
deck.heroes = [31]
assert deck.as_deckstring == TEST_DECKSTRING


def test_reencode_deckstring():
deck = deckstrings.Deck.from_deckstring(TEST_DECKSTRING)
assert deck.as_deckstring == TEST_DECKSTRING


def test_deckstrings():
def test_decode_sideboard_deckstring():
deck = deckstrings.Deck.from_deckstring(TEST_SIDEBOARD_DECKSTRING)
assert deck.get_dbf_id_list() == _decksorted(TEST_SIDEBOARD_DECKSTRING_CARDLIST)
assert deck.sideboard == _decksorted(TEST_SIDEBOARD_DECKSTRING_SIDEBOARD)
assert deck.format == FormatType.FT_WILD
assert deck.heroes == [101648] # Hedanis


def test_encode_sideboard_deckstring():
deck = deckstrings.Deck()
deck.cards = _decksorted(TEST_SIDEBOARD_DECKSTRING_CARDLIST)
deck.sideboard = _decksorted(TEST_SIDEBOARD_DECKSTRING_SIDEBOARD)
deck.format = FormatType.FT_WILD
deck.heroes = [101648]
assert deck.as_deckstring == TEST_SIDEBOARD_DECKSTRING


def test_reencode_sideboard_deckstring():
deck = deckstrings.Deck.from_deckstring(TEST_SIDEBOARD_DECKSTRING)
assert deck.as_deckstring == TEST_SIDEBOARD_DECKSTRING


def test_deckstrings_regression():
for deckdata in DECKSTRING_TEST_DATA:
sideboard = deckdata.get("sideboard", [])

# Encode tests
deck = deckstrings.Deck()
deck.cards = deckdata["cards"]
deck.sideboard = sideboard
deck.heroes = deckdata["heroes"]
deck.format = deckdata["format"]

Expand All @@ -144,5 +226,6 @@ def test_deckstrings():
# Decode tests
deck = deckstrings.Deck.from_deckstring(deckdata["deckstring"])
assert _decksorted(deck.cards) == _decksorted(deckdata["cards"])
assert _decksorted(deck.sideboard) == _decksorted(sideboard)
assert deck.heroes == deckdata["heroes"]
assert deck.format == deckdata["format"]