deckstrings 0.0.3 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 6148776b7c2053194b8395f146073d1cbb2e4287
4
- data.tar.gz: adbbba2b83eb857c7ae1fe1cbf037b2289c53794
3
+ metadata.gz: a5a397bdd301581b9c7e06931ad8f123bab9c6f1
4
+ data.tar.gz: 12821a30968d65f31d565dd3b0542263e453d263
5
5
  SHA512:
6
- metadata.gz: 4e2f2054b7934284201237c9d1bb33d2015f6c60033a9cee779d6049ce87078ab7dbcc158c1b7a3b5b03dd7109ee4de18f9e285267cc2496a3b81b5b690723ea
7
- data.tar.gz: 5bf099a6dbd36e59c28d5bb22a452b7c8122b4c45b0db91f8e48a4f79c2510234981dae0832594264b1b234881144d3c8b7ca84e68eaedb7cc99a7a8458c795f
6
+ metadata.gz: 76acb89402c69393cc41ca7a5a5a65e2dd02c85d1bfa7962e9c78ef7edf3b894407640cd106686e627ec1a0b047e1f2432af00a7f5067b5a55e9a09f97d2c4f1
7
+ data.tar.gz: ecf7998ac4975afd1382631a11b81c722e2d33816d63217154aaeffa29f8f2c72be85e7a9132bb1874a0c4fd03ed35aff765e9c32265eb109137b0566e470d24
data/README.md CHANGED
@@ -20,7 +20,7 @@ For additional entity metadata (e.g. hero class, card cost, card name), the DBF
20
20
 
21
21
  ## Decoding
22
22
 
23
- `Deckstrings::decode` provides the simplest decoding with the least validation.
23
+ [`Deckstrings::decode`](http://www.rubydoc.info/gems/deckstrings/Deckstrings#decode-class_method) decodes a deckstring with basic validation.
24
24
 
25
25
  ```ruby
26
26
  deckstring = 'AAECAZICCPIF+Az5DK6rAuC7ApS9AsnHApnTAgtAX/4BxAbkCLS7Asu8As+8At2+AqDNAofOAgA='
@@ -31,11 +31,11 @@ puts Deckstrings::decode(deckstring)
31
31
  {:format=>2, :heroes=>[274], :cards=>{754=>1, 1656=>1, 1657=>1, 38318=>1, 40416=>1, 40596=>1, 41929=>1, 43417=>1, 64=>2, 95=>2, 254=>2, 836=>2, 1124=>2, 40372=>2, 40523=>2, 40527=>2, 40797=>2, 42656=>2, 42759=>2}}
32
32
  ```
33
33
 
34
- `Deckstrings::Deck.parse` provides extended validation and additional deck information including card name and cost.
34
+ [`Deckstrings::Deck.decode`](http://www.rubydoc.info/gems/deckstrings/Deckstrings/Deck#decode-class_method) decodes a deckstring with extended validation and additional deck information including card names and costs.
35
35
 
36
36
  ```ruby
37
37
  deckstring = 'AAECAZICCPIF+Az5DK6rAuC7ApS9AsnHApnTAgtAX/4BxAbkCLS7Asu8As+8At2+AqDNAofOAgA='
38
- puts Deckstrings::Deck.parse(deckstring)
38
+ puts Deckstrings::Deck.decode(deckstring)
39
39
  ```
40
40
 
41
41
  ```text
@@ -66,9 +66,73 @@ Hero: Malfurion Stormrage
66
66
 
67
67
  ## Encoding
68
68
 
69
- `Deckstrings::encode`
69
+ [`Deckstrings::encode`](http://www.rubydoc.info/gems/deckstrings/Deckstrings#encode-class_method) encodes deck information in a deckstring with basic validation.
70
70
 
71
- `Deckstrings::Deck`
71
+ ```ruby
72
+ puts Deckstrings::encode(
73
+ format: Deckstrings::Format.standard,
74
+ heroes: [Deckstrings::Hero.malfurion],
75
+ cards: {
76
+ 254 => 2,
77
+ 40372 => 2,
78
+ 1124 => 2,
79
+ 836 => 2,
80
+ 40523 => 2,
81
+ 64 => 2,
82
+ 40527 => 2,
83
+ 38318 => 1,
84
+ 754 => 1,
85
+ 95 => 2,
86
+ 1657 => 1,
87
+ 42656 => 2,
88
+ 1656 => 1,
89
+ 40596 => 1,
90
+ 40797 => 2,
91
+ 43417 => 1,
92
+ 41929 => 1,
93
+ 42759 => 2,
94
+ 40416 => 1
95
+ }
96
+ )
97
+ ```
98
+
99
+ ```text
100
+ AAECAZICCPIF+Az5DK6rAuC7ApS9AsnHApnTAgtAX/4BxAbkCLS7Asu8As+8At2+AqDNAofOAgA=
101
+ ```
102
+
103
+ [`Deckstrings::Deck.encode`](http://www.rubydoc.info/gems/deckstrings/Deckstrings/Deck#encode-class_method) encodes deck information in a deckstring with extended validation.
104
+
105
+ ```ruby
106
+ puts Deckstrings::Deck.encode(
107
+ format: 2,
108
+ heroes: [274],
109
+ cards: {
110
+ 254 => 2,
111
+ 40372 => 2,
112
+ 1124 => 2,
113
+ 836 => 2,
114
+ 40523 => 2,
115
+ 64 => 2,
116
+ 40527 => 2,
117
+ 38318 => 1,
118
+ 754 => 1,
119
+ 95 => 2,
120
+ 1657 => 1,
121
+ 42656 => 2,
122
+ 1656 => 1,
123
+ 40596 => 1,
124
+ 40797 => 2,
125
+ 43417 => 1,
126
+ 41929 => 1,
127
+ 42759 => 2,
128
+ 40416 => 1
129
+ }
130
+ )
131
+ ```
132
+
133
+ ```text
134
+ AAECAZICCPIF+Az5DK6rAuC7ApS9AsnHApnTAgtAX/4BxAbkCLS7Asu8As+8At2+AqDNAofOAgA=
135
+ ```
72
136
 
73
137
  ## License
74
138
 
@@ -2,10 +2,8 @@ require 'base64'
2
2
  require 'json'
3
3
 
4
4
  module Deckstrings
5
+ # An error indicating a malformed deckstring.
5
6
  class FormatError < StandardError
6
- def initialize(message)
7
- super(message)
8
- end
9
7
  end
10
8
 
11
9
  # Enumeration of valid format types: wild and standard.
@@ -21,6 +19,7 @@ module Deckstrings
21
19
  define :standard, 2, 'Standard'
22
20
  end
23
21
 
22
+ # Enumeration of all nine Hearthstone classes.
24
23
  class HeroClass
25
24
  include Enum
26
25
 
@@ -92,6 +91,7 @@ module Deckstrings
92
91
  @id = id
93
92
  @name = name
94
93
  @hero_class = HeroClass.parse(hero_class)
94
+ raise ArgumentError, "Invalid hero class: #{hero_class}." if @hero_class.nil?
95
95
  end
96
96
 
97
97
  # @return [Hero] Jaina Proudmoore.
@@ -231,8 +231,8 @@ module Deckstrings
231
231
  Hero.new(id, hero['name'], hero['class'])
232
232
  end
233
233
 
234
- # @see https://hearthstonejson.com/ HearthstoneJSON for hero metadata.
235
234
  # @return [Integer] Hearthstone DBF ID of the hero.
235
+ # @see https://hearthstonejson.com/ HearthstoneJSON
236
236
  attr_reader :id
237
237
 
238
238
  # @return [String] Name of the hero.
@@ -243,7 +243,7 @@ module Deckstrings
243
243
  end
244
244
 
245
245
  # A Hearthstone card with basic metadata.
246
- # @see Deck.parse
246
+ # @see Deck.decode
247
247
  class Card
248
248
  def initialize(id, name, cost)
249
249
  @id = id
@@ -251,8 +251,8 @@ module Deckstrings
251
251
  @cost = cost
252
252
  end
253
253
 
254
- # @see https://hearthstonejson.com/ HearthstoneJSON for card metadata.
255
254
  # @return [Integer] Hearthstone DBF ID of the card.
255
+ # @see https://hearthstonejson.com/ HearthstoneJSON
256
256
  attr_reader :id
257
257
 
258
258
  # @return [String] Name of the card.
@@ -262,50 +262,103 @@ module Deckstrings
262
262
  attr_reader :cost
263
263
  end
264
264
 
265
- # A Hearthstone deck convertible to and from a deckstring.
266
- # @see Deck.parse
267
- # @see Deck#deckstring
265
+ # A Hearthstone deck with metadata that is convertible to and from a deckstring.
266
+ # @see Deck.decode
267
+ # @see Deck.encode
268
268
  class Deck
269
+ # Create a new deck from component parts.
270
+ # @param format [Integer, Deckstrings::Format] Format for this deck: wild or standard.
271
+ # @param heroes [Array<Integer, Deckstrings::Hero>] Heroes for this deck. Multiple heroes are supported, but typically
272
+ # this array will contain one element.
273
+ # @param cards [Hash{Integer, Deckstrings::Card => Integer}] Cards in the deck. A Hash from card ID to its instance count in the deck.
274
+ # @raise [ArgumentError] If format, heroes, or any cards are unknown.
269
275
  def initialize(format:, heroes:, cards:)
270
- # TODO: Translate RangeError -> FormatError.
271
-
272
276
  @format = Format.parse(format) if !format.is_a?(Format)
273
- if !@format
274
- raise FormatError.new("Unknown format: #{format}.")
275
- end
277
+ raise ArgumentError, "Unknown format: #{format}." if !@format
276
278
 
277
279
  @heroes = heroes.map do |id|
278
280
  hero = Database.instance.heroes[id]
279
- raise FormatError.new("Unknown hero: #{id}.") if hero.nil?
281
+ raise ArgumentError, "Unknown hero: #{id}." if hero.nil?
280
282
  Hero.new(id, hero['name'], hero['class'])
281
283
  end
282
284
 
283
285
  @cards = cards.map do |id, count|
284
286
  card = Database.instance.cards[id]
285
- raise FormatError.new("Unknown card: #{id}.") if card.nil?
287
+ raise ArgumentError, "Unknown card: #{id}." if card.nil?
286
288
  [Card.new(id, card['name'], card['cost']), count]
287
289
  end.sort_by { |card, _| card.cost }.to_h
288
290
  end
289
291
 
290
- # @see .encode
291
- # @see .decode
292
+ # Raw deck details.
293
+ # @return [{ format: Integer, heroes: Array<Integer>, cards: Hash{Integer => Integer} }] See {Deckstrings.decode} for details.
294
+ # @see Deckstrings.decode
292
295
  def raw
293
296
  heroes = @heroes.map(&:id)
294
297
  cards = @cards.map { |card, count| [card.id, count] }.to_h
295
298
  { format: @format.value, heroes: heroes, cards: cards }
296
299
  end
297
300
 
301
+ # Encoded deckstring for this deck.
298
302
  # @return [String] Base64-encoded compact byte string representing the deck.
303
+ # @see Deckstrings.encode
299
304
  # @see .encode
300
305
  def deckstring
301
306
  return Deckstrings::encode(self.raw)
302
307
  end
303
308
 
304
- # @see .decode
305
- # @see #deckstring
306
- def self.parse(deckstring)
309
+ # Decodes a Hearthstone deckstring into a {Deck} with basic hero and card metadata.
310
+ #
311
+ # This method validates the well-formedness of the deckstring, the embedded version, the format, card counts, and
312
+ # each hero/card ID.
313
+ #
314
+ # All IDs refer to unique Hearthstone DBF IDs which can be used in conjunction with [HearthstoneJSON metadata](https://hearthstonejson.com/).
315
+ # @example
316
+ # deck = Deckstrings::Deck.decode('AAEBAf0GAA/yAaIC3ALgBPcE+wWKBs4H2QexCMII2Q31DfoN9g4A')
317
+ # @example
318
+ # deck = Deckstrings::Deck.decode('AAECAZICCPIF+Az5DK6rAuC7ApS9AsnHApnTAgtAX/4BxAbkCLS7Asu8As+8At2+AqDNAofOAgA=')
319
+ # @param deckstring [String] Base64-encoded Hearthstone deckstring.
320
+ # @raise [FormatError] If the deckstring is malformed or contains invalid deck data.
321
+ # @return [Deck] Decoded Hearthstone deck.
322
+ # @see Deckstrings.decode
323
+ # @see Deck.encode
324
+ # @see https://hearthstonejson.com/ HearthstoneJSON
325
+ def self.decode(deckstring)
307
326
  parts = Deckstrings::decode(deckstring)
308
- Deck.new(parts)
327
+ begin
328
+ Deck.new(parts)
329
+ rescue ArgumentError => e
330
+ raise FormatError, e.to_s
331
+ end
332
+ end
333
+
334
+ # Encodes a Hearthstone deck as a compact deckstring.
335
+ #
336
+ # This method validates card counts, format, and each hero/card ID.
337
+ #
338
+ # All IDs refer to unique Hearthstone DBF IDs which can be seen in [HearthstoneJSON metadata](https://hearthstonejson.com/).
339
+ # @example
340
+ # deckstring = Deckstrings::Deck.encode(format: 2, heroes: [637], cards: { 1004 => 2, 315 => 2 })
341
+ # @example
342
+ # deckstring = Deckstrings::Deck.encode(
343
+ # format: Deckstrings::Format.standard,
344
+ # heroes: [Deckstrings::Hero.mage],
345
+ # cards: { 1004 => 2, 315 => 2 }
346
+ # )
347
+ # @param format [Integer, Deckstrings::Format] Format for this deck: wild or standard.
348
+ # @param heroes [Array<Integer, Deckstrings::Hero>] Heroes for this deck. Multiple heroes are supported, but typically
349
+ # this array will contain one element.
350
+ # @param cards [Hash{Integer, Deckstrings::Card => Integer}] Cards in the deck. A Hash from card ID to its instance count in the deck.
351
+ # @raise [FormatError] If any card counts are less than 1, or if any IDs do not refer to valid Hearthstone entities.
352
+ # @return [String] Base64-encoded compact byte string representing the deck.
353
+ # @see .encode
354
+ # @see Deck.decode
355
+ # @see https://hearthstonejson.com/ HearthstoneJSON
356
+ def self.encode(format:, heroes:, cards:)
357
+ begin
358
+ Deck.new(format: format, heroes: heroes, cards: cards).deckstring
359
+ rescue ArgumentError => e
360
+ raise FormatError, e.to_s
361
+ end
309
362
  end
310
363
 
311
364
  # @return [Boolean] `true` if the deck is Wild format, `false` otherwise.
@@ -320,6 +373,9 @@ module Deckstrings
320
373
  format.standard?
321
374
  end
322
375
 
376
+ # Pretty-printed deck listing.
377
+ # @example
378
+ # puts Deckstrings::Deck.decode('AAECAZICCPIF+Az5DK6rAuC7ApS9AsnHApnTAgtAX/4BxAbkCLS7Asu8As+8At2+AqDNAofOAgA=')
323
379
  # @example
324
380
  # Format: Standard
325
381
  # Class: Druid
@@ -346,41 +402,57 @@ module Deckstrings
346
402
  # 1× Kun the Forgotten King
347
403
  # @return [String] A pretty-printed listing of deck details.
348
404
  def to_s
405
+ listing = "Format: #{format}"
406
+
349
407
  hero = @heroes.first
350
- "Format: #{format}\nClass: #{hero.hero_class}\nHero: #{hero.name}\n\n" + cards.map do |card, count|
351
- "#{count}× #{card.name}"
352
- end.join("\n")
408
+ if hero
409
+ listing += "\nClass: #{hero.hero_class}\nHero: #{hero.name}"
410
+ end
411
+
412
+ if !cards.empty?
413
+ listing += "\n\n" + cards.map { |card, count| "#{count}× #{card.name}" }.join("\n")
414
+ end
415
+
416
+ listing
353
417
  end
354
418
 
355
419
  # @return [Format] Format for this deck.
356
420
  attr_reader :format
357
421
 
358
- # @return [Array<Hero>] Heroes associated with this deck. Typically, this array will contain one element.
422
+ # The heroes associated with this deck.
423
+ # @return [Array<Hero>] Typically, this array will contain one element.
359
424
  attr_reader :heroes
360
425
 
361
- # @return [Hash{Card => Integer}] The cards contained in the deck. A map between {Card} and the instance count in the deck.
426
+ # The cards contained in this deck.
427
+ # @return [Hash{Card => Integer}] A Hash from {Card} to its instance count in the deck.
362
428
  attr_reader :cards
363
429
  end
364
430
 
365
431
  using VarIntExtensions
366
432
 
367
433
  # Encodes a Hearthstone deck as a compact deckstring.
434
+ #
435
+ # This method validates card counts, but does not validate the format or individual hero/card IDs. For stricter validation,
436
+ # see {Deck.encode}.
437
+ #
438
+ # All IDs refer to unique Hearthstone DBF IDs which can be seen in [HearthstoneJSON metadata](https://hearthstonejson.com/).
368
439
  # @example
369
- # Deckstrings.encode(format: 2, heroes: [7], cards: { 44 => 1, 45 => 2 })
440
+ # deckstring = Deckstrings::encode(format: 2, heroes: [637], cards: { 1004 => 2, 315 => 2 })
370
441
  # @example
371
- # Deckstrings.encode(
442
+ # deckstring = Deckstrings::encode(
372
443
  # format: Deckstrings::Format.standard,
373
- # heroes: [Deckstrings::Hero.warrior],
374
- # cards: { 1 => 2, 2 => 2 }
444
+ # heroes: [Deckstrings::Hero.mage],
445
+ # cards: { 1004 => 2, 315 => 2 }
375
446
  # )
376
447
  # @param format [Integer, Deckstrings::Format] Format for this deck: wild or standard.
377
448
  # @param heroes [Array<Integer, Deckstrings::Hero>] Heroes for this deck. Multiple heroes are supported, but typically
378
449
  # this array will contain one element.
379
- # @param cards [Hash{Integer, Deckstrings::Card => Integer}] Cards in the deck.
380
- # @raise [ArgumentError] If any card counts are less than 1.
450
+ # @param cards [Hash{Integer, Deckstrings::Card => Integer}] Cards in the deck. A Hash from card ID to its instance count in the deck.
451
+ # @raise [FormatError] If any card counts are less than 1.
381
452
  # @return [String] Base64-encoded compact byte string representing the deck.
382
- # @see Deck#deckstring
453
+ # @see Deck.encode
383
454
  # @see .decode
455
+ # @see https://hearthstonejson.com/ HearthstoneJSON
384
456
  def self.encode(format:, heroes:, cards:)
385
457
  stream = StringIO.new('')
386
458
 
@@ -403,7 +475,7 @@ module Deckstrings
403
475
 
404
476
  invalid = by_count.keys.select { |count| count < 1 }
405
477
  unless invalid.empty?
406
- raise ArgumentError.new("Invalid card count: #{invalid.join(', ')}.")
478
+ raise FormatError, "Invalid card count: #{invalid.join(', ')}."
407
479
  end
408
480
 
409
481
  1.upto(3) do |count|
@@ -415,10 +487,15 @@ module Deckstrings
415
487
  end
416
488
  end
417
489
 
418
- Base64::encode64(stream.string).strip
490
+ Base64::strict_encode64(stream.string).strip
419
491
  end
420
492
 
421
- # Decodes a Hearthstone deckstring into format, hero, and card details.
493
+ # Decodes a Hearthstone deckstring into format, hero, and card counts.
494
+ #
495
+ # This method validates the well-formedness of the deckstring and the embedded version, but does not validate
496
+ # the format, individual hero/card IDs, or card counts. For stricter validation and additional deck info, see {Deck.decode}.
497
+ #
498
+ # All IDs refer to unique Hearthstone DBF IDs which can be used in conjunction with [HearthstoneJSON metadata](https://hearthstonejson.com/).
422
499
  # @example
423
500
  # deck = Deckstrings::decode('AAEBAf0GAA/yAaIC3ALgBPcE+wWKBs4H2QexCMII2Q31DfoN9g4A')
424
501
  # @example
@@ -426,24 +503,31 @@ module Deckstrings
426
503
  # @param deckstring [String] Base64-encoded Hearthstone deckstring.
427
504
  # @raise [FormatError] If the deckstring is malformed or contains invalid deck data.
428
505
  # @return [{ format: Integer, heroes: Array<Integer>, cards: Hash{Integer => Integer} }] Parsed Hearthstone deck details.
429
- # @see Deck#parse
506
+ # `heroes` is an array of hero IDs, but this will usually be just one element. `cards` is a
507
+ # Hash from card ID to its instance count in the deck.
508
+ # @see Deck.decode
430
509
  # @see .encode
510
+ # @see https://hearthstonejson.com/ HearthstoneJSON
431
511
  def self.decode(deckstring)
432
- begin
433
- if deckstring.nil? || deckstring.empty?
434
- raise ArgumentError.new('Invalid deckstring.')
435
- end
512
+ if deckstring.nil? || deckstring.empty?
513
+ raise FormatError, 'Invalid deckstring.'
514
+ end
436
515
 
437
- stream = StringIO.new(Base64::decode64(deckstring))
516
+ stream = begin
517
+ StringIO.new(Base64::strict_decode64(deckstring))
518
+ rescue ArgumentError
519
+ raise FormatError, 'Invalid base64-encoded string.'
520
+ end
438
521
 
522
+ begin
439
523
  reserved = stream.read_varint
440
524
  if reserved != 0
441
- raise FormatError.new("Unexpected reserved byte: #{reserved}.")
525
+ raise FormatError, "Unexpected reserved byte: #{reserved}."
442
526
  end
443
527
 
444
528
  version = stream.read_varint
445
529
  if version != 1
446
- raise FormatError.new("Unexpected version: #{version}.")
530
+ raise FormatError, "Unexpected version: #{version}."
447
531
  end
448
532
 
449
533
  format = stream.read_varint
@@ -464,14 +548,14 @@ module Deckstrings
464
548
  cards[card] = i < 3 ? i : stream.read_varint
465
549
  end
466
550
  end
467
-
468
- return {
469
- format: format,
470
- heroes: heroes,
471
- cards: cards
472
- }
473
551
  rescue EOFError
474
552
  raise FormatError, 'Unexpected end of data.'
475
553
  end
554
+
555
+ return {
556
+ format: format,
557
+ heroes: heroes,
558
+ cards: cards
559
+ }
476
560
  end
477
561
  end
@@ -18,9 +18,7 @@ module Deckstrings
18
18
  end
19
19
 
20
20
  def parse(value)
21
- enum = @@values[value]
22
- raise RangeError.new("Unknown value: #{value}.") if !enum
23
- enum
21
+ @@values[value]
24
22
  end
25
23
  end
26
24
 
@@ -7,7 +7,7 @@ module Deckstrings
7
7
  shift = 0
8
8
  loop do
9
9
  octet = self.getbyte
10
- raise EOFError.new('Unexpected end of data.') if octet.nil?
10
+ raise EOFError, 'Unexpected end of data.' if octet.nil?
11
11
 
12
12
  num |= (octet & 0x7f) << shift
13
13
  return num if octet & 0x80 == 0
metadata CHANGED
@@ -1,15 +1,29 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: deckstrings
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.3
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Chris Schmich
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2017-09-15 00:00:00.000000000 Z
12
- dependencies: []
11
+ date: 2017-09-16 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: yard
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '0.9'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '0.9'
13
27
  description: Ruby library for encoding and decoding Hearthstone deckstrings.
14
28
  email: schmch@gmail.com
15
29
  executables: []