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.
- checksums.yaml +4 -4
- data/README.md +113 -5
- data/lib/cw_card_utils/curve_calculator.rb +50 -0
- data/lib/cw_card_utils/deck_comparator.rb +47 -0
- data/lib/cw_card_utils/decklist_parser/archetype_detector.rb +34 -0
- data/lib/cw_card_utils/decklist_parser/card.rb +67 -2
- data/lib/cw_card_utils/decklist_parser/card_tagger.rb +32 -0
- data/lib/cw_card_utils/decklist_parser/color_identity_resolver.rb +7 -0
- data/lib/cw_card_utils/decklist_parser/deck.rb +177 -1
- data/lib/cw_card_utils/decklist_parser/parser.rb +26 -0
- data/lib/cw_card_utils/scryfall_cmc_data.rb +94 -6
- data/lib/cw_card_utils/synergy_probability.rb +95 -36
- data/lib/cw_card_utils/version.rb +8 -1
- data/lib/cw_card_utils.rb +21 -1
- metadata +2 -1
@@ -1,31 +1,70 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module CwCardUtils
|
4
|
-
# Calculates probability of drawing specific card combinations
|
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
|
-
|
14
|
-
|
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
|
-
|
57
|
+
targets = Array(target_names).uniq
|
58
|
+
case targets.size
|
20
59
|
when 1
|
21
|
-
prob_single(
|
60
|
+
prob_single(targets, draws)
|
22
61
|
when 2
|
23
|
-
prob_two_card_combo(
|
62
|
+
prob_two_card_combo(targets, draws)
|
24
63
|
when 3
|
25
|
-
prob_three_card_combo(
|
64
|
+
prob_three_card_combo(targets, draws)
|
26
65
|
else
|
27
66
|
# For >3 cards, fallback to approximation
|
28
|
-
approx_combo(
|
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
|
-
|
37
|
-
copies_b = count_copies([names[1]])
|
75
|
+
draws_clamped = clamp_draws(draws)
|
38
76
|
|
39
|
-
|
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,
|
83
|
+
miss_a = hypergeometric(@deck_size - copies_a, draws_clamped) / total
|
43
84
|
# Probability missing B
|
44
|
-
miss_b = hypergeometric(@deck_size - copies_b,
|
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),
|
87
|
+
miss_both = hypergeometric(@deck_size - (copies_a + copies_b), draws_clamped) / total
|
47
88
|
|
48
89
|
# Inclusion–exclusion
|
49
|
-
|
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
|
-
|
55
|
-
|
56
|
-
|
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,
|
102
|
+
total = hypergeometric(@deck_size, draws_clamped).to_f
|
59
103
|
|
60
|
-
miss_a = hypergeometric(@deck_size - copies_a,
|
61
|
-
miss_b = hypergeometric(@deck_size - copies_b,
|
62
|
-
miss_c = hypergeometric(@deck_size - copies_c,
|
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),
|
65
|
-
miss_ac = hypergeometric(@deck_size - (copies_a + copies_c),
|
66
|
-
miss_bc = hypergeometric(@deck_size - (copies_b + copies_c),
|
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),
|
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
|
-
|
115
|
+
prob = 1 - (miss_a + miss_b + miss_c) +
|
72
116
|
(miss_ab + miss_ac + miss_bc) -
|
73
|
-
miss_abc
|
117
|
+
miss_abc
|
118
|
+
prob.clamp(0.0, 1.0)
|
74
119
|
end
|
75
120
|
|
76
|
-
# Approximation for >3 cards
|
121
|
+
# Approximation for >3 cards
|
77
122
|
def approx_combo(target_names, draws)
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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.
|
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
|