cw_card_utils 0.1.2 → 0.1.6

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
  SHA256:
3
- metadata.gz: 211b10611a7f848b2de2e4500875a0ccdc2eda0f4ece2f1fdd8e69d9b43eae8f
4
- data.tar.gz: be91019d5a649e3f78bd973e9247c3a814d2c5a6bdf0362226330a0b311dcc26
3
+ metadata.gz: a8d8eb329c364d67fedf00a49698b9f145f06b6e08f7effae3eb92fb5c22d244
4
+ data.tar.gz: 8cc571695d9d0b3b07c2da11abffe0a047d08591c8b490fbaffdf88a21226fe5
5
5
  SHA512:
6
- metadata.gz: 8d068f85095b12b305e55f75a5192e3eca23b23b6edd17535214d3294c3fdbe8824d31d706b16a7c5aff3aaf49b6177cd1d631fa7f115f5aea01e340b956a6e3
7
- data.tar.gz: ad007fc86fbef1a975224268321d918df6dbcedda7fb79f32a0731806a09e68e9dfeaf3ac5bc42f7ae5e51ef54e20985ae79d86f6d0f9a3e4d04be8407fb3cc2
6
+ metadata.gz: 38000b90cc9c72b84efdf4c8eb892d20748fe4050af8874c99847a9e4b572b4fcbe573cfba49a964fcda60b8d177bbf9195f3f9b89c9f92c4f08d716d18c41ca
7
+ data.tar.gz: 9f62737b9b7fde768f4f990b0367b94b6e86d8bcab4a4925e1c91a8c6abfaae71a2ac56bd8ecfe5ffb74ec956010ad3c6d8658e102c032eeb3514e8488e0e79f
@@ -0,0 +1,108 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CwCardUtils
4
+
5
+ class DeckComparator
6
+ attr_reader :deck_a, :deck_b, :analysis
7
+
8
+ def initialize(deck_a, deck_b)
9
+ @deck_a = deck_a
10
+ @deck_b = deck_b
11
+ @analysis = {}
12
+ end
13
+
14
+ def compare
15
+ a_info = analyze_deck(deck_a)
16
+ b_info = analyze_deck(deck_b)
17
+
18
+ matchup = {
19
+ archetype_a: a_info[:archetype],
20
+ archetype_b: b_info[:archetype],
21
+ likely_winner: predict_matchup(a_info, b_info),
22
+ interaction_overlap: compare_interaction_density(a_info, b_info),
23
+ notes: generate_notes(a_info, b_info),
24
+ }
25
+
26
+ @analysis = matchup
27
+ end
28
+
29
+ private
30
+
31
+ def analyze_deck(deck)
32
+ detector = ArchetypeDetector.new(deck)
33
+ archetype = detector.detect
34
+
35
+ curve = deck.normalized_curve
36
+
37
+ early = ((curve[0] || 0) + (curve[1] || 0)).round(2)
38
+ mid = ((curve[2] || 0) + (curve[3] || 0)).round(2)
39
+ late = ((curve[4] || 0) + (curve[5] || 0) + (curve[6] || 0) + (curve[7] || 0)).round(2)
40
+
41
+ {
42
+ archetype: archetype,
43
+ color: detector.colors,
44
+ early_curve: early,
45
+ mid_curve: mid,
46
+ late_curve: late,
47
+ tag_ratios: detector.tag_ratios,
48
+ tag_counts: detector.tag_counts,
49
+ tribe: deck.tribe,
50
+ format: deck.format,
51
+ }
52
+ end
53
+
54
+ def predict_matchup(a, b)
55
+ return "Deck A (Aggro)" if is_aggro?(a) && is_control?(b)
56
+ return "Deck B (Aggro)" if is_aggro?(b) && is_control?(a)
57
+ return "Deck A (Faster curve)" if a[:early_curve] > b[:early_curve] + 0.15
58
+ return "Deck B (Faster curve)" if b[:early_curve] > a[:early_curve] + 0.15
59
+ "Even Matchup"
60
+ end
61
+
62
+ def is_aggro?(info)
63
+ info[:archetype].downcase.include?("aggro") || info[:early_curve] > 0.5
64
+ end
65
+
66
+ def is_control?(info)
67
+ info[:archetype].downcase.include?("control") || info[:late_curve] > 0.4
68
+ end
69
+
70
+ def compare_interaction_density(a, b)
71
+ a_interact = a[:tag_ratios][:interaction].to_f.round(2)
72
+ b_interact = b[:tag_ratios][:interaction].to_f.round(2)
73
+
74
+ if (a_interact - b_interact).abs < 0.05
75
+ "Similar density"
76
+ elsif a_interact > b_interact
77
+ "Deck A has more removal"
78
+ else
79
+ "Deck B has more removal"
80
+ end
81
+ end
82
+
83
+ def generate_notes(a, b)
84
+ notes = []
85
+
86
+ if a[:tribe] && b[:tribe]
87
+ notes << "Both decks are tribal: #{a[:tribe].capitalize} vs #{b[:tribe].capitalize}"
88
+ end
89
+
90
+ if a[:format] != b[:format]
91
+ notes << "Decks are from different formats: #{a[:format]} vs #{b[:format]}"
92
+ end
93
+
94
+ if a[:color] == b[:color]
95
+ notes << "Color overlap may result in symmetrical strategies"
96
+ end
97
+
98
+ notes << "Deck A has a stronger early game" if a[:early_curve] > b[:early_curve] + 0.2
99
+ notes << "Deck B has a stronger early game" if b[:early_curve] > a[:early_curve] + 0.2
100
+
101
+ notes << "Deck A has more combo elements" if a[:tag_ratios][:combo_piece].to_f > 0.1
102
+ notes << "Deck B has more combo elements" if b[:tag_ratios][:combo_piece].to_f > 0.1
103
+
104
+ notes
105
+ end
106
+ end
107
+
108
+ end
@@ -25,37 +25,34 @@ module CwCardUtils
25
25
  def tags
26
26
  t = []
27
27
 
28
- # Threat: beefy enough to demand an answer
28
+ # === Threat ===
29
29
  t << :threat if creature? && @power >= 3
30
30
 
31
- # Finishers: expensive and game-ending
31
+ # === Finisher ===
32
32
  t << :finisher if likely_finisher?
33
33
 
34
- # Ramp
34
+ # === Ramp ===
35
35
  t << :ramp if @text.match?(/add .* mana|search.*basic|put.*land|mana pool/)
36
36
 
37
- # Draw / value
37
+ # === Draw ===
38
38
  t << :draw if @text.match?(/draw.*card|investigate|learn/)
39
39
 
40
- # Tutors
40
+ # === Tutor ===
41
41
  t << :tutor if @text.match?(/search.*library/)
42
42
 
43
- # Interaction
44
- if @text.match?(/destroy|exile|counter|fight|return.*to.*hand|sacrifice.*creature|discard/i)
45
- t << :interaction
46
- end
43
+ # === Interaction ===
44
+ t << :interaction if @text.match?(/destroy|exile|counter|fight|return.*to.*hand|sacrifice.*creature|discard/i)
47
45
 
48
- # Lock / taxes
49
- if @text.match?(/players can'?t|opponents can'?t|skip.*step|each player.*sacrifice|tax/i)
50
- t << :lock_piece
51
- end
46
+ # === Lock Pieces ===
47
+ t << :lock_piece if @text.match?(/players can't|creatures can't|skip.*step|can't untap|prevent all combat damage|each player.*sacrifice/i)
52
48
 
53
- # Combo
54
- if @text.match?(/infinite|copy.*spell|storm|you win the game/i)
55
- t << :combo_piece
56
- end
49
+ # === Tax Effects ===
50
+ t << :tax if @text.match?(/whenever an opponent|opponents? .* pay|opponents? .* can't|noncreature spells .* cost|can't cast more than/i)
57
51
 
58
- # ETB
52
+ # === Combo ===
53
+ t << :combo_piece if @text.match?(/infinite|copy.*spell|storm|you win the game/i)
54
+
55
+ # === ETB Effects ===
59
56
  if @text.match?(/when.*enters.*the battlefield/)
60
57
  t << :etb
61
58
  t << :etb_buff if @text.match?(/creatures.*get.*\+/)
@@ -63,50 +60,57 @@ module CwCardUtils
63
60
  t << :etb_removal if @text.match?(/destroy|exile|tap|fight/)
64
61
  end
65
62
 
66
- # Evasion / scaling
63
+ # === Evasion / Scaling Threats ===
67
64
  if @keywords.any? { |kw| %w[flying trample menace unblockable].include?(kw) } || @text.match?(/flying|trample|menace|unblockable/)
68
65
  t << :evasive
69
66
  end
70
67
 
71
- # Synergistic finishers
72
- if @text.match?(/gets \+x\/\+x|for each other|creatures you control/i)
68
+ if @text.match?(/gets \+x\/\+x|for each other|creatures you control|other .*#{@tribe}s? you control/i)
73
69
  t << :scaling_threat
74
70
  t << :synergistic_finisher if @cmc <= 4 && t.include?(:evasive)
75
71
  end
76
72
 
77
- # Go-wide / synergy engine
73
+ # === Go-Wide Synergy ===
78
74
  if @text.match?(/whenever.*creature.*enters|token.*you control/i)
79
75
  t << :synergy
80
76
  end
81
77
 
78
+ # === Choose-a-type tribal enablers ===
82
79
  if @text.include?("choose a creature type")
83
80
  t << :tribal_synergy
84
- t << :tribal_creature if creature_subtypes.include?("vehicle")
85
-
81
+ t << :tribal_creature
86
82
  if @text.match?(/creatures.*chosen type.*get.*\+.*\+|whenever.*creature.*of the chosen type/i)
87
83
  t << :threat unless t.include?(:threat)
88
84
  end
89
85
  end
90
86
 
91
- # Tribal
87
+ if creature? && @power <= 2 && @tribe &&
88
+ (@text.include?("whenever") || @text.include?("enters") || @text.include?("lifelink") || @text.include?("vigilance") || @text.include?("+1/+1"))
89
+ t << :threat
90
+ end
91
+
92
+ # === Static tribal buffs ===
92
93
  if @tribe
93
- if creature_subtypes.include?(@tribe)
94
- t << :tribal_creature
95
- end
94
+ t << :tribal_creature if creature_subtypes.include?(@tribe)
95
+ t << :tribal_synergy if @text.include?("#{@tribe}s") || @text.include?("other #{@tribe}")
96
96
 
97
- # Tribal synergy via text (buffs, token creation, etc.)
98
- if @text.include?("#{@tribe}s") || @text.include?("other #{@tribe}")
97
+ if @text.match?(/other .*#{@tribe}s? you control.*\+\d\/\+\d|creature you control of the chosen type.*\+\d\/\+\d/i)
99
98
  t << :tribal_synergy
99
+ t << :threat unless t.include?(:threat)
100
100
  end
101
101
 
102
- # Static buffs to tribe = also a threat
103
- if @text.match?(/other .*#{@tribe}s? you control.*\+\d\/\+\d|creature you control of the chosen type.*\+\d\/\+\d|/i)
104
- t << :tribal_synergy
102
+ # Threat based on synergy
103
+ if @tribe && @text.match?(/creatures.*you control.*#{@tribe}|gets \+1\/\+1 for each.*#{@tribe}/)
104
+ t << :scaling_threat
105
105
  t << :threat unless t.include?(:threat)
106
106
  end
107
107
  end
108
108
 
109
- t.uniq
109
+ if t.include?(:scaling_threat) && !t.include?(:threat) # rubocop:disable Style/InverseMethods
110
+ t << :threat
111
+ end
112
+
113
+ t.uniq.sort
110
114
  end
111
115
 
112
116
  private
@@ -121,7 +125,7 @@ module CwCardUtils
121
125
 
122
126
  def creature_subtypes
123
127
  return [] unless creature?
124
- raw = @type_line.split(/[—-]/).last.to_s.strip # handles both em dash and hyphen
128
+ raw = @type_line.split(/[—-]/).last.to_s.strip
125
129
  raw.split.map(&:downcase)
126
130
  end
127
131
 
@@ -132,6 +136,118 @@ module CwCardUtils
132
136
  end
133
137
  end
134
138
 
139
+ class ArchetypeDetector
140
+ attr_reader :deck, :format, :tribe, :colors, :tag_counts, :tag_ratios
141
+
142
+ def initialize(deck)
143
+ @deck = deck
144
+ @format = deck.format.to_s.downcase # :standard, :edh, etc.
145
+ @tribe = deck.tribe&.capitalize # e.g., "Cat"
146
+ @colors = resolve_color_identity(deck)
147
+ @tag_counts = Hash.new(0)
148
+ @tag_ratios = {}
149
+ count_tags
150
+ calculate_ratios
151
+ end
152
+
153
+ def detect
154
+ archetype = detect_archetype
155
+ color_label = resolve_color_label(colors)
156
+ tribe_label = tribe ? " #{tribe} Tribal" : ""
157
+ archetype_label = " #{archetype.to_s.capitalize}"
158
+
159
+
160
+ [color_label, tribe_label, archetype_label].join.strip.squeeze(" ")
161
+ end
162
+
163
+ private
164
+
165
+ # === TAG COUNTING ===
166
+
167
+ def count_tags
168
+ total_tags = 0
169
+
170
+ deck.each do |card|
171
+ next unless card.respond_to?(:tags)
172
+
173
+ card.tags.each do |tag|
174
+ @tag_counts[tag] += card.count
175
+ total_tags += card.count
176
+ end
177
+ end
178
+
179
+ @tag_counts[:_total] = total_tags
180
+ end
181
+
182
+ def calculate_ratios
183
+ total = @tag_counts[:_total].to_f
184
+ return if total.zero?
185
+
186
+ @tag_counts.each do |tag, count|
187
+ next if tag == :_total
188
+ @tag_ratios[tag] = count / total
189
+ end
190
+ end
191
+
192
+ # === CMC UTILITY ===
193
+
194
+ def average_cmc
195
+ @average_cmc ||= begin
196
+ cmcs = deck.main.map { |card| card.respond_to?(:cmc) ? card.cmc.to_f : nil }.compact
197
+ return 0 if cmcs.empty? # rubocop:disable Lint/NoReturnInBeginEndBlocks
198
+ cmcs.sum / cmcs.size
199
+ end
200
+ end
201
+
202
+ # === FORMAT-AWARE DETECTION ===
203
+ def detect_archetype
204
+ if format == "edh"
205
+ return :combo if tag_ratios[:combo_piece].to_f > 0.10 && tag_ratios[:draw].to_f > 0.08
206
+
207
+ control_indicators = tag_counts[:interaction].to_i +
208
+ tag_counts[:draw].to_i +
209
+ tag_counts[:tax].to_i +
210
+ tag_counts[:lock_piece].to_i
211
+
212
+ return :control if control_indicators >= 14 && tag_counts[:threat].to_i <= 6
213
+
214
+ return :ramp if tag_counts[:ramp] >= 10 && average_cmc >= 4.0
215
+ return :midrange if tag_counts[:threat] >= 15 && tag_counts[:interaction] >= 6
216
+
217
+ return :aggro if tag_ratios[:threat].to_f >= 0.2 ||
218
+ tag_counts[:synergistic_finisher].to_i >= 3 ||
219
+ (tag_counts[:scaling_threat].to_i >= 6 && average_cmc <= 3.5) ||
220
+ (tag_counts[:synergy].to_i >= 6 && average_cmc <= 3.0)
221
+ else
222
+ return :combo if tag_ratios[:combo_piece].to_f >= 0.12
223
+ return :control if tag_ratios[:interaction].to_f >= 0.25 &&
224
+ tag_ratios[:threat].to_f < 0.2 &&
225
+ (tag_ratios[:draw].to_f + tag_ratios[:tax].to_f + tag_ratios[:lock_piece].to_f) >= 0.15
226
+
227
+ return :aggro if tag_ratios[:threat].to_f >= 0.2 ||
228
+ tag_counts[:synergistic_finisher].to_i >= 3 ||
229
+ (tag_counts[:scaling_threat].to_i >= 6 && average_cmc <= 3.5) ||
230
+ (tag_counts[:synergy].to_i >= 6 && average_cmc <= 3.0)
231
+
232
+ return :ramp if tag_ratios[:ramp].to_f >= 0.12 && tag_counts[:finisher] >= 3 && average_cmc >= 4.0
233
+ return :tempo if tag_ratios[:interaction].to_f >= 0.2 && tag_ratios[:threat].to_f >= 0.2 && average_cmc <= 3.5
234
+ return :midrange if tag_ratios[:threat].to_f >= 0.2 && tag_ratios[:interaction].to_f >= 0.1
235
+ end
236
+
237
+ :unknown
238
+ end
239
+
240
+ # === COLOR IDENTITY ===
241
+
242
+ def resolve_color_identity(deck)
243
+ deck.main.flat_map { |c| c.respond_to?(:color_identity) ? c.color_identity : [] }.uniq.sort
244
+ end
245
+
246
+ def resolve_color_label(identity)
247
+ ColorIdentityResolver.resolve(identity)
248
+ end
249
+ end
250
+
135
251
  class ColorIdentityResolver
136
252
  COLOR_NAMES = {
137
253
  %w[W] => "White",
@@ -182,6 +298,10 @@ module CwCardUtils
182
298
  class DecklistParser
183
299
  attr_reader :deck
184
300
 
301
+ IGNORED_TRIBAL_TYPES = %w[
302
+ human wizard soldier scout shaman warrior cleric rogue advisor knight druid
303
+ ].freeze
304
+
185
305
  def initialize(desklist, cmc_data_source = ScryfallCmcData.new)
186
306
  @decklist = desklist.is_a?(IO) ? desklist.read : desklist
187
307
  @deck = Deck.new(cmc_data_source)
@@ -288,8 +408,6 @@ module CwCardUtils
288
408
  @x_to_cast = []
289
409
 
290
410
  @cmc_data_source = cmc_data_source
291
-
292
- @curve_calculator = CurveCalculator.new(self)
293
411
  end
294
412
 
295
413
  def to_h
@@ -327,24 +445,28 @@ module CwCardUtils
327
445
  end
328
446
  end
329
447
 
448
+ def archetype
449
+ @archetype ||= ArchetypeDetector.new(self).detect
450
+ end
451
+
330
452
  def inspect
331
453
  "<Deck: main: #{mainboard_size} sideboard: #{sideboard_size} lands: #{lands_count} x_to_cast: #{x_to_cast_count} cards: #{cards_count}>"
332
454
  end
333
455
 
334
456
  def collapsed_curve
335
- @curve_calculator.collapsed_curve
457
+ @collapsed_curve ||= CurveCalculator.new(self).collapsed_curve
336
458
  end
337
459
 
338
460
  def curve
339
- @curve_calculator.curve
461
+ @curve ||=CurveCalculator.new(self).curve
340
462
  end
341
463
 
342
464
  def normalized_curve
343
- @curve_calculator.normalized_curve
465
+ @normalized_curve ||=CurveCalculator.new(self).normalized_curve
344
466
  end
345
467
 
346
468
  def collapsed_normalized_curve
347
- @curve_calculator.collapsed_normalized_curve
469
+ @collapsed_normalized_curve ||=CurveCalculator.new(self).collapsed_normalized_curve
348
470
  end
349
471
 
350
472
  def empty?
@@ -387,6 +509,20 @@ module CwCardUtils
387
509
  @cards_count = nil
388
510
  end
389
511
 
512
+ def format
513
+ @format ||= detect_format_for_deck
514
+ end
515
+
516
+ def detect_format_for_deck
517
+ if mainboard_size >= 100
518
+ :commander
519
+ elsif mainboard_size >= 60
520
+ :standard
521
+ else
522
+ :modern
523
+ end
524
+ end
525
+
390
526
  def mainboard_size
391
527
  @mainboard_size ||= main.sum { |card| card.count }
392
528
  end
@@ -428,21 +564,29 @@ module CwCardUtils
428
564
  next unless card.type&.include?("Creature")
429
565
 
430
566
  total_creatures += 1
431
- subtypes = card.type.split("—").last.to_s.strip.split
567
+ subtypes = card.type.split(/[—-]/).last.to_s.strip.split
432
568
  subtypes.each do |type|
433
- subtype_counts[type] += 1
569
+ subtype_counts[type.downcase] += 1
434
570
  end
435
571
  end
436
572
 
437
- return nil if total_creatures.zero?
573
+ return nil if total_creatures < 6 || subtype_counts.empty?
438
574
 
439
575
  most_common = subtype_counts.max_by { |_, count| count }
440
- if most_common && most_common[1] > (total_creatures / 2.0)
441
- most_common[0].downcase.to_sym
442
- else
443
- nil
576
+ return nil if most_common.nil?
577
+
578
+ dominant_type = most_common[0]
579
+ count = most_common[1]
580
+
581
+ # Suppress only if it's a boring type *and* barely dominant
582
+ if IGNORED_TRIBAL_TYPES.include?(dominant_type) && count.to_f / total_creatures < 0.7
583
+ return nil
444
584
  end
585
+
586
+ return dominant_type.to_sym if count.to_f / total_creatures >= 0.4
587
+ nil
445
588
  end
589
+
446
590
  end
447
591
  end
448
592
  end
@@ -5,9 +5,9 @@ require "json"
5
5
  module CwCardUtils
6
6
  # Represents a card with Scryfall data
7
7
  class ScryfallCard
8
- def initialize(name)
8
+ def initialize(name, data_source = ScryfallCmcData.new)
9
9
  @name = name
10
- @data = ScryfallCmcData.new.find_card(@name) || {}
10
+ @data = data_source.find_card(@name) || {}
11
11
  end
12
12
 
13
13
  def cmc
@@ -43,6 +43,7 @@ module CwCardUtils
43
43
  class ScryfallCmcData
44
44
  def initialize
45
45
  @data = JSON.parse(File.read(File.expand_path("../../data/scryfall.cards.cmc.json", __dir__)))
46
+ @found_cards = {}
46
47
  end
47
48
 
48
49
  def find_card(name)
@@ -52,43 +53,50 @@ module CwCardUtils
52
53
  end
53
54
 
54
55
  def cmc_for_card(name)
55
- @data.find { |card| card["name"] == name }["cmc"]
56
+ @found_cards[name] ||= find_card(name)
57
+ @found_cards[name]["cmc"]
56
58
  rescue StandardError
57
59
  nil
58
60
  end
59
61
 
60
62
  def color_identity_for_card(name)
61
- @data.find { |card| card["name"] == name }["color_identity"]
63
+ @found_cards[name] ||= find_card(name)
64
+ @found_cards[name]["color_identity"]
62
65
  rescue StandardError
63
66
  nil
64
67
  end
65
68
 
66
69
  def keywords_for_card(name)
67
- @data.find { |card| card["name"] == name }["keywords"]
70
+ @found_cards[name] ||= find_card(name)
71
+ @found_cards[name]["keywords"]
68
72
  rescue StandardError
69
73
  nil
70
74
  end
71
75
 
72
76
  def toughness_for_card(name)
73
- @data.find { |card| card["name"] == name }["toughness"]
77
+ @found_cards[name] ||= find_card(name)
78
+ @found_cards[name]["toughness"]
74
79
  rescue StandardError
75
80
  nil
76
81
  end
77
82
 
78
83
  def power_for_card(name)
79
- @data.find { |card| card["name"] == name }["power"]
84
+ @found_cards[name] ||= find_card(name)
85
+ @found_cards[name]["power"]
80
86
  rescue StandardError
81
87
  nil
82
88
  end
83
89
 
84
90
  def oracle_text_for_card(name)
85
- @data.find { |card| card["name"] == name }["oracle_text"]
91
+ @found_cards[name] ||= find_card(name)
92
+ @found_cards[name]["oracle_text"]
86
93
  rescue StandardError
87
94
  nil
88
95
  end
89
96
 
90
97
  def type_for_card(name)
91
- type = @data.find { |card| card["name"] == name }["type_line"]
98
+ @found_cards[name] ||= find_card(name)
99
+ type = @found_cards[name]["type_line"]
92
100
  if type.include?("Land")
93
101
  type = nil
94
102
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module CwCardUtils
4
- VERSION = "0.1.2"
4
+ VERSION = "0.1.6"
5
5
  end
data/lib/cw_card_utils.rb CHANGED
@@ -4,6 +4,7 @@ require_relative "cw_card_utils/version"
4
4
  require_relative "cw_card_utils/curve_calculator"
5
5
  require_relative "cw_card_utils/decklist_parser"
6
6
  require_relative "cw_card_utils/scryfall_cmc_data"
7
+ require_relative "cw_card_utils/deck_comparator"
7
8
 
8
9
  module CwCardUtils
9
10
  class Error < StandardError; end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cw_card_utils
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.2
4
+ version: 0.1.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ryan Stenhouse
@@ -38,6 +38,7 @@ files:
38
38
  - data/scryfall.cards.cmc.json
39
39
  - lib/cw_card_utils.rb
40
40
  - lib/cw_card_utils/curve_calculator.rb
41
+ - lib/cw_card_utils/deck_comparator.rb
41
42
  - lib/cw_card_utils/decklist_parser.rb
42
43
  - lib/cw_card_utils/scryfall_cmc_data.rb
43
44
  - lib/cw_card_utils/version.rb