deckstrings 0.0.1 → 0.0.2

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: 165dd18189a80411f1cf056c839300f4740ad9e2
4
- data.tar.gz: b075f96b17e110de658cd794de55ba3a4fc81f13
3
+ metadata.gz: 80a4c98e3e84f0390bac22795833f6b07e87668b
4
+ data.tar.gz: aac1b9bb8f72e78dc1c37ce7d800b21b9bfea124
5
5
  SHA512:
6
- metadata.gz: 1e3853e3128d65fcba0b474bb7cc6dc35b0421605cb207a44d7d129e1b73b4272c2d7de71b2089d6f8ff2c77fabe4acab1bbdc3eb1d25376a9beeff0fc04b30e
7
- data.tar.gz: a9a27f25681cd2a616b11c4b245e5f5fbc19130f96ec52f2755c6f5bb5c84827adae7594509c00d7971050a399dfd17147cc9e03ceebcb326e3ad260d3bdcad3
6
+ metadata.gz: 995a352c3728e5fa27aef25e0b1b41d0f39de4535e42e7b6f8fa918b48618dccbe7176503242910233c7ef3ecd8f866b28066b9756612250fc10c18ec9060843
7
+ data.tar.gz: 2364c4b40827f0971386195cf1d92b151fdbf448c0ff2dd95c4d58e3f3177d542984cf7236b13b9bbfc8b27ffdd04290d4c0eadfd75bf57f9ff16f39b34e83ed
data/README.md ADDED
@@ -0,0 +1,76 @@
1
+ # Hearthstone Deckstrings [![Gem Version](https://badge.fury.io/rb/deckstrings.svg)](http://rubygems.org/gems/deckstrings) [![Build Status](https://travis-ci.org/schmich/hearthstone-deckstrings.svg?branch=master)](https://travis-ci.org/schmich/hearthstone-deckstrings)
2
+
3
+ Ruby library for encoding and decoding [Hearthstone deckstrings](https://hearthsim.info/docs/deckstrings/). See [documentation](http://www.rubydoc.info/gems/deckstrings) for help.
4
+
5
+ ## Usage
6
+
7
+ ```bash
8
+ gem install deckstrings
9
+ ```
10
+
11
+ ```ruby
12
+ require 'deckstrings'
13
+ ```
14
+
15
+ Hearthstone deckstrings encode a Hearthstone deck in a compact format.
16
+
17
+ The IDs used in deckstrings and in this library refer to Hearthstone DBF IDs which uniquely define Hearthstone entities like cards and heroes.
18
+
19
+ For additional entity metadata (e.g. hero class, card cost, card name), the DBF IDs can be used in conjunction with the [HearthstoneJSON](https://hearthstonejson.com/) database.
20
+
21
+ ## Decoding
22
+
23
+ `Deckstrings::decode` provides the simplest decoding with the least validation.
24
+
25
+ ```ruby
26
+ deckstring = 'AAECAZICCPIF+Az5DK6rAuC7ApS9AsnHApnTAgtAX/4BxAbkCLS7Asu8As+8At2+AqDNAofOAgA='
27
+ puts Deckstrings::decode(deckstring)
28
+ ```
29
+
30
+ ```text
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
+ ```
33
+
34
+ `Deckstrings::Deck.parse` provides extended validation and additional deck information including card name and cost.
35
+
36
+ ```ruby
37
+ deckstring = 'AAECAZICCPIF+Az5DK6rAuC7ApS9AsnHApnTAgtAX/4BxAbkCLS7Asu8As+8At2+AqDNAofOAgA='
38
+ puts Deckstrings::Deck.parse(deckstring)
39
+ ```
40
+
41
+ ```text
42
+ Format: Standard
43
+ Class: Druid
44
+ Hero: Malfurion Stormrage
45
+
46
+ 2× Innervate
47
+ 2× Jade Idol
48
+ 2× Wild Growth
49
+ 2× Wrath
50
+ 2× Jade Blossom
51
+ 2× Swipe
52
+ 2× Jade Spirit
53
+ 1× Fandral Staghelm
54
+ 1× Spellbreaker
55
+ 2× Nourish
56
+ 1× Big Game Hunter
57
+ 2× Spreading Plague
58
+ 1× The Black Knight
59
+ 1× Aya Blackpaw
60
+ 2× Jade Behemoth
61
+ 1× Malfurion the Pestilent
62
+ 1× Primordial Drake
63
+ 2× Ultimate Infestation
64
+ 1× Kun the Forgotten King
65
+ ```
66
+
67
+ ## Encoding
68
+
69
+ `Deckstrings::encode`
70
+
71
+ `Deckstrings::Deck`
72
+
73
+ ## License
74
+
75
+ Copyright © 2017 Chris Schmich
76
+ MIT License. See [LICENSE](LICENSE) for details.
data/lib/deckstrings.rb CHANGED
@@ -1,4 +1,3 @@
1
1
  require 'deckstrings/enum'
2
2
  require 'deckstrings/varint'
3
- require 'deckstrings/version'
4
3
  require 'deckstrings/deckstrings'
@@ -1,251 +1,392 @@
1
1
  require 'base64'
2
2
  require 'json'
3
3
 
4
- class FormatError < StandardError
5
- def initialize(message)
6
- super(message)
4
+ module Deckstrings
5
+ class FormatError < StandardError
6
+ def initialize(message)
7
+ super(message)
8
+ end
7
9
  end
8
- end
9
10
 
10
- class Format
11
- include Enum
12
- define :wild, 1, 'Wild'
13
- define :standard, 2, 'Standard'
14
- end
11
+ # Enumeration of valid format types: wild and standard.
12
+ class Format
13
+ include Enum
15
14
 
16
- class HeroClass
17
- include Enum
18
- define :mage, 'mage', 'Mage'
19
- define :rogue, 'rogue', 'Rogue'
20
- define :druid, 'druid', 'Druid'
21
- define :hunter, 'hunter', 'Hunter'
22
- define :shaman, 'shaman', 'Shaman'
23
- define :priest, 'priest', 'Priest'
24
- define :warrior, 'warrior', 'Warrior'
25
- define :paladin, 'paladin', 'Paladin'
26
- define :warlock, 'warlock', 'Warlock'
27
- end
15
+ # @!scope class
16
+ # @return [Format] Wild format.
17
+ define :wild, 1, 'Wild'
28
18
 
29
- class Database
30
- def initialize
31
- file = File.expand_path('database.json', File.dirname(__FILE__))
32
- @database = JSON.parse(File.read(file))
19
+ # @!scope class
20
+ # @return [Format] Standard format.
21
+ define :standard, 2, 'Standard'
33
22
  end
34
23
 
35
- def self.instance
36
- @@instance ||= Database.new
37
- end
24
+ class HeroClass
25
+ include Enum
38
26
 
39
- def cards
40
- @cards ||= begin
41
- @database['cards'].map { |k, v| [k.to_i, v] }.to_h
42
- end
43
- end
27
+ # @!scope class
28
+ # @return [HeroClass] Mage class.
29
+ define :mage, 'mage', 'Mage'
44
30
 
45
- def heroes
46
- @heroes ||= begin
47
- @database['heroes'].map { |k, v| [k.to_i, v] }.to_h
48
- end
49
- end
50
- end
31
+ # @!scope class
32
+ # @return [HeroClass] Rogue class.
33
+ define :rogue, 'rogue', 'Rogue'
51
34
 
52
- class Hero
53
- def initialize(id, name, hero_class)
54
- @id = id
55
- @name = name
56
- @hero_class = HeroClass.parse(hero_class)
57
- end
35
+ # @!scope class
36
+ # @return [HeroClass] Druid class.
37
+ define :druid, 'druid', 'Druid'
58
38
 
59
- def self.mage
60
- self.jaina
61
- end
39
+ # @!scope class
40
+ # @return [HeroClass] Hunter class.
41
+ define :hunter, 'hunter', 'Hunter'
62
42
 
63
- def self.jaina
64
- self.from_id(637)
65
- end
43
+ # @!scope class
44
+ # @return [HeroClass] Shaman class.
45
+ define :shaman, 'shaman', 'Shaman'
66
46
 
67
- def self.khadgar
68
- self.from_id(39117)
69
- end
47
+ # @!scope class
48
+ # @return [HeroClass] Priest class.
49
+ define :priest, 'priest', 'Priest'
70
50
 
71
- def self.rogue
72
- self.valeera
73
- end
51
+ # @!scope class
52
+ # @return [HeroClass] Warrior class.
53
+ define :warrior, 'warrior', 'Warrior'
74
54
 
75
- def self.valeera
76
- self.from_id(930)
77
- end
55
+ # @!scope class
56
+ # @return [HeroClass] Paladin class.
57
+ define :paladin, 'paladin', 'Paladin'
78
58
 
79
- def self.maiev
80
- self.from_id(40195)
59
+ # @!scope class
60
+ # @return [HeroClass] Warlock class.
61
+ define :warlock, 'warlock', 'Warlock'
81
62
  end
82
63
 
83
- def self.druid
84
- self.malfurion
85
- end
64
+ # @private
65
+ class Database
66
+ def initialize
67
+ file = File.expand_path('database.json', File.dirname(__FILE__))
68
+ @database = JSON.parse(File.read(file))
69
+ end
86
70
 
87
- def self.malfurion
88
- self.from_id(274)
89
- end
71
+ def self.instance
72
+ @@instance ||= Database.new
73
+ end
90
74
 
91
- def self.shaman
92
- self.thrall
93
- end
75
+ def cards
76
+ @cards ||= begin
77
+ @database['cards'].map { |k, v| [k.to_i, v] }.to_h
78
+ end
79
+ end
94
80
 
95
- def self.thrall
96
- self.from_id(1066)
81
+ def heroes
82
+ @heroes ||= begin
83
+ @database['heroes'].map { |k, v| [k.to_i, v] }.to_h
84
+ end
85
+ end
97
86
  end
98
87
 
99
- def self.morgl
100
- self.from_id(40183)
101
- end
88
+ # A Hearthstone hero with basic metadata.
89
+ # @see Deck#heroes
90
+ class Hero
91
+ def initialize(id, name, hero_class)
92
+ @id = id
93
+ @name = name
94
+ @hero_class = HeroClass.parse(hero_class)
95
+ end
102
96
 
103
- def self.priest
104
- self.anduin
105
- end
97
+ # @return [Hero] Jaina Proudmoore.
98
+ def self.mage
99
+ self.jaina
100
+ end
106
101
 
107
- def self.anduin
108
- self.from_id(813)
109
- end
102
+ # @return [Hero] Jaina Proudmoore.
103
+ def self.jaina
104
+ self.from_id(637)
105
+ end
110
106
 
111
- def self.tyrande
112
- self.from_id(41887)
113
- end
107
+ # @return [Hero] Khadgar.
108
+ def self.khadgar
109
+ self.from_id(39117)
110
+ end
114
111
 
115
- def self.hunter
116
- self.rexxar
117
- end
112
+ # @return [Hero] Valeera Sanguinar.
113
+ def self.rogue
114
+ self.valeera
115
+ end
118
116
 
119
- def self.rexxar
120
- self.from_id(31)
121
- end
117
+ # @return [Hero] Valeera Sanguinar.
118
+ def self.valeera
119
+ self.from_id(930)
120
+ end
122
121
 
123
- def self.alleria
124
- self.from_id(2826)
125
- end
122
+ # @return [Hero] Maiev Shadowsong.
123
+ def self.maiev
124
+ self.from_id(40195)
125
+ end
126
126
 
127
- def self.warlock
128
- self.guldan
129
- end
127
+ # @return [Hero] Malfurion Stormrage.
128
+ def self.druid
129
+ self.malfurion
130
+ end
130
131
 
131
- def self.guldan
132
- self.from_id(893)
133
- end
132
+ # @return [Hero] Malfurion Stormrage.
133
+ def self.malfurion
134
+ self.from_id(274)
135
+ end
134
136
 
135
- def self.paladin
136
- self.uther
137
- end
137
+ # @return [Hero] Thrall.
138
+ def self.shaman
139
+ self.thrall
140
+ end
138
141
 
139
- def self.uther
140
- self.from_id(671)
141
- end
142
+ # @return [Hero] Thrall.
143
+ def self.thrall
144
+ self.from_id(1066)
145
+ end
142
146
 
143
- def self.liadrin
144
- self.from_id(2827)
145
- end
147
+ # @return [Hero] Morgl the Oracle.
148
+ def self.morgl
149
+ self.from_id(40183)
150
+ end
146
151
 
147
- def self.arthas
148
- self.from_id(46116)
149
- end
152
+ # @return [Hero] Anduin Wrynn.
153
+ def self.priest
154
+ self.anduin
155
+ end
150
156
 
151
- def self.warrior
152
- self.garrosh
153
- end
157
+ # @return [Hero] Anduin Wrynn.
158
+ def self.anduin
159
+ self.from_id(813)
160
+ end
154
161
 
155
- def self.garrosh
156
- self.from_id(7)
157
- end
162
+ # @return [Hero] Tyrande Whisperwind.
163
+ def self.tyrande
164
+ self.from_id(41887)
165
+ end
158
166
 
159
- def self.magni
160
- self.from_id(2828)
161
- end
167
+ # @return [Hero] Rexxar.
168
+ def self.hunter
169
+ self.rexxar
170
+ end
162
171
 
163
- def self.from_id(id)
164
- heroes = Database.instance.heroes
165
- hero = heroes[id]
166
- Hero.new(id, hero['name'], hero['class'])
167
- end
172
+ # @return [Hero] Rexxar.
173
+ def self.rexxar
174
+ self.from_id(31)
175
+ end
168
176
 
169
- attr_reader :id, :name, :hero_class
170
- end
177
+ # @return [Hero] Alleria Windrunner.
178
+ def self.alleria
179
+ self.from_id(2826)
180
+ end
171
181
 
172
- class Card
173
- def initialize(id, name, cost)
174
- @id = id
175
- @name = name
176
- @cost = cost
177
- end
182
+ # @return [Hero] Gul'dan.
183
+ def self.warlock
184
+ self.guldan
185
+ end
178
186
 
179
- attr_accessor :id, :name, :cost
180
- end
187
+ # @return [Hero] Gul'dan.
188
+ def self.guldan
189
+ self.from_id(893)
190
+ end
191
+
192
+ # @return [Hero] Uther Lightbringer.
193
+ def self.paladin
194
+ self.uther
195
+ end
196
+
197
+ # @return [Hero] Uther Lightbringer.
198
+ def self.uther
199
+ self.from_id(671)
200
+ end
201
+
202
+ # @return [Hero] Lady Liadrin.
203
+ def self.liadrin
204
+ self.from_id(2827)
205
+ end
206
+
207
+ # @return [Hero] Prince Arthas.
208
+ def self.arthas
209
+ self.from_id(46116)
210
+ end
211
+
212
+ # @return [Hero] Garrosh Hellscream.
213
+ def self.warrior
214
+ self.garrosh
215
+ end
181
216
 
182
- class Deck
183
- def initialize(format:, heroes:, cards:)
184
- @format = Format.parse(format) if !format.is_a?(Format)
185
- if !@format
186
- raise FormatError.new("Unknown format: #{format}.")
217
+ # @return [Hero] Garrosh Hellscream.
218
+ def self.garrosh
219
+ self.from_id(7)
187
220
  end
188
221
 
189
- @heroes = heroes.map do |id|
222
+ # @return [Hero] Magni Bronzebeard.
223
+ def self.magni
224
+ self.from_id(2828)
225
+ end
226
+
227
+ # @param id [Integer] Hero's Hearthstone DBF ID.
228
+ # @return [Hero] Hero corresponding to DBF ID.
229
+ def self.from_id(id)
190
230
  hero = Database.instance.heroes[id]
191
- raise FormatError.new("Unknown hero: #{id}.") if hero.nil?
192
231
  Hero.new(id, hero['name'], hero['class'])
193
232
  end
194
233
 
195
- @cards = cards.map do |id, count|
196
- card = Database.instance.cards[id]
197
- raise FormatError.new("Unknown card: #{id}.") if card.nil?
198
- [Card.new(id, card['name'], card['cost']), count]
199
- end.sort_by { |card, _| card.cost }.to_h
200
- end
234
+ # @see https://hearthstonejson.com/ HearthstoneJSON for hero metadata.
235
+ # @return [Integer] Hearthstone DBF ID of the hero.
236
+ attr_reader :id
201
237
 
202
- def deckstring
203
- heroes = @heroes.map(&:id)
204
- cards = @cards.map { |card, count| [card.id, count] }.to_h
205
- return Deckstring.encode(format: @format.value, heroes: heroes, cards: cards)
206
- end
238
+ # @return [String] Name of the hero.
239
+ attr_reader :name
207
240
 
208
- def self.parse(deckstring)
209
- parts = Deckstring.decode(deckstring)
210
- Deck.new(parts)
241
+ # @return [HeroClass] Class of the hero.
242
+ attr_reader :hero_class
211
243
  end
212
244
 
213
- def wild?
214
- format == Format.wild
215
- end
245
+ # A Hearthstone card with basic metadata.
246
+ # @see Deck.parse
247
+ class Card
248
+ def initialize(id, name, cost)
249
+ @id = id
250
+ @name = name
251
+ @cost = cost
252
+ end
216
253
 
217
- def standard?
218
- format == Format.standard
219
- end
254
+ # @see https://hearthstonejson.com/ HearthstoneJSON for card metadata.
255
+ # @return [Integer] Hearthstone DBF ID of the card.
256
+ attr_reader :id
257
+
258
+ # @return [String] Name of the card.
259
+ attr_reader :name
220
260
 
221
- def to_s
222
- hero = @heroes.first
223
- "Format: #{format}\nHero: #{hero.name} (#{hero.hero_class})\n" + cards.map do |card, count|
224
- "#{count}× #{card.name}"
225
- end.join("\n")
261
+ # @return [Integer] Mana cost of the card.
262
+ attr_reader :cost
226
263
  end
227
264
 
228
- attr_reader :format, :heroes, :cards
229
- end
265
+ # A Hearthstone deck convertible to and from a deckstring.
266
+ # @see Deck.parse
267
+ # @see Deck#deckstring
268
+ class Deck
269
+ def initialize(format:, heroes:, cards:)
270
+ # TODO: Translate RangeError -> FormatError.
230
271
 
231
- class Deckstring
232
- using VarIntExtensions
272
+ @format = Format.parse(format) if !format.is_a?(Format)
273
+ if !@format
274
+ raise FormatError.new("Unknown format: #{format}.")
275
+ end
233
276
 
234
- def self.encode(format:, heroes:, cards:)
235
- if format.nil?
236
- # TODO
277
+ @heroes = heroes.map do |id|
278
+ hero = Database.instance.heroes[id]
279
+ raise FormatError.new("Unknown hero: #{id}.") if hero.nil?
280
+ Hero.new(id, hero['name'], hero['class'])
281
+ end
282
+
283
+ @cards = cards.map do |id, count|
284
+ card = Database.instance.cards[id]
285
+ raise FormatError.new("Unknown card: #{id}.") if card.nil?
286
+ [Card.new(id, card['name'], card['cost']), count]
287
+ end.sort_by { |card, _| card.cost }.to_h
237
288
  end
238
289
 
239
- if heroes.nil?
240
- # TODO
290
+ # @see .encode
291
+ # @see .decode
292
+ def raw
293
+ heroes = @heroes.map(&:id)
294
+ cards = @cards.map { |card, count| [card.id, count] }.to_h
295
+ { format: @format.value, heroes: heroes, cards: cards }
241
296
  end
242
297
 
243
- if cards.nil?
244
- # TODO
298
+ # @return [String] Base64-encoded compact byte string representing the deck.
299
+ # @see .encode
300
+ def deckstring
301
+ return Deckstrings::encode(self.raw)
245
302
  end
246
303
 
304
+ # @see .decode
305
+ # @see #deckstring
306
+ def self.parse(deckstring)
307
+ parts = Deckstrings::decode(deckstring)
308
+ Deck.new(parts)
309
+ end
310
+
311
+ # @return [Boolean] `true` if the deck is Wild format, `false` otherwise.
312
+ # @see #standard?
313
+ def wild?
314
+ format.wild?
315
+ end
316
+
317
+ # @return [Boolean] `true` if the deck is Standard format, `false` otherwise.
318
+ # @see #wild?
319
+ def standard?
320
+ format.standard?
321
+ end
322
+
323
+ # @example
324
+ # Format: Standard
325
+ # Class: Druid
326
+ # Hero: Malfurion Stormrage
327
+ #
328
+ # 2× Innervate
329
+ # 2× Jade Idol
330
+ # 2× Wild Growth
331
+ # 2× Wrath
332
+ # 2× Jade Blossom
333
+ # 2× Swipe
334
+ # 2× Jade Spirit
335
+ # 1× Fandral Staghelm
336
+ # 1× Spellbreaker
337
+ # 2× Nourish
338
+ # 1× Big Game Hunter
339
+ # 2× Spreading Plague
340
+ # 1× The Black Knight
341
+ # 1× Aya Blackpaw
342
+ # 2× Jade Behemoth
343
+ # 1× Malfurion the Pestilent
344
+ # 1× Primordial Drake
345
+ # 2× Ultimate Infestation
346
+ # 1× Kun the Forgotten King
347
+ # @return [String] A pretty-printed listing of deck details.
348
+ def to_s
349
+ 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")
353
+ end
354
+
355
+ # @return [Format] Format for this deck.
356
+ attr_reader :format
357
+
358
+ # @return [Array<Hero>] Heroes associated with this deck. Typically, this array will contain one element.
359
+ attr_reader :heroes
360
+
361
+ # @return [Hash{Card => Integer}] The cards contained in the deck. A map between {Card} and the instance count in the deck.
362
+ attr_reader :cards
363
+ end
364
+
365
+ using VarIntExtensions
366
+
367
+ # Encodes a Hearthstone deck as a compact deckstring.
368
+ # @example
369
+ # Deckstrings.encode(format: 2, heroes: [7], cards: { 44 => 1, 45 => 2 })
370
+ # @example
371
+ # Deckstrings.encode(
372
+ # format: Deckstrings::Format.standard,
373
+ # heroes: [Deckstrings::Hero.warrior],
374
+ # cards: { 1 => 2, 2 => 2 }
375
+ # )
376
+ # @param format [Integer, Deckstrings::Format] Format for this deck: wild or standard.
377
+ # @param heroes [Array<Integer, Deckstrings::Hero>] Heroes for this deck. Multiple heroes are supported, but typically
378
+ # 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.
381
+ # @return [String] Base64-encoded compact byte string representing the deck.
382
+ # @see Deck#deckstring
383
+ # @see .decode
384
+ def self.encode(format:, heroes:, cards:)
247
385
  stream = StringIO.new('')
248
386
 
387
+ format = format.is_a?(Deckstrings::Format) ? format.value : format
388
+ heroes = heroes.map { |hero| hero.is_a?(Deckstrings::Hero) ? hero.id : hero }
389
+
249
390
  # Reserved slot, version, and format.
250
391
  stream.write_varint(0)
251
392
  stream.write_varint(1)
@@ -260,6 +401,11 @@ class Deckstring
260
401
  # Cards.
261
402
  by_count = cards.group_by { |id, n| n > 2 ? 3 : n }
262
403
 
404
+ invalid = by_count.keys.select { |count| count < 1 }
405
+ unless invalid.empty?
406
+ raise ArgumentError.new("Invalid card count: #{invalid.join(', ')}.")
407
+ end
408
+
263
409
  1.upto(3) do |count|
264
410
  group = by_count[count] || []
265
411
  stream.write_varint(group.length)
@@ -272,46 +418,60 @@ class Deckstring
272
418
  Base64::encode64(stream.string).strip
273
419
  end
274
420
 
421
+ # Decodes a Hearthstone deckstring into format, hero, and card details.
422
+ # @example
423
+ # deck = Deckstrings::decode('AAEBAf0GAA/yAaIC3ALgBPcE+wWKBs4H2QexCMII2Q31DfoN9g4A')
424
+ # @example
425
+ # deck = Deckstrings::decode('AAECAZICCPIF+Az5DK6rAuC7ApS9AsnHApnTAgtAX/4BxAbkCLS7Asu8As+8At2+AqDNAofOAgA=')
426
+ # @param deckstring [String] Base64-encoded Hearthstone deckstring.
427
+ # @raise [FormatError] If the deckstring is malformed or contains invalid deck data.
428
+ # @return [{ format: Integer, heroes: Array<Integer>, cards: Hash{Integer => Integer} }] Parsed Hearthstone deck details.
429
+ # @see Deck#parse
430
+ # @see .encode
275
431
  def self.decode(deckstring)
276
- if !deckstring
277
- # TODO
278
- end
279
-
280
- stream = StringIO.new(Base64::decode64(deckstring))
432
+ begin
433
+ if deckstring.nil? || deckstring.empty?
434
+ raise ArgumentError.new('Invalid deckstring.')
435
+ end
281
436
 
282
- reserved = stream.read_varint
283
- if reserved != 0
284
- raise FormatError.new("Unexpected reserved byte: #{reserved}.")
285
- end
437
+ stream = StringIO.new(Base64::decode64(deckstring))
286
438
 
287
- version = stream.read_varint
288
- if version != 1
289
- raise FormatError.new("Unexpected version: #{version}.")
290
- end
439
+ reserved = stream.read_varint
440
+ if reserved != 0
441
+ raise FormatError.new("Unexpected reserved byte: #{reserved}.")
442
+ end
291
443
 
292
- format = stream.read_varint
444
+ version = stream.read_varint
445
+ if version != 1
446
+ raise FormatError.new("Unexpected version: #{version}.")
447
+ end
293
448
 
294
- # Heroes
295
- heroes = []
296
- length = stream.read_varint
297
- length.times do
298
- heroes << stream.read_varint
299
- end
449
+ format = stream.read_varint
300
450
 
301
- # Cards
302
- cards = {}
303
- 1.upto(3) do |i|
451
+ # Heroes
452
+ heroes = []
304
453
  length = stream.read_varint
305
454
  length.times do
306
- card = stream.read_varint
307
- cards[card] = i < 3 ? i : stream.read_varint
455
+ heroes << stream.read_varint
308
456
  end
309
- end
310
457
 
311
- return {
312
- format: format,
313
- heroes: heroes,
314
- cards: cards
315
- }
458
+ # Cards
459
+ cards = {}
460
+ 1.upto(3) do |i|
461
+ length = stream.read_varint
462
+ length.times do
463
+ card = stream.read_varint
464
+ cards[card] = i < 3 ? i : stream.read_varint
465
+ end
466
+ end
467
+
468
+ return {
469
+ format: format,
470
+ heroes: heroes,
471
+ cards: cards
472
+ }
473
+ rescue EOFError
474
+ raise FormatError, 'Unexpected end of data.'
475
+ end
316
476
  end
317
477
  end
@@ -1,39 +1,49 @@
1
- module Enum
2
- def self.included(base)
3
- base.extend ClassMethods
4
- end
1
+ module Deckstrings
2
+ module Enum
3
+ def self.included(base)
4
+ base.extend ClassMethods
5
+ end
5
6
 
6
- module ClassMethods
7
- def define(symbol, value, display = nil)
8
- @@values ||= {}
9
- @@values[value] = instance = self.new(symbol, value, display)
10
- self.class.send :define_method, symbol do
11
- instance
7
+ # @private
8
+ module ClassMethods
9
+ def define(symbol, value, display = nil)
10
+ @@values ||= {}
11
+ @@values[value] = instance = self.new(symbol, value, display)
12
+ self.class.send :define_method, symbol do
13
+ instance
14
+ end
15
+ self.send :define_method, "#{symbol}?".to_sym do
16
+ @symbol == symbol
17
+ end
12
18
  end
13
- end
14
19
 
15
- def parse(value)
16
- enum = @@values[value]
17
- raise RangeError.new("Unknown value: #{value}.") if !enum
18
- enum
20
+ def parse(value)
21
+ enum = @@values[value]
22
+ raise RangeError.new("Unknown value: #{value}.") if !enum
23
+ enum
24
+ end
19
25
  end
20
- end
21
26
 
22
- def initialize(symbol, value, display)
23
- @symbol = symbol
24
- @value = value
25
- @display = display
26
- end
27
+ # @private
28
+ def initialize(symbol, value, display)
29
+ @symbol = symbol
30
+ @value = value
31
+ @display = display
32
+ end
27
33
 
28
- def to_s
29
- @display || @symbol.to_s
30
- end
34
+ # @return [String] A string description of this enum instance.
35
+ def to_s
36
+ @display || @symbol.to_s
37
+ end
31
38
 
32
- def symbol
33
- @symbol
34
- end
39
+ # @return [Symbol] The unique symbol for this enum instance.
40
+ def symbol
41
+ @symbol
42
+ end
35
43
 
36
- def value
37
- @value
44
+ # @return [Integer, String, Object] The parseable value for this enum instance.
45
+ def value
46
+ @value
47
+ end
38
48
  end
39
49
  end
@@ -1,29 +1,32 @@
1
- module VarIntExtensions
2
- refine StringIO do
3
- def read_varint
4
- num = 0
5
- shift = 0
6
- loop do
7
- octet = self.getbyte
8
- raise FormatError.new('Unexpected end of data.') if octet.nil?
1
+ module Deckstrings
2
+ # @private
3
+ module VarIntExtensions
4
+ refine StringIO do
5
+ def read_varint
6
+ num = 0
7
+ shift = 0
8
+ loop do
9
+ octet = self.getbyte
10
+ raise EOFError.new('Unexpected end of data.') if octet.nil?
9
11
 
10
- num |= (octet & 0x7f) << shift
11
- return num if octet & 0x80 == 0
12
- shift += 7
12
+ num |= (octet & 0x7f) << shift
13
+ return num if octet & 0x80 == 0
14
+ shift += 7
15
+ end
13
16
  end
14
- end
15
17
 
16
- def write_varint(num)
17
- octets = []
18
- loop do
19
- octet = num & 0x7f
20
- if (num >>= 7) > 0
21
- octet |= 0x80
22
- octets << (octet | 0x80)
23
- else
24
- octets << octet
25
- self.write(octets.pack('C*'))
26
- return
18
+ def write_varint(num)
19
+ octets = []
20
+ loop do
21
+ octet = num & 0x7f
22
+ if (num >>= 7) > 0
23
+ octet |= 0x80
24
+ octets << (octet | 0x80)
25
+ else
26
+ octets << octet
27
+ self.write(octets.pack('C*'))
28
+ return
29
+ end
27
30
  end
28
31
  end
29
32
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: deckstrings
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.0.2
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-06 00:00:00.000000000 Z
11
+ date: 2017-09-15 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: Ruby library for encoding and decoding Hearthstone deckstrings.
14
14
  email: schmch@gmail.com
@@ -17,12 +17,12 @@ extensions: []
17
17
  extra_rdoc_files: []
18
18
  files:
19
19
  - LICENSE
20
+ - README.md
20
21
  - lib/deckstrings.rb
21
22
  - lib/deckstrings/database.json
22
23
  - lib/deckstrings/deckstrings.rb
23
24
  - lib/deckstrings/enum.rb
24
25
  - lib/deckstrings/varint.rb
25
- - lib/deckstrings/version.rb
26
26
  homepage: https://github.com/schmich/hearthstone-deckstrings
27
27
  licenses:
28
28
  - MIT
@@ -35,7 +35,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
35
35
  requirements:
36
36
  - - ">="
37
37
  - !ruby/object:Gem::Version
38
- version: 2.0.0
38
+ version: 2.1.0
39
39
  required_rubygems_version: !ruby/object:Gem::Requirement
40
40
  requirements:
41
41
  - - ">="
@@ -1,3 +0,0 @@
1
- module Deckstrings
2
- VERSION = '0.0.1'
3
- end