deckstrings 0.0.1 → 0.0.2

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: 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