cw_card_utils 0.1.10 → 0.1.12

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.
@@ -1,31 +1,70 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module CwCardUtils
4
- # Calculates probability of drawing specific card combinations for synergy analysis
4
+ # Public: Calculates probability of drawing specific card combinations.
5
+ # 日本語: 特定カードの組み合わせを引く確率を計算します。
5
6
  class SynergyProbability
7
+ # Public: Initialize with a deck and its size.
8
+ # 日本語: デッキとそのサイズで初期化します。
9
+ #
10
+ # @param deck [CwCardUtils::DecklistParser::Deck]
11
+ # @param deck_size [Integer]
12
+ # @return [void]
6
13
  def initialize(deck, deck_size: 60)
7
14
  @deck = deck
8
15
  @deck_size = deck_size
9
16
  end
10
17
 
11
18
  # Probability of drawing at least one of the target cards
19
+ # Public: Probability of drawing at least one of targets in given draws.
20
+ # 日本語: 指定枚数のドローで、対象のいずれか1枚以上を引く確率。
21
+ #
22
+ # How it works (EN): Uses the complement rule with hypergeometric
23
+ # coefficients: 1 - C(N - T, d) / C(N, d), where N is deck size,
24
+ # T is total copies of targets, and d is draws (clamped).
25
+ #
26
+ # 仕組み (JA): 超幾何係数の補集合を用います。N をデッキ枚数、T を
27
+ # 対象の総枚数、d をドロー枚数 (クランプ済み) とすると、
28
+ # 1 - C(N - T, d) / C(N, d) で計算します。
29
+ #
30
+ # @param target_names [Array<String>]
31
+ # @param draws [Integer]
32
+ # @return [Float] 0.0..1.0
12
33
  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)
34
+ targets = Array(target_names).uniq
35
+ draws_clamped = clamp_draws(draws)
36
+ total_copies = count_copies(targets)
37
+ total = hypergeometric(@deck_size, draws_clamped).to_f
38
+ prob = 1 - hypergeometric(@deck_size - total_copies, draws_clamped).to_f / total
39
+ prob.clamp(0.0, 1.0)
15
40
  end
16
41
 
17
42
  # Probability of drawing ALL cards in the targets list (synergy pair/trio)
43
+ # Public: Probability of drawing all targets in given draws.
44
+ # 日本語: 指定枚数のドローで対象の全カードを揃える確率。
45
+ #
46
+ # How it works (EN): For 1/2/3-card combos, uses exact inclusion–
47
+ # exclusion with hypergeometric terms. For 4+ cards, uses a
48
+ # conservative approximation by summing miss-probabilities.
49
+ #
50
+ # 仕組み (JA): 1/2/3 枚コンボは超幾何項の包除原理で厳密に算出。
51
+ # 4 枚以上では各カードの不在確率を合算する保守的近似を用います。
52
+ #
53
+ # @param target_names [Array<String>]
54
+ # @param draws [Integer]
55
+ # @return [Float] 0.0..1.0
18
56
  def prob_combo(target_names, draws)
19
- case target_names.size
57
+ targets = Array(target_names).uniq
58
+ case targets.size
20
59
  when 1
21
- prob_single(target_names, draws)
60
+ prob_single(targets, draws)
22
61
  when 2
23
- prob_two_card_combo(target_names, draws)
62
+ prob_two_card_combo(targets, draws)
24
63
  when 3
25
- prob_three_card_combo(target_names, draws)
64
+ prob_three_card_combo(targets, draws)
26
65
  else
27
66
  # For >3 cards, fallback to approximation
28
- approx_combo(target_names, draws)
67
+ approx_combo(targets, draws)
29
68
  end
30
69
  end
31
70
 
@@ -33,60 +72,70 @@ module CwCardUtils
33
72
 
34
73
  # Exact for 2-card combos
35
74
  def prob_two_card_combo(names, draws)
36
- copies_a = count_copies([names[0]])
37
- copies_b = count_copies([names[1]])
75
+ draws_clamped = clamp_draws(draws)
38
76
 
39
- total = hypergeometric(@deck_size, draws).to_f
77
+ copies_a = copies_by_name[names[0]]
78
+ copies_b = copies_by_name[names[1]]
79
+
80
+ total = hypergeometric(@deck_size, draws_clamped).to_f
40
81
 
41
82
  # Probability missing A
42
- miss_a = hypergeometric(@deck_size - copies_a, draws) / total
83
+ miss_a = hypergeometric(@deck_size - copies_a, draws_clamped) / total
43
84
  # Probability missing B
44
- miss_b = hypergeometric(@deck_size - copies_b, draws) / total
85
+ miss_b = hypergeometric(@deck_size - copies_b, draws_clamped) / total
45
86
  # Probability missing both
46
- miss_both = hypergeometric(@deck_size - (copies_a + copies_b), draws) / total
87
+ miss_both = hypergeometric(@deck_size - (copies_a + copies_b), draws_clamped) / total
47
88
 
48
89
  # Inclusion–exclusion
49
- [1 - (miss_a + miss_b - miss_both), 0].max
90
+ prob = 1 - (miss_a + miss_b - miss_both)
91
+ prob.clamp(0.0, 1.0)
50
92
  end
51
93
 
52
94
  # Exact for 3-card combos
53
95
  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]])
96
+ draws_clamped = clamp_draws(draws)
97
+
98
+ copies_a = copies_by_name[names[0]]
99
+ copies_b = copies_by_name[names[1]]
100
+ copies_c = copies_by_name[names[2]]
57
101
 
58
- total = hypergeometric(@deck_size, draws).to_f
102
+ total = hypergeometric(@deck_size, draws_clamped).to_f
59
103
 
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
104
+ miss_a = hypergeometric(@deck_size - copies_a, draws_clamped) / total
105
+ miss_b = hypergeometric(@deck_size - copies_b, draws_clamped) / total
106
+ miss_c = hypergeometric(@deck_size - copies_c, draws_clamped) / total
63
107
 
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
108
+ miss_ab = hypergeometric(@deck_size - (copies_a + copies_b), draws_clamped) / total
109
+ miss_ac = hypergeometric(@deck_size - (copies_a + copies_c), draws_clamped) / total
110
+ miss_bc = hypergeometric(@deck_size - (copies_b + copies_c), draws_clamped) / total
67
111
 
68
- miss_abc = hypergeometric(@deck_size - (copies_a + copies_b + copies_c), draws) / total
112
+ miss_abc = hypergeometric(@deck_size - (copies_a + copies_b + copies_c), draws_clamped) / total
69
113
 
70
114
  # Inclusion–exclusion for 3 sets
71
- [1 - (miss_a + miss_b + miss_c) +
115
+ prob = 1 - (miss_a + miss_b + miss_c) +
72
116
  (miss_ab + miss_ac + miss_bc) -
73
- miss_abc, 0].max
117
+ miss_abc
118
+ prob.clamp(0.0, 1.0)
74
119
  end
75
120
 
76
- # Approximation for >3 cards (same as old method)
121
+ # Approximation for >3 cards
77
122
  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
123
+ draws_clamped = clamp_draws(draws)
124
+ total = hypergeometric(@deck_size, draws_clamped).to_f
125
+
126
+ prob_missing = target_names.sum do |name|
127
+ copies = copies_by_name[name]
128
+ hypergeometric(@deck_size - copies, draws_clamped).to_f / total
83
129
  end
84
- [1 - prob_missing, 0].max
130
+
131
+ prob = 1 - prob_missing
132
+ prob.clamp(0.0, 1.0)
85
133
  end
86
134
 
87
135
  # Utility: count how many copies of given cards are in the deck
88
136
  def count_copies(names)
89
- @deck.main.sum { |card| names.include?(card.name) ? card.count : 0 }
137
+ unique = names.uniq
138
+ @deck.main.sum { |card| unique.include?(card.name) ? card.count : 0 }
90
139
  end
91
140
 
92
141
  # Hypergeometric combination helper
@@ -95,9 +144,19 @@ module CwCardUtils
95
144
  factorial(n) / (factorial(k) * factorial(n - k))
96
145
  end
97
146
 
147
+ def clamp_draws(draws)
148
+ return 0 if draws.to_i < 0
149
+ [draws.to_i, @deck_size].min
150
+ end
151
+
98
152
  def factorial(n)
99
153
  return 1 if n.zero?
100
154
  (1..n).reduce(1, :*)
101
155
  end
156
+
157
+ # Only need to calculate this once for the deck passed in.
158
+ def copies_by_name
159
+ @copies_by_name ||= @deck.main.each_with_object(Hash.new(0)) { |card, h| h[card.name] += card.count }
160
+ end
102
161
  end
103
162
  end
@@ -1,5 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ ##
4
+ # Public: Version of the CwCardUtils gem.
5
+ # 日本語: この CwCardUtils gem のバージョン定義です。
3
6
  module CwCardUtils
4
- VERSION = "0.1.10"
7
+ # Public: The current version string.
8
+ # 日本語: 現在のバージョン文字列です。
9
+ #
10
+ # @return [String]
11
+ VERSION = "0.1.12"
5
12
  end
data/lib/cw_card_utils.rb CHANGED
@@ -7,17 +7,37 @@ require_relative "cw_card_utils/scryfall_cmc_data"
7
7
  require_relative "cw_card_utils/synergy_probability"
8
8
  require_relative "cw_card_utils/deck_comparator"
9
9
 
10
+ ##
11
+ # Public: Top-level namespace for Card Utils.
12
+ # 日本語: Card Utils のトップレベル名前空間です。
10
13
  module CwCardUtils
14
+ ##
15
+ # Public: Gem-specific error base class.
16
+ # 日本語: このライブラリ用の基本的なエラークラスです。
11
17
  class Error < StandardError; end
12
18
 
13
- # Configuration for the library
19
+ # Public: Configuration for the library.
20
+ # 日本語: ライブラリの設定を行います。
14
21
  class << self
22
+ # Public: Overrides the global card data source.
23
+ # 日本語: グローバルなカードデータソースを上書きします。
24
+ #
25
+ # @param source [CwCardUtils::CardDataSource]
15
26
  attr_writer :card_data_source
16
27
 
28
+ # Public: Returns the global card data source used by parsers and models.
29
+ # 日本語: パーサやモデルが使用するグローバルなカードデータソースを返します。
30
+ #
31
+ # @return [CwCardUtils::CardDataSource]
17
32
  def card_data_source
18
33
  @card_data_source ||= ScryfallCmcData.instance
19
34
  end
20
35
 
36
+ # Public: Yields the module for configuration.
37
+ # 日本語: 設定用にモジュール自身をブロックに渡します。
38
+ #
39
+ # @yield [CwCardUtils]
40
+ # @return [void]
21
41
  def configure
22
42
  yield self if block_given?
23
43
  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.10
4
+ version: 0.1.12
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ryan Stenhouse
@@ -57,6 +57,7 @@ metadata:
57
57
  homepage_uri: https://cracklingwit.com
58
58
  source_code_uri: https://github.com/cracklingwit/card_utils
59
59
  changelog_uri: https://github.com/cracklingwit/card_utils/commits/main/
60
+ documentation_uri: https://card-utils.rdoc.cracklingwit.com
60
61
  rdoc_options: []
61
62
  require_paths:
62
63
  - lib