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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 33af01174c18c349d5ecadd729ff82f228645e4d7de70345e031483a9b6e4aeb
4
- data.tar.gz: d95e60dcf30519bdeca9ca86ab04c727e2b76c1a6de990417bd21bbe41e7ad26
3
+ metadata.gz: 23145f32617794cf1af83523bf179d6d9260cc75251a24207ae6ea6ce0274599
4
+ data.tar.gz: 7cd65b1d7e93b8f9c2438df235d77c44e294d543ff1f22e8b0aa16efb5e51192
5
5
  SHA512:
6
- metadata.gz: dfa2f46a027dda4ffd67bdace956049c47bdc3a009a70f36ade4c722939d38658d05bf8adf57d85753442e44a71f6aeb12a247579277e1ecd38ce17de846bd9e
7
- data.tar.gz: b0e79425dc4ce0d47788cba711c19e80554cf1a89eb63b695cbb03fa2ba0286b2e451cc20364a3b3344fd97cb0f5a56db16ba3464e90a341563fcf58ec245b1d
6
+ metadata.gz: 556bc7d3c4dc4d87b752d2923eba061b23356a6e2330f33691e34ee215d7cdd9e6148cd8c4a9082ddbcda179ffd68bef162d0bc6e11c25db3bc05b95589f83d9
7
+ data.tar.gz: e81abacff1c5dae3476a1ffb9e6a779105546f7a020edcc36fb463082842645a784e922d89660c2e779c4ceaa1d51dadf14b84ed4ae33d6326fe7188ab1135b7
data/README.md CHANGED
@@ -1,50 +1,79 @@
1
1
  # CwCardUtils
2
2
 
3
- TODO: Delete this and the text below, and describe your gem
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
- TODO: Replace `UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG` with your gem name right after releasing it to RubyGems.org. Please do not do it earlier due to security reasons. Alternatively, replace this section with instructions to install your gem from git if you don't plan to release to RubyGems.org.
7
+ Add this line to your application's Gemfile:
8
+
9
+ ```ruby
10
+ gem 'cw_card_utils'
11
+ ```
10
12
 
11
- Install the gem and add to the application's Gemfile by executing:
13
+ And then execute:
12
14
 
13
15
  ```bash
14
- bundle add UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG
16
+ bundle install
15
17
  ```
16
18
 
17
- If bundler is not being used to manage dependencies, install the gem by executing:
19
+ ## Configuration
18
20
 
19
- ```bash
20
- gem install UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG
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
- TODO: Write usage instructions here
37
+ ### Basic Deck Parsing
26
38
 
27
- ## Development
39
+ ```ruby
40
+ require 'cw_card_utils'
28
41
 
29
- 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.
42
+ # Parse a decklist
43
+ decklist = <<~DECK
44
+ 4 Lightning Bolt
45
+ 4 Mountain
46
+ 2 Shock
47
+ DECK
30
48
 
31
- To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
49
+ deck = CwCardUtils::DecklistParser::Parser.new(decklist).parse
32
50
 
33
- ## Contributing
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
- Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/cw_card_utils. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/[USERNAME]/cw_card_utils/blob/main/CODE_OF_CONDUCT.md).
57
+ ### Custom Data Sources
36
58
 
37
- ## License
59
+ You can implement your own card data source by inheriting from `CwCardUtils::CardDataSource`:
38
60
 
39
- The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
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
- ## Code of Conduct
69
+ # Configure the library to use your data source
70
+ CwCardUtils.card_data_source = MyCustomDataSource.new
71
+ ```
42
72
 
43
- Everyone interacting in the CwCardUtils project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/[USERNAME]/cw_card_utils/blob/main/CODE_OF_CONDUCT.md).
73
+ ## Development
44
74
 
45
- ## Included Data
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
- This gem includes [bulk data from Scryfall][1] to provide fallback for the curve calculator. For production
48
- or real-life usage, this should use your own data store.
77
+ ## Contributing
49
78
 
50
- [1]: https://scryfall.com/docs/api/bulk-data
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
- (card.tags & [:synergistic_finisher, :tribal_synergy, :scaling_threat]).any?
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 = CwCardUtils::ScryfallCmcData.instance)
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 = CwCardUtils::ScryfallCmcData.instance)
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 = ScryfallCmcData.instance)
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
- total_copies = count_copies(target_names)
14
- 1 - hypergeometric(@deck_size - total_copies, draws).to_f / hypergeometric(@deck_size, draws)
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
- case target_names.size
23
+ targets = Array(target_names).uniq
24
+ case targets.size
20
25
  when 1
21
- prob_single(target_names, draws)
26
+ prob_single(targets, draws)
22
27
  when 2
23
- prob_two_card_combo(target_names, draws)
28
+ prob_two_card_combo(targets, draws)
24
29
  when 3
25
- prob_three_card_combo(target_names, draws)
30
+ prob_three_card_combo(targets, draws)
26
31
  else
27
32
  # For >3 cards, fallback to approximation
28
- approx_combo(target_names, draws)
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
- copies_a = count_copies([names[0]])
37
- copies_b = count_copies([names[1]])
41
+ draws_clamped = clamp_draws(draws)
38
42
 
39
- total = hypergeometric(@deck_size, draws).to_f
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, draws) / total
49
+ miss_a = hypergeometric(@deck_size - copies_a, draws_clamped) / total
43
50
  # Probability missing B
44
- miss_b = hypergeometric(@deck_size - copies_b, draws) / total
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), draws) / total
53
+ miss_both = hypergeometric(@deck_size - (copies_a + copies_b), draws_clamped) / total
47
54
 
48
55
  # Inclusion–exclusion
49
- [1 - (miss_a + miss_b - miss_both), 0].max
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
- copies_a = count_copies([names[0]])
55
- copies_b = count_copies([names[1]])
56
- copies_c = count_copies([names[2]])
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, draws).to_f
68
+ total = hypergeometric(@deck_size, draws_clamped).to_f
59
69
 
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
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), 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
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), draws) / total
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
- [1 - (miss_a + miss_b + miss_c) +
81
+ prob = 1 - (miss_a + miss_b + miss_c) +
72
82
  (miss_ab + miss_ac + miss_bc) -
73
- miss_abc, 0].max
83
+ miss_abc
84
+ prob.clamp(0.0, 1.0)
74
85
  end
75
86
 
76
- # Approximation for >3 cards (same as old method)
87
+ # Approximation for >3 cards
77
88
  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
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
- [1 - prob_missing, 0].max
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
- @deck.main.sum { |card| names.include?(card.name) ? card.count : 0 }
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module CwCardUtils
4
- VERSION = "0.1.9"
4
+ VERSION = "0.1.11"
5
5
  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
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.9
4
+ version: 0.1.11
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ryan Stenhouse