module Deckstrings

Public Class Methods

decode(deckstring) click to toggle source

Decodes a Hearthstone deckstring into format, hero, and card counts.

This method validates the well-formedness of the deckstring and the embedded version, but does not validate the format, individual hero/card IDs, or card counts. For stricter validation and additional deck info, see {Deck.decode}.

All IDs refer to unique Hearthstone DBF IDs which can be used in conjunction with [HearthstoneJSON metadata](hearthstonejson.com/). @example

deck = Deckstrings::decode('AAEBAf0GAA/yAaIC3ALgBPcE+wWKBs4H2QexCMII2Q31DfoN9g4A')

@example

deck = Deckstrings::decode('AAECAZICCPIF+Az5DK6rAuC7ApS9AsnHApnTAgtAX/4BxAbkCLS7Asu8As+8At2+AqDNAofOAgA=')

@param deckstring [String] Base64-encoded Hearthstone deckstring. @raise [FormatError] If the deckstring is malformed or contains invalid deck data. @return [{ format: Integer, heroes: Array<Integer>, cards: Hash{Integer => Integer} }] Parsed Hearthstone deck details.

`heroes` is an array of hero IDs, but this will usually be just one element. `cards` is a
Hash from card ID to its instance count in the deck.

@see Deck.decode @see .encode @see hearthstonejson.com/ HearthstoneJSON

# File lib/deckstrings/deckstrings.rb, line 526
def self.decode(deckstring)
  if deckstring.nil? || deckstring.empty?
    raise FormatError, 'Invalid deckstring.'
  end

  stream = begin
    StringIO.new(Base64::strict_decode64(deckstring))
  rescue ArgumentError
    raise FormatError, 'Invalid base64-encoded string.'
  end

  begin
    reserved = stream.read_varint
    if reserved != 0
      raise FormatError, "Unexpected reserved byte: #{reserved}."
    end

    version = stream.read_varint
    if version != 1
      raise FormatError, "Unexpected version: #{version}."
    end

    format = stream.read_varint

    # Heroes
    heroes = []
    length = stream.read_varint
    length.times do
      heroes << stream.read_varint
    end

    # Cards
    cards = {}
    1.upto(3) do |i|
      length = stream.read_varint
      length.times do
        card = stream.read_varint
        cards[card] = i < 3 ? i : stream.read_varint
      end
    end
  rescue EOFError
    raise FormatError, 'Unexpected end of data.'
  end

  return {
    format: format,
    heroes: heroes,
    cards: cards
  }
end
encode(format:, heroes:, cards:) click to toggle source

Encodes a Hearthstone deck as a compact deckstring.

This method validates card counts, but does not validate the format or individual hero/card IDs. For stricter validation, see {Deck.encode}.

All IDs refer to unique Hearthstone DBF IDs which can be seen in [HearthstoneJSON metadata](hearthstonejson.com/). @example

deckstring = Deckstrings::encode(format: 2, heroes: [637], cards: { 1004 => 2, 315 => 2 })

@example

deckstring = Deckstrings::encode(
  format: Deckstrings::Format.standard,
  heroes: [Deckstrings::Hero.mage],
  cards: { 1004 => 2, 315 => 2 }
)

@param format [Integer, Deckstrings::Format] Format for this deck: wild or standard. @param heroes [Array<Integer, Deckstrings::Hero>] Heroes for this deck. Multiple heroes are supported, but typically

this array will contain one element.

@param cards [Hash{Integer, Deckstrings::Card => Integer}] Cards in the deck. A Hash from card ID to its instance count in the deck. @raise [FormatError] If any card counts are less than 1. @return [String] Base64-encoded compact byte string representing the deck. @see Deck.encode @see .decode @see hearthstonejson.com/ HearthstoneJSON

# File lib/deckstrings/deckstrings.rb, line 471
def self.encode(format:, heroes:, cards:)
  stream = StringIO.new('')

  format = format.is_a?(Deckstrings::Format) ? format.value : format
  heroes = heroes.map { |hero| hero.is_a?(Deckstrings::Hero) ? hero.id : hero }

  # Reserved slot, version, and format.
  stream.write_varint(0)
  stream.write_varint(1)
  stream.write_varint(format)

  # Heroes.
  stream.write_varint(heroes.length)
  heroes.sort.each do |hero|
    stream.write_varint(hero)
  end

  # Cards.
  by_count = cards.group_by { |id, n| n > 2 ? 3 : n }

  invalid = by_count.keys.select { |count| count < 1 }
  unless invalid.empty?
    raise FormatError, "Invalid card count: #{invalid.join(', ')}."
  end

  1.upto(3) do |count|
    group = by_count[count] || []
    stream.write_varint(group.length)
    group.sort_by { |id, n| id }.each do |id, n|
      stream.write_varint(id)
      stream.write_varint(n) if n > 2
    end
  end

  Base64::strict_encode64(stream.string).strip
end