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.
@@ -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,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CwCardUtils
4
+ VERSION = "0.1.0"
5
+ 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
@@ -0,0 +1,4 @@
1
+ module CwCardUtils
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ 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: []