cw_card_utils 0.1.0
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 +7 -0
- data/.rspec +3 -0
- data/.rubocop.yml +8 -0
- data/CODE_OF_CONDUCT.md +132 -0
- data/LICENSE.txt +21 -0
- data/README.md +43 -0
- data/Rakefile +12 -0
- data/data/scryfall.cards.cmc.json +698932 -0
- data/lib/cw_card_utils/curve_calculator.rb +51 -0
- data/lib/cw_card_utils/decklist_parser.rb +152 -0
- data/lib/cw_card_utils/scryfall_cmc_data.rb +29 -0
- data/lib/cw_card_utils/version.rb +5 -0
- data/lib/cw_card_utils.rb +10 -0
- data/sig/cw_card_utils.rbs +4 -0
- metadata +69 -0
@@ -0,0 +1,51 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CwCardUtils
|
4
|
+
class CurveCalculator
|
5
|
+
def initialize(deck)
|
6
|
+
@deck = deck
|
7
|
+
@deck_size = deck.size
|
8
|
+
@raw_curve = {}
|
9
|
+
@normalized_curve = {}
|
10
|
+
end
|
11
|
+
|
12
|
+
def curve
|
13
|
+
calculate_curve if @raw_curve.empty?
|
14
|
+
@raw_curve
|
15
|
+
end
|
16
|
+
|
17
|
+
def normalized_curve
|
18
|
+
normalize_curve if @normalized_curve.empty?
|
19
|
+
@normalized_curve
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def calculate_curve
|
25
|
+
@deck.each do |card|
|
26
|
+
cmc = card.cmc
|
27
|
+
|
28
|
+
# If CMC is nil, and it is a land, skip it, otherwise add it to the curve in the
|
29
|
+
# zero cost bucket. (Handles X to cast cards.)
|
30
|
+
if cmc.nil? || cmc == 0
|
31
|
+
if card.type.include?("Land")
|
32
|
+
next
|
33
|
+
else
|
34
|
+
bucket = 0
|
35
|
+
end
|
36
|
+
else
|
37
|
+
bucket = cmc.ceil
|
38
|
+
end
|
39
|
+
|
40
|
+
@raw_curve[bucket] ||= 0
|
41
|
+
@raw_curve[bucket] += card.count
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def normalize_curve
|
46
|
+
@normalized_curve = curve.sort_by { |cmc, _| cmc }.to_h.transform_values do |count|
|
47
|
+
(count / @deck_size.to_f).round(4)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,152 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CwCardUtils
|
4
|
+
# Parses a decklist and returns a Deck object.
|
5
|
+
class DecklistParser
|
6
|
+
attr_reader :deck
|
7
|
+
|
8
|
+
def initialize(desklist, cmc_data_source = ScryfallCmcData.new)
|
9
|
+
@decklist = desklist.is_a?(IO) ? desklist.read : desklist
|
10
|
+
@deck = Deck.new(cmc_data_source)
|
11
|
+
end
|
12
|
+
|
13
|
+
def inspect
|
14
|
+
"<DecklistParser: #{@decklist.length}>"
|
15
|
+
end
|
16
|
+
|
17
|
+
|
18
|
+
# Parses the decklist and returns a Deck object.
|
19
|
+
def parse
|
20
|
+
return @deck if @deck.any?
|
21
|
+
|
22
|
+
sideboard = false
|
23
|
+
|
24
|
+
@decklist.each_line do |line|
|
25
|
+
line = line.strip
|
26
|
+
next if line.empty?
|
27
|
+
|
28
|
+
sideboard = true if line.downcase.start_with?("sideboard") && (sideboard == false)
|
29
|
+
|
30
|
+
if line.match?(/^(Deck|Sideboard|About|Commander|Creatures|Lands|Spells|Artifacts|Enchantments|Planeswalkers|Mainboard|Maybeboard|Companion)/i)
|
31
|
+
next
|
32
|
+
end
|
33
|
+
next if line.start_with?("#") # skip comment-style lines
|
34
|
+
next if line.start_with?("//") # skip comment-style lines
|
35
|
+
next if line.start_with?("Name") # skip deck name
|
36
|
+
|
37
|
+
# Match patterns like: "4 Lightning Bolt", "2 Sol Ring (CMM) 452", "1 Atraxa, Praetors' Voice"
|
38
|
+
next unless match = line.match(/^(\d+)\s+(.+?)(?:\s+\(.*?\)\s+\d+)?$/)
|
39
|
+
|
40
|
+
count = match[1].to_i
|
41
|
+
name = match[2].strip
|
42
|
+
target = sideboard ? :sideboard : :mainboard
|
43
|
+
@deck.add({ count: count, name: name }, target)
|
44
|
+
end
|
45
|
+
|
46
|
+
@deck
|
47
|
+
end
|
48
|
+
|
49
|
+
# A Deck is a collection of cards.
|
50
|
+
class Deck
|
51
|
+
# A Card is a single card in a deck.
|
52
|
+
class Card
|
53
|
+
def initialize(name, count, cmc_data_source)
|
54
|
+
@name = name
|
55
|
+
@count = count
|
56
|
+
@cmc_data_source = cmc_data_source
|
57
|
+
end
|
58
|
+
|
59
|
+
attr_reader :name, :count
|
60
|
+
|
61
|
+
def cmc
|
62
|
+
@cmc ||= @cmc_data_source.cmc_for_card(@name)
|
63
|
+
end
|
64
|
+
|
65
|
+
def type
|
66
|
+
@type ||= @cmc_data_source.type_for_card(@name) || "Land"
|
67
|
+
end
|
68
|
+
|
69
|
+
def inspect
|
70
|
+
"<Card: #{@name} (#{@count}) #{cmc}>"
|
71
|
+
end
|
72
|
+
|
73
|
+
def to_h
|
74
|
+
{ name: @name, count: @count, cmc: cmc, type: type }
|
75
|
+
end
|
76
|
+
|
77
|
+
def to_json
|
78
|
+
to_h.to_json
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def initialize(cmc_data_source)
|
83
|
+
@main = []
|
84
|
+
@sideboard = []
|
85
|
+
@cmc_data_source = cmc_data_source
|
86
|
+
end
|
87
|
+
|
88
|
+
def to_h
|
89
|
+
{
|
90
|
+
mainboard: @main.map { |c| Card.new(c[:name], c[:count], @cmc_data_source).to_h },
|
91
|
+
sideboard: @sideboard.map { |c| Card.new(c[:name], c[:count], @cmc_data_source).to_h }
|
92
|
+
}
|
93
|
+
end
|
94
|
+
|
95
|
+
def to_json
|
96
|
+
to_h.to_json
|
97
|
+
end
|
98
|
+
|
99
|
+
def inspect
|
100
|
+
"<Deck: main: #{@main.size} sideboard: #{@sideboard.size}>"
|
101
|
+
end
|
102
|
+
|
103
|
+
def empty?
|
104
|
+
@main.empty? && @sideboard.empty?
|
105
|
+
end
|
106
|
+
|
107
|
+
def any?
|
108
|
+
!empty?
|
109
|
+
end
|
110
|
+
|
111
|
+
def each(&block)
|
112
|
+
@main.each do |c|
|
113
|
+
c = Card.new(c[:name], c[:count], @cmc_data_source)
|
114
|
+
block.call(c)
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
attr_reader :main, :sideboard
|
119
|
+
|
120
|
+
def add(card, target = :mainboard)
|
121
|
+
reset_counters
|
122
|
+
if target == :mainboard
|
123
|
+
@main << card
|
124
|
+
else
|
125
|
+
@sideboard << card
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
def reset_counters
|
130
|
+
@mainboard_size = nil
|
131
|
+
@sideboard_size = nil
|
132
|
+
@cards_count = nil
|
133
|
+
end
|
134
|
+
|
135
|
+
def mainboard_size
|
136
|
+
@mainboard_size ||= main.sum { |card| card[:count] }
|
137
|
+
end
|
138
|
+
|
139
|
+
def sideboard_size
|
140
|
+
@sideboard_size ||= sideboard.sum { |card| card[:count] }
|
141
|
+
end
|
142
|
+
|
143
|
+
def cards_count
|
144
|
+
@cards_count ||= mainboard_size + sideboard_size
|
145
|
+
end
|
146
|
+
|
147
|
+
def size
|
148
|
+
cards_count
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "json"
|
4
|
+
|
5
|
+
module CwCardUtils
|
6
|
+
# Handles conversion mana cost (CMC) data for Magic: The Gathering cards
|
7
|
+
class ScryfallCmcData
|
8
|
+
def initialize
|
9
|
+
@raw_data = File.read(File.expand_path("../../data/scryfall.cards.cmc.json", __dir__))
|
10
|
+
@data = JSON.parse(@raw_data)
|
11
|
+
end
|
12
|
+
|
13
|
+
def cmc_for_card(name)
|
14
|
+
@data.find { |card| card["name"] == name }["cmc"]
|
15
|
+
rescue StandardError
|
16
|
+
nil
|
17
|
+
end
|
18
|
+
|
19
|
+
def type_for_card(name)
|
20
|
+
@data.find { |card| card["name"] == name }["type_line"]
|
21
|
+
rescue StandardError
|
22
|
+
nil
|
23
|
+
end
|
24
|
+
|
25
|
+
def cmc_data
|
26
|
+
@data
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "cw_card_utils/version"
|
4
|
+
require_relative "cw_card_utils/curve_calculator"
|
5
|
+
require_relative "cw_card_utils/decklist_parser"
|
6
|
+
require_relative "cw_card_utils/scryfall_cmc_data"
|
7
|
+
|
8
|
+
module CwCardUtils
|
9
|
+
class Error < StandardError; end
|
10
|
+
end
|
metadata
ADDED
@@ -0,0 +1,69 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: cw_card_utils
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Ryan Stenhouse
|
8
|
+
bindir: exe
|
9
|
+
cert_chain: []
|
10
|
+
date: 2025-07-26 00:00:00.000000000 Z
|
11
|
+
dependencies:
|
12
|
+
- !ruby/object:Gem::Dependency
|
13
|
+
name: json
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
15
|
+
requirements:
|
16
|
+
- - ">="
|
17
|
+
- !ruby/object:Gem::Version
|
18
|
+
version: '0'
|
19
|
+
type: :runtime
|
20
|
+
prerelease: false
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
22
|
+
requirements:
|
23
|
+
- - ">="
|
24
|
+
- !ruby/object:Gem::Version
|
25
|
+
version: '0'
|
26
|
+
email:
|
27
|
+
- hello@ryanstenhouse.dev
|
28
|
+
executables: []
|
29
|
+
extensions: []
|
30
|
+
extra_rdoc_files: []
|
31
|
+
files:
|
32
|
+
- ".rspec"
|
33
|
+
- ".rubocop.yml"
|
34
|
+
- CODE_OF_CONDUCT.md
|
35
|
+
- LICENSE.txt
|
36
|
+
- README.md
|
37
|
+
- Rakefile
|
38
|
+
- data/scryfall.cards.cmc.json
|
39
|
+
- lib/cw_card_utils.rb
|
40
|
+
- lib/cw_card_utils/curve_calculator.rb
|
41
|
+
- lib/cw_card_utils/decklist_parser.rb
|
42
|
+
- lib/cw_card_utils/scryfall_cmc_data.rb
|
43
|
+
- lib/cw_card_utils/version.rb
|
44
|
+
- sig/cw_card_utils.rbs
|
45
|
+
homepage: https://cracklingwit.com
|
46
|
+
licenses:
|
47
|
+
- MIT
|
48
|
+
metadata:
|
49
|
+
homepage_uri: https://cracklingwit.com
|
50
|
+
source_code_uri: https://github.com/cracklingwit/card_utils
|
51
|
+
changelog_uri: https://github.com/cracklingwit/card_utils/commits/main/
|
52
|
+
rdoc_options: []
|
53
|
+
require_paths:
|
54
|
+
- lib
|
55
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
56
|
+
requirements:
|
57
|
+
- - ">="
|
58
|
+
- !ruby/object:Gem::Version
|
59
|
+
version: 3.1.0
|
60
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
61
|
+
requirements:
|
62
|
+
- - ">="
|
63
|
+
- !ruby/object:Gem::Version
|
64
|
+
version: '0'
|
65
|
+
requirements: []
|
66
|
+
rubygems_version: 3.6.4
|
67
|
+
specification_version: 4
|
68
|
+
summary: 'Crackling Wit Card Utils - For working with Magic: The Gathering cards.'
|
69
|
+
test_files: []
|