deckstrings 0.0.3 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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: []