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 +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 [![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,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