cw_card_utils 0.1.9 → 0.1.11
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/README.md +52 -23
- data/lib/cw_card_utils/deck_comparator.rb +1 -1
- data/lib/cw_card_utils/decklist_parser/card.rb +2 -2
- data/lib/cw_card_utils/decklist_parser/parser.rb +2 -2
- data/lib/cw_card_utils/scryfall_cmc_data.rb +2 -2
- data/lib/cw_card_utils/synergy_probability.rb +60 -35
- data/lib/cw_card_utils/version.rb +1 -1
- data/lib/cw_card_utils.rb +13 -0
- metadata +1 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 23145f32617794cf1af83523bf179d6d9260cc75251a24207ae6ea6ce0274599
|
4
|
+
data.tar.gz: 7cd65b1d7e93b8f9c2438df235d77c44e294d543ff1f22e8b0aa16efb5e51192
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 556bc7d3c4dc4d87b752d2923eba061b23356a6e2330f33691e34ee215d7cdd9e6148cd8c4a9082ddbcda179ffd68bef162d0bc6e11c25db3bc05b95589f83d9
|
7
|
+
data.tar.gz: e81abacff1c5dae3476a1ffb9e6a779105546f7a020edcc36fb463082842645a784e922d89660c2e779c4ceaa1d51dadf14b84ed4ae33d6326fe7188ab1135b7
|
data/README.md
CHANGED
@@ -1,50 +1,79 @@
|
|
1
1
|
# CwCardUtils
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/cw_card_utils`. To experiment with that code, run `bin/console` for an interactive prompt.
|
3
|
+
A Ruby gem for analyzing Magic: The Gathering decklists and calculating various metrics.
|
6
4
|
|
7
5
|
## Installation
|
8
6
|
|
9
|
-
|
7
|
+
Add this line to your application's Gemfile:
|
8
|
+
|
9
|
+
```ruby
|
10
|
+
gem 'cw_card_utils'
|
11
|
+
```
|
10
12
|
|
11
|
-
|
13
|
+
And then execute:
|
12
14
|
|
13
15
|
```bash
|
14
|
-
bundle
|
16
|
+
bundle install
|
15
17
|
```
|
16
18
|
|
17
|
-
|
19
|
+
## Configuration
|
18
20
|
|
19
|
-
|
20
|
-
|
21
|
+
You can configure the card data source used by the library:
|
22
|
+
|
23
|
+
```ruby
|
24
|
+
# In a Rails initializer (config/initializers/cw_card_utils.rb)
|
25
|
+
CwCardUtils.configure do |config|
|
26
|
+
config.card_data_source = MyCustomDataSource.new
|
27
|
+
end
|
28
|
+
|
29
|
+
# Or set it directly
|
30
|
+
CwCardUtils.card_data_source = MyCustomDataSource.new
|
21
31
|
```
|
22
32
|
|
33
|
+
The default data source is `CwCardUtils::ScryfallCmcData.instance`, which loads card data from a local JSON file.
|
34
|
+
|
23
35
|
## Usage
|
24
36
|
|
25
|
-
|
37
|
+
### Basic Deck Parsing
|
26
38
|
|
27
|
-
|
39
|
+
```ruby
|
40
|
+
require 'cw_card_utils'
|
28
41
|
|
29
|
-
|
42
|
+
# Parse a decklist
|
43
|
+
decklist = <<~DECK
|
44
|
+
4 Lightning Bolt
|
45
|
+
4 Mountain
|
46
|
+
2 Shock
|
47
|
+
DECK
|
30
48
|
|
31
|
-
|
49
|
+
deck = CwCardUtils::DecklistParser::Parser.new(decklist).parse
|
32
50
|
|
33
|
-
|
51
|
+
# Access deck information
|
52
|
+
puts deck.mainboard_size # => 10
|
53
|
+
puts deck.color_identity # => ["R"]
|
54
|
+
puts deck.archetype # => :aggro
|
55
|
+
```
|
34
56
|
|
35
|
-
|
57
|
+
### Custom Data Sources
|
36
58
|
|
37
|
-
|
59
|
+
You can implement your own card data source by inheriting from `CwCardUtils::CardDataSource`:
|
38
60
|
|
39
|
-
|
61
|
+
```ruby
|
62
|
+
class MyCustomDataSource < CwCardUtils::CardDataSource
|
63
|
+
def find_card(name)
|
64
|
+
# Your implementation here
|
65
|
+
# Should return a hash with card data or nil
|
66
|
+
end
|
67
|
+
end
|
40
68
|
|
41
|
-
|
69
|
+
# Configure the library to use your data source
|
70
|
+
CwCardUtils.card_data_source = MyCustomDataSource.new
|
71
|
+
```
|
42
72
|
|
43
|
-
|
73
|
+
## Development
|
44
74
|
|
45
|
-
|
75
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
46
76
|
|
47
|
-
|
48
|
-
or real-life usage, this should use your own data store.
|
77
|
+
## Contributing
|
49
78
|
|
50
|
-
|
79
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/cracklingwit/cw_card_utils.
|
@@ -90,7 +90,7 @@ module CwCardUtils
|
|
90
90
|
|
91
91
|
def extract_synergy_pairs(deck)
|
92
92
|
synergy_cards = deck.main.select do |card|
|
93
|
-
|
93
|
+
card.tags.intersect?([:synergistic_finisher, :tribal_synergy, :scaling_threat])
|
94
94
|
end
|
95
95
|
return [] if synergy_cards.size < 2
|
96
96
|
synergy_cards.map(&:name).combination(2).to_a
|
@@ -4,11 +4,11 @@ module CwCardUtils
|
|
4
4
|
module DecklistParser
|
5
5
|
# A Card is a single card in a deck.
|
6
6
|
class Card
|
7
|
-
def initialize(name, count, cmc_data_source =
|
7
|
+
def initialize(name, count, cmc_data_source = nil)
|
8
8
|
@name = name
|
9
9
|
@count = count
|
10
10
|
@tags = []
|
11
|
-
@cmc_data_source = cmc_data_source
|
11
|
+
@cmc_data_source = cmc_data_source || CwCardUtils.card_data_source
|
12
12
|
end
|
13
13
|
|
14
14
|
attr_reader :name, :count, :cmc_data_source
|
@@ -6,9 +6,9 @@ module CwCardUtils
|
|
6
6
|
class Parser
|
7
7
|
attr_reader :deck
|
8
8
|
|
9
|
-
def initialize(decklist, cmc_data_source =
|
9
|
+
def initialize(decklist, cmc_data_source = nil)
|
10
10
|
@decklist = decklist.is_a?(IO) ? decklist.read : decklist
|
11
|
-
@deck = Deck.new(cmc_data_source)
|
11
|
+
@deck = Deck.new(cmc_data_source || CwCardUtils.card_data_source)
|
12
12
|
end
|
13
13
|
|
14
14
|
def inspect
|
@@ -79,9 +79,9 @@ module CwCardUtils
|
|
79
79
|
|
80
80
|
# Represents a card with Scryfall data
|
81
81
|
class ScryfallCard
|
82
|
-
def initialize(name, data_source =
|
82
|
+
def initialize(name, data_source = nil)
|
83
83
|
@name = name
|
84
|
-
@data = data_source.find_card(@name) || {}
|
84
|
+
@data = (data_source || CwCardUtils.card_data_source).find_card(@name) || {}
|
85
85
|
end
|
86
86
|
|
87
87
|
def cmc
|
@@ -10,22 +10,27 @@ module CwCardUtils
|
|
10
10
|
|
11
11
|
# Probability of drawing at least one of the target cards
|
12
12
|
def prob_single(target_names, draws)
|
13
|
-
|
14
|
-
|
13
|
+
targets = Array(target_names).uniq
|
14
|
+
draws_clamped = clamp_draws(draws)
|
15
|
+
total_copies = count_copies(targets)
|
16
|
+
total = hypergeometric(@deck_size, draws_clamped).to_f
|
17
|
+
prob = 1 - hypergeometric(@deck_size - total_copies, draws_clamped).to_f / total
|
18
|
+
prob.clamp(0.0, 1.0)
|
15
19
|
end
|
16
20
|
|
17
21
|
# Probability of drawing ALL cards in the targets list (synergy pair/trio)
|
18
22
|
def prob_combo(target_names, draws)
|
19
|
-
|
23
|
+
targets = Array(target_names).uniq
|
24
|
+
case targets.size
|
20
25
|
when 1
|
21
|
-
prob_single(
|
26
|
+
prob_single(targets, draws)
|
22
27
|
when 2
|
23
|
-
prob_two_card_combo(
|
28
|
+
prob_two_card_combo(targets, draws)
|
24
29
|
when 3
|
25
|
-
prob_three_card_combo(
|
30
|
+
prob_three_card_combo(targets, draws)
|
26
31
|
else
|
27
32
|
# For >3 cards, fallback to approximation
|
28
|
-
approx_combo(
|
33
|
+
approx_combo(targets, draws)
|
29
34
|
end
|
30
35
|
end
|
31
36
|
|
@@ -33,60 +38,70 @@ module CwCardUtils
|
|
33
38
|
|
34
39
|
# Exact for 2-card combos
|
35
40
|
def prob_two_card_combo(names, draws)
|
36
|
-
|
37
|
-
copies_b = count_copies([names[1]])
|
41
|
+
draws_clamped = clamp_draws(draws)
|
38
42
|
|
39
|
-
|
43
|
+
copies_a = copies_by_name[names[0]]
|
44
|
+
copies_b = copies_by_name[names[1]]
|
45
|
+
|
46
|
+
total = hypergeometric(@deck_size, draws_clamped).to_f
|
40
47
|
|
41
48
|
# Probability missing A
|
42
|
-
miss_a = hypergeometric(@deck_size - copies_a,
|
49
|
+
miss_a = hypergeometric(@deck_size - copies_a, draws_clamped) / total
|
43
50
|
# Probability missing B
|
44
|
-
miss_b = hypergeometric(@deck_size - copies_b,
|
51
|
+
miss_b = hypergeometric(@deck_size - copies_b, draws_clamped) / total
|
45
52
|
# Probability missing both
|
46
|
-
miss_both = hypergeometric(@deck_size - (copies_a + copies_b),
|
53
|
+
miss_both = hypergeometric(@deck_size - (copies_a + copies_b), draws_clamped) / total
|
47
54
|
|
48
55
|
# Inclusion–exclusion
|
49
|
-
|
56
|
+
prob = 1 - (miss_a + miss_b - miss_both)
|
57
|
+
prob.clamp(0.0, 1.0)
|
50
58
|
end
|
51
59
|
|
52
60
|
# Exact for 3-card combos
|
53
61
|
def prob_three_card_combo(names, draws)
|
54
|
-
|
55
|
-
|
56
|
-
|
62
|
+
draws_clamped = clamp_draws(draws)
|
63
|
+
|
64
|
+
copies_a = copies_by_name[names[0]]
|
65
|
+
copies_b = copies_by_name[names[1]]
|
66
|
+
copies_c = copies_by_name[names[2]]
|
57
67
|
|
58
|
-
total = hypergeometric(@deck_size,
|
68
|
+
total = hypergeometric(@deck_size, draws_clamped).to_f
|
59
69
|
|
60
|
-
miss_a = hypergeometric(@deck_size - copies_a,
|
61
|
-
miss_b = hypergeometric(@deck_size - copies_b,
|
62
|
-
miss_c = hypergeometric(@deck_size - copies_c,
|
70
|
+
miss_a = hypergeometric(@deck_size - copies_a, draws_clamped) / total
|
71
|
+
miss_b = hypergeometric(@deck_size - copies_b, draws_clamped) / total
|
72
|
+
miss_c = hypergeometric(@deck_size - copies_c, draws_clamped) / total
|
63
73
|
|
64
|
-
miss_ab = hypergeometric(@deck_size - (copies_a + copies_b),
|
65
|
-
miss_ac = hypergeometric(@deck_size - (copies_a + copies_c),
|
66
|
-
miss_bc = hypergeometric(@deck_size - (copies_b + copies_c),
|
74
|
+
miss_ab = hypergeometric(@deck_size - (copies_a + copies_b), draws_clamped) / total
|
75
|
+
miss_ac = hypergeometric(@deck_size - (copies_a + copies_c), draws_clamped) / total
|
76
|
+
miss_bc = hypergeometric(@deck_size - (copies_b + copies_c), draws_clamped) / total
|
67
77
|
|
68
|
-
miss_abc = hypergeometric(@deck_size - (copies_a + copies_b + copies_c),
|
78
|
+
miss_abc = hypergeometric(@deck_size - (copies_a + copies_b + copies_c), draws_clamped) / total
|
69
79
|
|
70
80
|
# Inclusion–exclusion for 3 sets
|
71
|
-
|
81
|
+
prob = 1 - (miss_a + miss_b + miss_c) +
|
72
82
|
(miss_ab + miss_ac + miss_bc) -
|
73
|
-
miss_abc
|
83
|
+
miss_abc
|
84
|
+
prob.clamp(0.0, 1.0)
|
74
85
|
end
|
75
86
|
|
76
|
-
# Approximation for >3 cards
|
87
|
+
# Approximation for >3 cards
|
77
88
|
def approx_combo(target_names, draws)
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
89
|
+
draws_clamped = clamp_draws(draws)
|
90
|
+
total = hypergeometric(@deck_size, draws_clamped).to_f
|
91
|
+
|
92
|
+
prob_missing = target_names.sum do |name|
|
93
|
+
copies = copies_by_name[name]
|
94
|
+
hypergeometric(@deck_size - copies, draws_clamped).to_f / total
|
83
95
|
end
|
84
|
-
|
96
|
+
|
97
|
+
prob = 1 - prob_missing
|
98
|
+
prob.clamp(0.0, 1.0)
|
85
99
|
end
|
86
100
|
|
87
101
|
# Utility: count how many copies of given cards are in the deck
|
88
102
|
def count_copies(names)
|
89
|
-
|
103
|
+
unique = names.uniq
|
104
|
+
@deck.main.sum { |card| unique.include?(card.name) ? card.count : 0 }
|
90
105
|
end
|
91
106
|
|
92
107
|
# Hypergeometric combination helper
|
@@ -95,9 +110,19 @@ module CwCardUtils
|
|
95
110
|
factorial(n) / (factorial(k) * factorial(n - k))
|
96
111
|
end
|
97
112
|
|
113
|
+
def clamp_draws(draws)
|
114
|
+
return 0 if draws.to_i < 0
|
115
|
+
[draws.to_i, @deck_size].min
|
116
|
+
end
|
117
|
+
|
98
118
|
def factorial(n)
|
99
119
|
return 1 if n.zero?
|
100
120
|
(1..n).reduce(1, :*)
|
101
121
|
end
|
122
|
+
|
123
|
+
# Only need to calculate this once for the deck passed in.
|
124
|
+
def copies_by_name
|
125
|
+
@copies_by_name ||= @deck.main.each_with_object(Hash.new(0)) { |card, h| h[card.name] += card.count }
|
126
|
+
end
|
102
127
|
end
|
103
128
|
end
|
data/lib/cw_card_utils.rb
CHANGED
@@ -9,4 +9,17 @@ require_relative "cw_card_utils/deck_comparator"
|
|
9
9
|
|
10
10
|
module CwCardUtils
|
11
11
|
class Error < StandardError; end
|
12
|
+
|
13
|
+
# Configuration for the library
|
14
|
+
class << self
|
15
|
+
attr_writer :card_data_source
|
16
|
+
|
17
|
+
def card_data_source
|
18
|
+
@card_data_source ||= ScryfallCmcData.instance
|
19
|
+
end
|
20
|
+
|
21
|
+
def configure
|
22
|
+
yield self if block_given?
|
23
|
+
end
|
24
|
+
end
|
12
25
|
end
|