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 +4 -4
- data/README.md +76 -0
- data/lib/deckstrings.rb +0 -1
- data/lib/deckstrings/deckstrings.rb +370 -210
- data/lib/deckstrings/enum.rb +39 -29
- data/lib/deckstrings/varint.rb +26 -23
- metadata +4 -4
- data/lib/deckstrings/version.rb +0 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 80a4c98e3e84f0390bac22795833f6b07e87668b
|
4
|
+
data.tar.gz: aac1b9bb8f72e78dc1c37ce7d800b21b9bfea124
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 995a352c3728e5fa27aef25e0b1b41d0f39de4535e42e7b6f8fa918b48618dccbe7176503242910233c7ef3ecd8f866b28066b9756612250fc10c18ec9060843
|
7
|
+
data.tar.gz: 2364c4b40827f0971386195cf1d92b151fdbf448c0ff2dd95c4d58e3f3177d542984cf7236b13b9bbfc8b27ffdd04290d4c0eadfd75bf57f9ff16f39b34e83ed
|
data/README.md
ADDED
@@ -0,0 +1,76 @@
|
|
1
|
+
# Hearthstone Deckstrings [](http://rubygems.org/gems/deckstrings) [](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,251 +1,392 @@
|
|
1
1
|
require 'base64'
|
2
2
|
require 'json'
|
3
3
|
|
4
|
-
|
5
|
-
|
6
|
-
|
4
|
+
module Deckstrings
|
5
|
+
class FormatError < StandardError
|
6
|
+
def initialize(message)
|
7
|
+
super(message)
|
8
|
+
end
|
7
9
|
end
|
8
|
-
end
|
9
10
|
|
10
|
-
|
11
|
-
|
12
|
-
|
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
|
17
|
-
|
18
|
-
|
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
|
30
|
-
|
31
|
-
|
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
|
-
|
36
|
-
|
37
|
-
end
|
24
|
+
class HeroClass
|
25
|
+
include Enum
|
38
26
|
|
39
|
-
|
40
|
-
@
|
41
|
-
|
42
|
-
end
|
43
|
-
end
|
27
|
+
# @!scope class
|
28
|
+
# @return [HeroClass] Mage class.
|
29
|
+
define :mage, 'mage', 'Mage'
|
44
30
|
|
45
|
-
|
46
|
-
@
|
47
|
-
|
48
|
-
end
|
49
|
-
end
|
50
|
-
end
|
31
|
+
# @!scope class
|
32
|
+
# @return [HeroClass] Rogue class.
|
33
|
+
define :rogue, 'rogue', 'Rogue'
|
51
34
|
|
52
|
-
class
|
53
|
-
|
54
|
-
|
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
|
-
|
60
|
-
|
61
|
-
|
39
|
+
# @!scope class
|
40
|
+
# @return [HeroClass] Hunter class.
|
41
|
+
define :hunter, 'hunter', 'Hunter'
|
62
42
|
|
63
|
-
|
64
|
-
|
65
|
-
|
43
|
+
# @!scope class
|
44
|
+
# @return [HeroClass] Shaman class.
|
45
|
+
define :shaman, 'shaman', 'Shaman'
|
66
46
|
|
67
|
-
|
68
|
-
|
69
|
-
|
47
|
+
# @!scope class
|
48
|
+
# @return [HeroClass] Priest class.
|
49
|
+
define :priest, 'priest', 'Priest'
|
70
50
|
|
71
|
-
|
72
|
-
|
73
|
-
|
51
|
+
# @!scope class
|
52
|
+
# @return [HeroClass] Warrior class.
|
53
|
+
define :warrior, 'warrior', 'Warrior'
|
74
54
|
|
75
|
-
|
76
|
-
|
77
|
-
|
55
|
+
# @!scope class
|
56
|
+
# @return [HeroClass] Paladin class.
|
57
|
+
define :paladin, 'paladin', 'Paladin'
|
78
58
|
|
79
|
-
|
80
|
-
|
59
|
+
# @!scope class
|
60
|
+
# @return [HeroClass] Warlock class.
|
61
|
+
define :warlock, 'warlock', 'Warlock'
|
81
62
|
end
|
82
63
|
|
83
|
-
|
84
|
-
|
85
|
-
|
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
|
-
|
88
|
-
|
89
|
-
|
71
|
+
def self.instance
|
72
|
+
@@instance ||= Database.new
|
73
|
+
end
|
90
74
|
|
91
|
-
|
92
|
-
|
93
|
-
|
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
|
-
|
96
|
-
|
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
|
-
|
100
|
-
|
101
|
-
|
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
|
-
|
104
|
-
self.
|
105
|
-
|
97
|
+
# @return [Hero] Jaina Proudmoore.
|
98
|
+
def self.mage
|
99
|
+
self.jaina
|
100
|
+
end
|
106
101
|
|
107
|
-
|
108
|
-
self.
|
109
|
-
|
102
|
+
# @return [Hero] Jaina Proudmoore.
|
103
|
+
def self.jaina
|
104
|
+
self.from_id(637)
|
105
|
+
end
|
110
106
|
|
111
|
-
|
112
|
-
self.
|
113
|
-
|
107
|
+
# @return [Hero] Khadgar.
|
108
|
+
def self.khadgar
|
109
|
+
self.from_id(39117)
|
110
|
+
end
|
114
111
|
|
115
|
-
|
116
|
-
self.
|
117
|
-
|
112
|
+
# @return [Hero] Valeera Sanguinar.
|
113
|
+
def self.rogue
|
114
|
+
self.valeera
|
115
|
+
end
|
118
116
|
|
119
|
-
|
120
|
-
self.
|
121
|
-
|
117
|
+
# @return [Hero] Valeera Sanguinar.
|
118
|
+
def self.valeera
|
119
|
+
self.from_id(930)
|
120
|
+
end
|
122
121
|
|
123
|
-
|
124
|
-
self.
|
125
|
-
|
122
|
+
# @return [Hero] Maiev Shadowsong.
|
123
|
+
def self.maiev
|
124
|
+
self.from_id(40195)
|
125
|
+
end
|
126
126
|
|
127
|
-
|
128
|
-
self.
|
129
|
-
|
127
|
+
# @return [Hero] Malfurion Stormrage.
|
128
|
+
def self.druid
|
129
|
+
self.malfurion
|
130
|
+
end
|
130
131
|
|
131
|
-
|
132
|
-
self.
|
133
|
-
|
132
|
+
# @return [Hero] Malfurion Stormrage.
|
133
|
+
def self.malfurion
|
134
|
+
self.from_id(274)
|
135
|
+
end
|
134
136
|
|
135
|
-
|
136
|
-
self.
|
137
|
-
|
137
|
+
# @return [Hero] Thrall.
|
138
|
+
def self.shaman
|
139
|
+
self.thrall
|
140
|
+
end
|
138
141
|
|
139
|
-
|
140
|
-
self.
|
141
|
-
|
142
|
+
# @return [Hero] Thrall.
|
143
|
+
def self.thrall
|
144
|
+
self.from_id(1066)
|
145
|
+
end
|
142
146
|
|
143
|
-
|
144
|
-
self.
|
145
|
-
|
147
|
+
# @return [Hero] Morgl the Oracle.
|
148
|
+
def self.morgl
|
149
|
+
self.from_id(40183)
|
150
|
+
end
|
146
151
|
|
147
|
-
|
148
|
-
self.
|
149
|
-
|
152
|
+
# @return [Hero] Anduin Wrynn.
|
153
|
+
def self.priest
|
154
|
+
self.anduin
|
155
|
+
end
|
150
156
|
|
151
|
-
|
152
|
-
self.
|
153
|
-
|
157
|
+
# @return [Hero] Anduin Wrynn.
|
158
|
+
def self.anduin
|
159
|
+
self.from_id(813)
|
160
|
+
end
|
154
161
|
|
155
|
-
|
156
|
-
self.
|
157
|
-
|
162
|
+
# @return [Hero] Tyrande Whisperwind.
|
163
|
+
def self.tyrande
|
164
|
+
self.from_id(41887)
|
165
|
+
end
|
158
166
|
|
159
|
-
|
160
|
-
self.
|
161
|
-
|
167
|
+
# @return [Hero] Rexxar.
|
168
|
+
def self.hunter
|
169
|
+
self.rexxar
|
170
|
+
end
|
162
171
|
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
end
|
172
|
+
# @return [Hero] Rexxar.
|
173
|
+
def self.rexxar
|
174
|
+
self.from_id(31)
|
175
|
+
end
|
168
176
|
|
169
|
-
|
170
|
-
|
177
|
+
# @return [Hero] Alleria Windrunner.
|
178
|
+
def self.alleria
|
179
|
+
self.from_id(2826)
|
180
|
+
end
|
171
181
|
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
@cost = cost
|
177
|
-
end
|
182
|
+
# @return [Hero] Gul'dan.
|
183
|
+
def self.warlock
|
184
|
+
self.guldan
|
185
|
+
end
|
178
186
|
|
179
|
-
|
180
|
-
|
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
|
-
|
183
|
-
|
184
|
-
|
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
|
-
@
|
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
|
-
@
|
196
|
-
|
197
|
-
|
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
|
-
|
203
|
-
|
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
|
-
|
209
|
-
|
210
|
-
Deck.new(parts)
|
241
|
+
# @return [HeroClass] Class of the hero.
|
242
|
+
attr_reader :hero_class
|
211
243
|
end
|
212
244
|
|
213
|
-
|
214
|
-
|
215
|
-
|
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
|
-
|
218
|
-
|
219
|
-
|
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
|
-
|
222
|
-
|
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
|
-
|
229
|
-
|
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
|
-
|
232
|
-
|
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
|
-
|
235
|
-
|
236
|
-
|
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
|
-
|
240
|
-
|
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
|
-
|
244
|
-
|
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
|
-
|
277
|
-
|
278
|
-
|
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
|
-
|
283
|
-
if reserved != 0
|
284
|
-
raise FormatError.new("Unexpected reserved byte: #{reserved}.")
|
285
|
-
end
|
437
|
+
stream = StringIO.new(Base64::decode64(deckstring))
|
286
438
|
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
439
|
+
reserved = stream.read_varint
|
440
|
+
if reserved != 0
|
441
|
+
raise FormatError.new("Unexpected reserved byte: #{reserved}.")
|
442
|
+
end
|
291
443
|
|
292
|
-
|
444
|
+
version = stream.read_varint
|
445
|
+
if version != 1
|
446
|
+
raise FormatError.new("Unexpected version: #{version}.")
|
447
|
+
end
|
293
448
|
|
294
|
-
|
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
|
-
|
302
|
-
|
303
|
-
1.upto(3) do |i|
|
451
|
+
# Heroes
|
452
|
+
heroes = []
|
304
453
|
length = stream.read_varint
|
305
454
|
length.times do
|
306
|
-
|
307
|
-
cards[card] = i < 3 ? i : stream.read_varint
|
455
|
+
heroes << stream.read_varint
|
308
456
|
end
|
309
|
-
end
|
310
457
|
|
311
|
-
|
312
|
-
|
313
|
-
|
314
|
-
|
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
|
data/lib/deckstrings/enum.rb
CHANGED
@@ -1,39 +1,49 @@
|
|
1
|
-
module
|
2
|
-
|
3
|
-
base
|
4
|
-
|
1
|
+
module Deckstrings
|
2
|
+
module Enum
|
3
|
+
def self.included(base)
|
4
|
+
base.extend ClassMethods
|
5
|
+
end
|
5
6
|
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
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
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
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
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
+
# @private
|
28
|
+
def initialize(symbol, value, display)
|
29
|
+
@symbol = symbol
|
30
|
+
@value = value
|
31
|
+
@display = display
|
32
|
+
end
|
27
33
|
|
28
|
-
|
29
|
-
|
30
|
-
|
34
|
+
# @return [String] A string description of this enum instance.
|
35
|
+
def to_s
|
36
|
+
@display || @symbol.to_s
|
37
|
+
end
|
31
38
|
|
32
|
-
|
33
|
-
|
34
|
-
|
39
|
+
# @return [Symbol] The unique symbol for this enum instance.
|
40
|
+
def symbol
|
41
|
+
@symbol
|
42
|
+
end
|
35
43
|
|
36
|
-
|
37
|
-
|
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
|
data/lib/deckstrings/varint.rb
CHANGED
@@ -1,29 +1,32 @@
|
|
1
|
-
module
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
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
|
-
|
11
|
-
|
12
|
-
|
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
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
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.
|
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-
|
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.
|
38
|
+
version: 2.1.0
|
39
39
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
40
40
|
requirements:
|
41
41
|
- - ">="
|
data/lib/deckstrings/version.rb
DELETED