cw_card_utils 0.1.6 → 0.1.8
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/lib/cw_card_utils/deck_comparator.rb +113 -22
- data/lib/cw_card_utils/decklist_parser/archetype_detector.rb +117 -0
- data/lib/cw_card_utils/decklist_parser/card.rb +60 -0
- data/lib/cw_card_utils/decklist_parser/card_tagger.rb +141 -0
- data/lib/cw_card_utils/decklist_parser/color_identity_resolver.rb +51 -0
- data/lib/cw_card_utils/decklist_parser/deck.rb +223 -0
- data/lib/cw_card_utils/decklist_parser/parser.rb +51 -0
- data/lib/cw_card_utils/decklist_parser.rb +6 -590
- data/lib/cw_card_utils/scryfall_cmc_data.rb +98 -51
- data/lib/cw_card_utils/synergy_probability.rb +103 -0
- data/lib/cw_card_utils/version.rb +1 -1
- data/lib/cw_card_utils.rb +1 -0
- metadata +8 -1
@@ -1,11 +1,85 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require "json"
|
4
|
+
require "singleton"
|
4
5
|
|
5
6
|
module CwCardUtils
|
7
|
+
# Abstract base class for card data sources
|
8
|
+
class CardDataSource
|
9
|
+
def find_card(name)
|
10
|
+
raise NotImplementedError, "Subclasses must implement find_card"
|
11
|
+
end
|
12
|
+
|
13
|
+
def cmc_for_card(name)
|
14
|
+
card = find_card(name)
|
15
|
+
card&.dig("cmc")
|
16
|
+
rescue StandardError
|
17
|
+
nil
|
18
|
+
end
|
19
|
+
|
20
|
+
def color_identity_for_card(name)
|
21
|
+
card = find_card(name)
|
22
|
+
card&.dig("color_identity") || []
|
23
|
+
rescue StandardError
|
24
|
+
[]
|
25
|
+
end
|
26
|
+
|
27
|
+
def keywords_for_card(name)
|
28
|
+
card = find_card(name)
|
29
|
+
card&.dig("keywords") || []
|
30
|
+
rescue StandardError
|
31
|
+
[]
|
32
|
+
end
|
33
|
+
|
34
|
+
def toughness_for_card(name)
|
35
|
+
card = find_card(name)
|
36
|
+
card&.dig("toughness")
|
37
|
+
rescue StandardError
|
38
|
+
nil
|
39
|
+
end
|
40
|
+
|
41
|
+
def power_for_card(name)
|
42
|
+
card = find_card(name)
|
43
|
+
card&.dig("power")
|
44
|
+
rescue StandardError
|
45
|
+
nil
|
46
|
+
end
|
47
|
+
|
48
|
+
def oracle_text_for_card(name)
|
49
|
+
card = find_card(name)
|
50
|
+
card&.dig("oracle_text")
|
51
|
+
rescue StandardError
|
52
|
+
nil
|
53
|
+
end
|
54
|
+
|
55
|
+
def type_for_card(name)
|
56
|
+
card = find_card(name)
|
57
|
+
type = card&.dig("type_line")
|
58
|
+
return nil if type&.include?("Land")
|
59
|
+
type
|
60
|
+
rescue StandardError
|
61
|
+
nil
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
# Example MongoDB data source implementation
|
66
|
+
# Users can implement their own data sources by inheriting from CardDataSource
|
67
|
+
class MongoCardDataSource < CardDataSource
|
68
|
+
def initialize(collection)
|
69
|
+
super()
|
70
|
+
@collection = collection
|
71
|
+
end
|
72
|
+
|
73
|
+
def find_card(name)
|
74
|
+
@collection.find_one({ "name" => name })
|
75
|
+
rescue StandardError
|
76
|
+
nil
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
6
80
|
# Represents a card with Scryfall data
|
7
81
|
class ScryfallCard
|
8
|
-
def initialize(name, data_source = ScryfallCmcData.
|
82
|
+
def initialize(name, data_source = ScryfallCmcData.instance)
|
9
83
|
@name = name
|
10
84
|
@data = data_source.find_card(@name) || {}
|
11
85
|
end
|
@@ -40,74 +114,47 @@ module CwCardUtils
|
|
40
114
|
end
|
41
115
|
|
42
116
|
# Handles conversion mana cost (CMC) data for Magic: The Gathering cards
|
43
|
-
|
117
|
+
# Uses Singleton pattern to avoid loading the large JSON file multiple times
|
118
|
+
class ScryfallCmcData < CardDataSource
|
119
|
+
include Singleton
|
120
|
+
|
44
121
|
def initialize
|
45
|
-
|
122
|
+
super
|
123
|
+
@data = nil
|
124
|
+
@card_index = nil
|
46
125
|
@found_cards = {}
|
47
126
|
end
|
48
127
|
|
49
128
|
def find_card(name)
|
50
|
-
|
129
|
+
load_data_if_needed
|
130
|
+
@card_index[name] || @found_cards[name] ||= linear_search(name)
|
51
131
|
rescue StandardError
|
52
132
|
nil
|
53
133
|
end
|
54
134
|
|
55
|
-
def
|
56
|
-
|
57
|
-
@
|
58
|
-
rescue StandardError
|
59
|
-
nil
|
60
|
-
end
|
61
|
-
|
62
|
-
def color_identity_for_card(name)
|
63
|
-
@found_cards[name] ||= find_card(name)
|
64
|
-
@found_cards[name]["color_identity"]
|
65
|
-
rescue StandardError
|
66
|
-
nil
|
67
|
-
end
|
68
|
-
|
69
|
-
def keywords_for_card(name)
|
70
|
-
@found_cards[name] ||= find_card(name)
|
71
|
-
@found_cards[name]["keywords"]
|
72
|
-
rescue StandardError
|
73
|
-
nil
|
135
|
+
def cmc_data
|
136
|
+
load_data_if_needed
|
137
|
+
@data
|
74
138
|
end
|
75
139
|
|
76
|
-
|
77
|
-
@found_cards[name] ||= find_card(name)
|
78
|
-
@found_cards[name]["toughness"]
|
79
|
-
rescue StandardError
|
80
|
-
nil
|
81
|
-
end
|
140
|
+
private
|
82
141
|
|
83
|
-
def
|
84
|
-
|
85
|
-
@found_cards[name]["power"]
|
86
|
-
rescue StandardError
|
87
|
-
nil
|
88
|
-
end
|
142
|
+
def load_data_if_needed
|
143
|
+
return if @data
|
89
144
|
|
90
|
-
|
91
|
-
|
92
|
-
@found_cards[name]["oracle_text"]
|
93
|
-
rescue StandardError
|
94
|
-
nil
|
145
|
+
@data = JSON.parse(File.read(File.expand_path("../../data/scryfall.cards.cmc.json", __dir__)))
|
146
|
+
build_card_index
|
95
147
|
end
|
96
148
|
|
97
|
-
def
|
98
|
-
@
|
99
|
-
|
100
|
-
|
101
|
-
type = nil
|
149
|
+
def build_card_index
|
150
|
+
@card_index = {}
|
151
|
+
@data.each do |card|
|
152
|
+
@card_index[card["name"]] = card
|
102
153
|
end
|
103
|
-
|
104
|
-
type
|
105
|
-
rescue StandardError
|
106
|
-
nil
|
107
154
|
end
|
108
155
|
|
109
|
-
def
|
110
|
-
@data
|
156
|
+
def linear_search(name)
|
157
|
+
@data.find { |card| card["name"] == name }
|
111
158
|
end
|
112
159
|
end
|
113
160
|
end
|
@@ -0,0 +1,103 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CwCardUtils
|
4
|
+
# Calculates probability of drawing specific card combinations for synergy analysis
|
5
|
+
class SynergyProbability
|
6
|
+
def initialize(deck, deck_size: 60)
|
7
|
+
@deck = deck
|
8
|
+
@deck_size = deck_size
|
9
|
+
end
|
10
|
+
|
11
|
+
# Probability of drawing at least one of the target cards
|
12
|
+
def prob_single(target_names, draws)
|
13
|
+
total_copies = count_copies(target_names)
|
14
|
+
1 - hypergeometric(@deck_size - total_copies, draws).to_f / hypergeometric(@deck_size, draws)
|
15
|
+
end
|
16
|
+
|
17
|
+
# Probability of drawing ALL cards in the targets list (synergy pair/trio)
|
18
|
+
def prob_combo(target_names, draws)
|
19
|
+
case target_names.size
|
20
|
+
when 1
|
21
|
+
prob_single(target_names, draws)
|
22
|
+
when 2
|
23
|
+
prob_two_card_combo(target_names, draws)
|
24
|
+
when 3
|
25
|
+
prob_three_card_combo(target_names, draws)
|
26
|
+
else
|
27
|
+
# For >3 cards, fallback to approximation
|
28
|
+
approx_combo(target_names, draws)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
# Exact for 2-card combos
|
35
|
+
def prob_two_card_combo(names, draws)
|
36
|
+
copies_a = count_copies([names[0]])
|
37
|
+
copies_b = count_copies([names[1]])
|
38
|
+
|
39
|
+
total = hypergeometric(@deck_size, draws).to_f
|
40
|
+
|
41
|
+
# Probability missing A
|
42
|
+
miss_a = hypergeometric(@deck_size - copies_a, draws) / total
|
43
|
+
# Probability missing B
|
44
|
+
miss_b = hypergeometric(@deck_size - copies_b, draws) / total
|
45
|
+
# Probability missing both
|
46
|
+
miss_both = hypergeometric(@deck_size - (copies_a + copies_b), draws) / total
|
47
|
+
|
48
|
+
# Inclusion–exclusion
|
49
|
+
[1 - (miss_a + miss_b - miss_both), 0].max
|
50
|
+
end
|
51
|
+
|
52
|
+
# Exact for 3-card combos
|
53
|
+
def prob_three_card_combo(names, draws)
|
54
|
+
copies_a = count_copies([names[0]])
|
55
|
+
copies_b = count_copies([names[1]])
|
56
|
+
copies_c = count_copies([names[2]])
|
57
|
+
|
58
|
+
total = hypergeometric(@deck_size, draws).to_f
|
59
|
+
|
60
|
+
miss_a = hypergeometric(@deck_size - copies_a, draws) / total
|
61
|
+
miss_b = hypergeometric(@deck_size - copies_b, draws) / total
|
62
|
+
miss_c = hypergeometric(@deck_size - copies_c, draws) / total
|
63
|
+
|
64
|
+
miss_ab = hypergeometric(@deck_size - (copies_a + copies_b), draws) / total
|
65
|
+
miss_ac = hypergeometric(@deck_size - (copies_a + copies_c), draws) / total
|
66
|
+
miss_bc = hypergeometric(@deck_size - (copies_b + copies_c), draws) / total
|
67
|
+
|
68
|
+
miss_abc = hypergeometric(@deck_size - (copies_a + copies_b + copies_c), draws) / total
|
69
|
+
|
70
|
+
# Inclusion–exclusion for 3 sets
|
71
|
+
[1 - (miss_a + miss_b + miss_c) +
|
72
|
+
(miss_ab + miss_ac + miss_bc) -
|
73
|
+
miss_abc, 0].max
|
74
|
+
end
|
75
|
+
|
76
|
+
# Approximation for >3 cards (same as old method)
|
77
|
+
def approx_combo(target_names, draws)
|
78
|
+
prob_missing = 0.0
|
79
|
+
target_names.each do |name|
|
80
|
+
copies = count_copies([name])
|
81
|
+
miss_prob = hypergeometric(@deck_size - copies, draws).to_f / hypergeometric(@deck_size, draws)
|
82
|
+
prob_missing += miss_prob
|
83
|
+
end
|
84
|
+
[1 - prob_missing, 0].max
|
85
|
+
end
|
86
|
+
|
87
|
+
# Utility: count how many copies of given cards are in the deck
|
88
|
+
def count_copies(names)
|
89
|
+
@deck.main.sum { |card| names.include?(card.name) ? card.count : 0 }
|
90
|
+
end
|
91
|
+
|
92
|
+
# Hypergeometric combination helper
|
93
|
+
def hypergeometric(n, k)
|
94
|
+
return 0 if k > n
|
95
|
+
factorial(n) / (factorial(k) * factorial(n - k))
|
96
|
+
end
|
97
|
+
|
98
|
+
def factorial(n)
|
99
|
+
return 1 if n.zero?
|
100
|
+
(1..n).reduce(1, :*)
|
101
|
+
end
|
102
|
+
end
|
103
|
+
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/synergy_probability"
|
7
8
|
require_relative "cw_card_utils/deck_comparator"
|
8
9
|
|
9
10
|
module CwCardUtils
|
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.
|
4
|
+
version: 0.1.8
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Ryan Stenhouse
|
@@ -40,7 +40,14 @@ files:
|
|
40
40
|
- lib/cw_card_utils/curve_calculator.rb
|
41
41
|
- lib/cw_card_utils/deck_comparator.rb
|
42
42
|
- lib/cw_card_utils/decklist_parser.rb
|
43
|
+
- lib/cw_card_utils/decklist_parser/archetype_detector.rb
|
44
|
+
- lib/cw_card_utils/decklist_parser/card.rb
|
45
|
+
- lib/cw_card_utils/decklist_parser/card_tagger.rb
|
46
|
+
- lib/cw_card_utils/decklist_parser/color_identity_resolver.rb
|
47
|
+
- lib/cw_card_utils/decklist_parser/deck.rb
|
48
|
+
- lib/cw_card_utils/decklist_parser/parser.rb
|
43
49
|
- lib/cw_card_utils/scryfall_cmc_data.rb
|
50
|
+
- lib/cw_card_utils/synergy_probability.rb
|
44
51
|
- lib/cw_card_utils/version.rb
|
45
52
|
- sig/cw_card_utils.rbs
|
46
53
|
homepage: https://cracklingwit.com
|