cw_card_utils 0.1.11 → 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 +35 -1
- data/lib/cw_card_utils/version.rb +8 -1
- data/lib/cw_card_utils.rb +21 -1
- metadata +2 -1
@@ -2,12 +2,18 @@
|
|
2
2
|
|
3
3
|
module CwCardUtils
|
4
4
|
module DecklistParser
|
5
|
-
# A
|
5
|
+
# Public: A deck is a collection of cards and utilities around it.
|
6
|
+
# 日本語: デッキはカードの集合であり、各種ユーティリティを提供します。
|
6
7
|
class Deck
|
7
8
|
IGNORED_TRIBAL_TYPES = %w[
|
8
9
|
human wizard soldier scout shaman warrior cleric rogue advisor knight druid
|
9
10
|
].freeze
|
10
11
|
|
12
|
+
# Public: Initialize an empty deck with a data source.
|
13
|
+
# 日本語: データソースを指定して空のデッキを初期化します。
|
14
|
+
#
|
15
|
+
# @param cmc_data_source [CwCardUtils::CardDataSource]
|
16
|
+
# @return [void]
|
11
17
|
def initialize(cmc_data_source)
|
12
18
|
@main = []
|
13
19
|
@sideboard = []
|
@@ -17,6 +23,10 @@ module CwCardUtils
|
|
17
23
|
@cmc_data_source = cmc_data_source
|
18
24
|
end
|
19
25
|
|
26
|
+
# Public: Serialize as a Hash suitable for JSON.
|
27
|
+
# 日本語: JSON化に適したHashへシリアライズします。
|
28
|
+
#
|
29
|
+
# @return [Hash]
|
20
30
|
def to_h
|
21
31
|
{
|
22
32
|
mainboard: main.map(&:to_h),
|
@@ -25,26 +35,61 @@ module CwCardUtils
|
|
25
35
|
}
|
26
36
|
end
|
27
37
|
|
38
|
+
# Public: Unique color identity across mainboard.
|
39
|
+
# 日本語: メインボードの色アイデンティティ集合。
|
40
|
+
#
|
41
|
+
# @return [Array<String>]
|
28
42
|
def color_identity
|
29
43
|
@color_identity ||= main.map(&:color_identity).flatten.uniq
|
30
44
|
end
|
31
45
|
|
46
|
+
# Public: Human readable color label.
|
47
|
+
# 日本語: 人間が読みやすい色表記。
|
48
|
+
#
|
49
|
+
# @return [String]
|
32
50
|
def color_identity_string
|
33
51
|
@color_identity_string ||= CwCardUtils::DecklistParser::ColorIdentityResolver.resolve(color_identity)
|
34
52
|
end
|
35
53
|
|
54
|
+
# Public: JSON representation of the deck.
|
55
|
+
# 日本語: デッキのJSON表現。
|
56
|
+
#
|
57
|
+
# @return [String]
|
36
58
|
def to_json(*_args)
|
37
59
|
to_h.to_json
|
38
60
|
end
|
39
61
|
|
62
|
+
# Public: Mainboard cards with tags applied.
|
63
|
+
# 日本語: タグ適用済みのメインボードカード一覧。
|
64
|
+
#
|
65
|
+
# How it works (EN): Lazily computes tags for each card via
|
66
|
+
# `CardTagger` the first time access occurs, then returns the
|
67
|
+
# mutated card objects for downstream consumers.
|
68
|
+
#
|
69
|
+
# 仕組み (JA): 初回アクセス時に `CardTagger` で各カードのタグを
|
70
|
+
# 遅延計算し、以降はタグ付け済みカードを返します。
|
71
|
+
#
|
72
|
+
# @return [Array<CwCardUtils::DecklistParser::Card>]
|
40
73
|
def main
|
41
74
|
tag_cards(@main)
|
42
75
|
end
|
43
76
|
|
77
|
+
# Public: Sideboard cards with tags applied.
|
78
|
+
# 日本語: タグ適用済みのサイドボードカード一覧。
|
79
|
+
#
|
80
|
+
# How it works (EN): Same lazy-tagging behavior as `main`, but for
|
81
|
+
# sideboard entries.
|
82
|
+
#
|
83
|
+
# 仕組み (JA): `main` と同様に遅延タグ付けを行いますが、対象は
|
84
|
+
# サイドボードです。
|
85
|
+
#
|
86
|
+
# @return [Array<CwCardUtils::DecklistParser::Card>]
|
44
87
|
def sideboard
|
45
88
|
tag_cards(@sideboard)
|
46
89
|
end
|
47
90
|
|
91
|
+
# Internal: Apply tags to cards lazily.
|
92
|
+
# 日本語: 遅延的にカードへタグを適用します。
|
48
93
|
def tag_cards(cards)
|
49
94
|
cards.map do |c|
|
50
95
|
c.tags = CwCardUtils::DecklistParser::CardTagger.new(c, self).tags
|
@@ -52,44 +97,117 @@ module CwCardUtils
|
|
52
97
|
end
|
53
98
|
end
|
54
99
|
|
100
|
+
# Public: Detected archetype label.
|
101
|
+
# 日本語: 検出されたアーキタイプのラベル。
|
102
|
+
#
|
103
|
+
# Intent (EN): Summarize the deck’s strategic identity using
|
104
|
+
# aggregated tag ratios/counts and average CMC.
|
105
|
+
#
|
106
|
+
# 意図 (JA): タグの比率/件数と平均 CMC を用いて、デッキの戦略的
|
107
|
+
# アイデンティティを要約します。
|
108
|
+
#
|
109
|
+
# @return [String]
|
55
110
|
def archetype
|
56
111
|
@archetype ||= CwCardUtils::DecklistParser::ArchetypeDetector.new(self).detect
|
57
112
|
end
|
58
113
|
|
114
|
+
# Public: Human-friendly deck summary string.
|
115
|
+
# 日本語: 人が読みやすいデッキ概要文字列。
|
116
|
+
#
|
117
|
+
# @return [String]
|
59
118
|
def inspect
|
60
119
|
"<Deck: main: #{mainboard_size} sideboard: #{sideboard_size} lands: #{lands_count} x_to_cast: #{x_to_cast_count} cards: #{cards_count}>"
|
61
120
|
end
|
62
121
|
|
122
|
+
# Public: Collapsed curve counts.
|
123
|
+
# 日本語: まとめられたカーブのカウント。
|
124
|
+
#
|
125
|
+
# Intent (EN): Provide a coarse view of the deck’s curve using
|
126
|
+
# canonical buckets for quick comparison.
|
127
|
+
#
|
128
|
+
# 意図 (JA): 代表的なバケットでカーブを粗く表し、比較を容易に
|
129
|
+
# します。
|
130
|
+
#
|
131
|
+
# @return [Hash{String=>Integer}]
|
63
132
|
def collapsed_curve
|
64
133
|
@collapsed_curve ||= CurveCalculator.new(self).collapsed_curve
|
65
134
|
end
|
66
135
|
|
136
|
+
# Public: Raw curve counts.
|
137
|
+
# 日本語: 素のカーブカウント。
|
138
|
+
#
|
139
|
+
# Intent (EN): Exact histogram of non-land CMC ceilings used by
|
140
|
+
# calculators and visualizations.
|
141
|
+
#
|
142
|
+
# 意図 (JA): 計算や可視化で用いる、土地以外の CMC 切り上げの
|
143
|
+
# ヒストグラムです。
|
144
|
+
#
|
145
|
+
# @return [Hash{Integer=>Integer}]
|
67
146
|
def curve
|
68
147
|
@curve ||= CurveCalculator.new(self).curve
|
69
148
|
end
|
70
149
|
|
150
|
+
# Public: Normalized curve fractions.
|
151
|
+
# 日本語: 正規化されたカーブ比率。
|
152
|
+
#
|
153
|
+
# Intent (EN): Convert raw counts into proportions of non-land
|
154
|
+
# cards to compare decks of different sizes.
|
155
|
+
#
|
156
|
+
# 意図 (JA): デッキサイズが異なる場合でも比較できるよう、土地以外
|
157
|
+
# の枚数で割った比率に変換します。
|
158
|
+
#
|
159
|
+
# @return [Hash{Integer=>Float}]
|
71
160
|
def normalized_curve
|
72
161
|
@normalized_curve ||= CurveCalculator.new(self).normalized_curve
|
73
162
|
end
|
74
163
|
|
164
|
+
# Public: Collapsed normalized curve.
|
165
|
+
# 日本語: まとめられかつ正規化されたカーブ。
|
166
|
+
#
|
167
|
+
# Intent (EN): Normalize the collapsed buckets for concise and
|
168
|
+
# comparable summaries.
|
169
|
+
#
|
170
|
+
# 意図 (JA): まとめたバケットを正規化し、簡潔かつ比較しやすい
|
171
|
+
# 要約を提供します。
|
172
|
+
#
|
173
|
+
# @return [Hash{String=>Float}]
|
75
174
|
def collapsed_normalized_curve
|
76
175
|
@collapsed_normalized_curve ||= CurveCalculator.new(self).collapsed_normalized_curve
|
77
176
|
end
|
78
177
|
|
178
|
+
# Public: True when both main and sideboard are empty.
|
179
|
+
# 日本語: メイン・サイドの両方が空であれば true。
|
180
|
+
#
|
181
|
+
# @return [Boolean]
|
79
182
|
def empty?
|
80
183
|
@main.empty? && @sideboard.empty?
|
81
184
|
end
|
82
185
|
|
186
|
+
# Public: True when deck has any cards.
|
187
|
+
# 日本語: 1枚でもカードがあれば true。
|
188
|
+
#
|
189
|
+
# @return [Boolean]
|
83
190
|
def any?
|
84
191
|
!empty?
|
85
192
|
end
|
86
193
|
|
194
|
+
# Public: Enumerate mainboard cards.
|
195
|
+
# 日本語: メインボードのカードを列挙します。
|
196
|
+
#
|
197
|
+
# @yield [CwCardUtils::DecklistParser::Card] カードをブロックに渡します
|
198
|
+
# @return [void]
|
87
199
|
def each(&block)
|
88
200
|
@main.each do |c|
|
89
201
|
block.call(c)
|
90
202
|
end
|
91
203
|
end
|
92
204
|
|
205
|
+
# Public: Add a card hash to mainboard or sideboard.
|
206
|
+
# 日本語: カードをメイン/サイドに追加します。
|
207
|
+
#
|
208
|
+
# @param c [Hash] { name:, count: }
|
209
|
+
# @param target [Symbol] :mainboard or :sideboard
|
210
|
+
# @return [void]
|
93
211
|
def add(c, target = :mainboard)
|
94
212
|
reset_counters
|
95
213
|
card = Card.new(c[:name], c[:count], @cmc_data_source)
|
@@ -108,6 +226,8 @@ module CwCardUtils
|
|
108
226
|
end
|
109
227
|
end
|
110
228
|
|
229
|
+
# Internal: Invalidate derived counters.
|
230
|
+
# 日本語: 派生カウンタを無効化します。
|
111
231
|
def reset_counters
|
112
232
|
@mainboard_size = nil
|
113
233
|
@sideboard_size = nil
|
@@ -116,10 +236,26 @@ module CwCardUtils
|
|
116
236
|
@cards_count = nil
|
117
237
|
end
|
118
238
|
|
239
|
+
# Public: Detected format (:commander, :standard, :modern)。
|
240
|
+
# 日本語: 判定されたフォーマット (:commander, :standard, :modern)。
|
241
|
+
#
|
242
|
+
# @return [Symbol]
|
119
243
|
def format
|
120
244
|
@format ||= detect_format_for_deck
|
121
245
|
end
|
122
246
|
|
247
|
+
# Public: Detect the format from deck size and singleton rules.
|
248
|
+
# 日本語: デッキサイズとシングルトン規則からフォーマットを判定します。
|
249
|
+
#
|
250
|
+
# How it works (EN): Uses size thresholds (>=100 => Commander),
|
251
|
+
# then checks for singleton behavior ignoring basic lands to
|
252
|
+
# disambiguate Commander-like 60+ lists from Standard.
|
253
|
+
#
|
254
|
+
# 仕組み (JA): サイズ閾値 (>=100 は Commander) を用い、基本土地を
|
255
|
+
# 無視したシングルトン性を確認して、Standard と似た 60+ の
|
256
|
+
# Commander らしさを判別します。
|
257
|
+
#
|
258
|
+
# @return [Symbol]
|
123
259
|
def detect_format_for_deck
|
124
260
|
if mainboard_size >= 100
|
125
261
|
:commander
|
@@ -135,6 +271,8 @@ module CwCardUtils
|
|
135
271
|
end
|
136
272
|
end
|
137
273
|
|
274
|
+
# Internal: Whether the deck is singleton ignoring basic lands.
|
275
|
+
# 日本語: 基本土地を無視してシングルトンかどうか。
|
138
276
|
def is_singleton_deck?
|
139
277
|
# Count non-basic land cards and check for duplicates
|
140
278
|
non_basic_lands = @main.reject { |card| is_basic_land?(card) }
|
@@ -149,44 +287,82 @@ module CwCardUtils
|
|
149
287
|
card_counts.values.all? { |count| count == 1 }
|
150
288
|
end
|
151
289
|
|
290
|
+
# Internal: Whether a card is a basic land.
|
291
|
+
# 日本語: 基本土地であるかどうか。
|
152
292
|
def is_basic_land?(card)
|
153
293
|
basic_land_names = %w[Plains Island Swamp Mountain Forest Wastes]
|
154
294
|
basic_land_names.include?(card.name)
|
155
295
|
end
|
156
296
|
|
297
|
+
# Public: Total mainboard card count.
|
298
|
+
# 日本語: メインボードの総枚数。
|
299
|
+
#
|
300
|
+
# @return [Integer]
|
157
301
|
def mainboard_size
|
158
302
|
@mainboard_size ||= main.sum { |card| card.count }
|
159
303
|
end
|
160
304
|
|
305
|
+
# Public: Total sideboard card count.
|
306
|
+
# 日本語: サイドボードの総枚数。
|
307
|
+
#
|
308
|
+
# @return [Integer]
|
161
309
|
def sideboard_size
|
162
310
|
@sideboard_size ||= sideboard.sum { |card| card.count }
|
163
311
|
end
|
164
312
|
|
313
|
+
# Public: Land count.
|
314
|
+
# 日本語: 土地の枚数。
|
315
|
+
#
|
316
|
+
# @return [Integer]
|
165
317
|
def lands_count
|
166
318
|
@lands.sum { |card| card.count }
|
167
319
|
end
|
168
320
|
|
321
|
+
# Public: X-to-cast count.
|
322
|
+
# 日本語: Xコストのカード枚数。
|
323
|
+
#
|
324
|
+
# @return [Integer]
|
169
325
|
def x_to_cast_count
|
170
326
|
@x_to_cast.sum { |card| card.count }
|
171
327
|
end
|
172
328
|
|
329
|
+
# Public: Total main+sideboard count.
|
330
|
+
# 日本語: メイン+サイドの合計枚数。
|
331
|
+
#
|
332
|
+
# @return [Integer]
|
173
333
|
def cards_count
|
174
334
|
mainboard_size + sideboard_size
|
175
335
|
end
|
176
336
|
|
337
|
+
# Public: Total non-land count.
|
338
|
+
# 日本語: 土地以外の総枚数。
|
339
|
+
#
|
340
|
+
# @return [Integer]
|
177
341
|
def count_without_lands
|
178
342
|
cards_count - lands_count
|
179
343
|
end
|
180
344
|
|
345
|
+
# Public: Deck size including sideboard.
|
346
|
+
# 日本語: サイドボードを含むデッキサイズ。
|
347
|
+
#
|
348
|
+
# @return [Integer]
|
181
349
|
def size
|
182
350
|
cards_count
|
183
351
|
end
|
184
352
|
|
353
|
+
# Public: Dominant tribe symbol or nil.
|
354
|
+
# 日本語: 優勢な部族(トライブ)のシンボル、もしくは nil。
|
355
|
+
#
|
356
|
+
# @return [Symbol, nil]
|
185
357
|
def tribe
|
186
358
|
return @tribe if @tribe
|
187
359
|
@tribe = detect_tribe_for_deck
|
188
360
|
end
|
189
361
|
|
362
|
+
# Public: Detect dominant creature subtype if present.
|
363
|
+
# 日本語: 優勢なクリーチャー・サブタイプがあれば検出します。
|
364
|
+
#
|
365
|
+
# @return [Symbol, nil]
|
190
366
|
def detect_tribe_for_deck
|
191
367
|
subtype_counts = Hash.new(0)
|
192
368
|
total_creatures = 0
|
@@ -3,19 +3,45 @@
|
|
3
3
|
module CwCardUtils
|
4
4
|
module DecklistParser
|
5
5
|
# Parses a decklist and returns a Deck object.
|
6
|
+
# Public: Parses a decklist string/IO into a Deck.
|
7
|
+
# 日本語: デッキリスト文字列/IOを解析して Deck を生成します。
|
6
8
|
class Parser
|
7
9
|
attr_reader :deck
|
8
10
|
|
11
|
+
# Public: Create a new parser.
|
12
|
+
# 日本語: 新しいパーサを作成します。
|
13
|
+
#
|
14
|
+
# @param decklist [String, IO]
|
15
|
+
# @param cmc_data_source [CwCardUtils::CardDataSource]
|
16
|
+
# @return [void]
|
9
17
|
def initialize(decklist, cmc_data_source = nil)
|
10
18
|
@decklist = decklist.is_a?(IO) ? decklist.read : decklist
|
11
19
|
@deck = Deck.new(cmc_data_source || CwCardUtils.card_data_source)
|
12
20
|
end
|
13
21
|
|
22
|
+
# Public: Human-readable summary.
|
23
|
+
# 日本語: 人間が読みやすい概要。
|
24
|
+
#
|
25
|
+
# @return [String]
|
14
26
|
def inspect
|
15
27
|
"<DecklistParser::Parser: #{@decklist.length}>"
|
16
28
|
end
|
17
29
|
|
18
30
|
# Parses the decklist and returns a Deck object.
|
31
|
+
# Public: Parse and return a Deck. Idempotent.
|
32
|
+
# 日本語: 解析して Deck を返します。多重実行に耐性があります。
|
33
|
+
#
|
34
|
+
# How it works (EN): Streams the decklist line-by-line, skipping
|
35
|
+
# section headers and comments, toggling sideboard state as
|
36
|
+
# appropriate, and parsing counts/names using a permissive regex
|
37
|
+
# that tolerates set codes and collector numbers.
|
38
|
+
#
|
39
|
+
# 仕組み (JA): デッキリストを 1 行ずつ処理し、セクション見出しや
|
40
|
+
# コメントをスキップ、適宜サイドボード状態を切替えながら、
|
41
|
+
# 枚数とカード名を寛容な正規表現で抽出します (セットコードや
|
42
|
+
# コレクタ番号を許容)。
|
43
|
+
#
|
44
|
+
# @return [CwCardUtils::DecklistParser::Deck]
|
19
45
|
def parse
|
20
46
|
return @deck if @deck.any?
|
21
47
|
|
@@ -4,12 +4,23 @@ require "json"
|
|
4
4
|
require "singleton"
|
5
5
|
|
6
6
|
module CwCardUtils
|
7
|
-
# Abstract base class for card data sources
|
7
|
+
# Public: Abstract base class for card data sources.
|
8
|
+
# 日本語: カードデータソースの抽象基底クラスです。
|
8
9
|
class CardDataSource
|
10
|
+
# Public: Find a card record by name.
|
11
|
+
# 日本語: カード名でレコードを検索します。
|
12
|
+
#
|
13
|
+
# @param name [String]
|
14
|
+
# @return [Hash, #dig, nil]
|
9
15
|
def find_card(name)
|
10
16
|
raise NotImplementedError, "Subclasses must implement find_card"
|
11
17
|
end
|
12
18
|
|
19
|
+
# Public: Fetch converted mana cost for card.
|
20
|
+
# 日本語: 指定カードの点数で見たマナ・コストを取得します。
|
21
|
+
#
|
22
|
+
# @param name [String]
|
23
|
+
# @return [Numeric, nil]
|
13
24
|
def cmc_for_card(name)
|
14
25
|
card = find_card(name)
|
15
26
|
card&.dig("cmc") || card&.cmc
|
@@ -17,6 +28,11 @@ module CwCardUtils
|
|
17
28
|
nil
|
18
29
|
end
|
19
30
|
|
31
|
+
# Public: Fetch color identity for card.
|
32
|
+
# 日本語: 指定カードの色アイデンティティを取得します。
|
33
|
+
#
|
34
|
+
# @param name [String]
|
35
|
+
# @return [Array<String>]
|
20
36
|
def color_identity_for_card(name)
|
21
37
|
card = find_card(name)
|
22
38
|
card&.dig("color_identity") || card&.color_identity || []
|
@@ -24,6 +40,11 @@ module CwCardUtils
|
|
24
40
|
[]
|
25
41
|
end
|
26
42
|
|
43
|
+
# Public: Fetch keywords for card.
|
44
|
+
# 日本語: 指定カードのキーワードを取得します。
|
45
|
+
#
|
46
|
+
# @param name [String]
|
47
|
+
# @return [Array<String>]
|
27
48
|
def keywords_for_card(name)
|
28
49
|
card = find_card(name)
|
29
50
|
card&.dig("keywords") || card&.keywords || []
|
@@ -31,6 +52,11 @@ module CwCardUtils
|
|
31
52
|
[]
|
32
53
|
end
|
33
54
|
|
55
|
+
# Public: Fetch toughness for card.
|
56
|
+
# 日本語: 指定カードのタフネスを取得します。
|
57
|
+
#
|
58
|
+
# @param name [String]
|
59
|
+
# @return [String, Integer, nil]
|
34
60
|
def toughness_for_card(name)
|
35
61
|
card = find_card(name)
|
36
62
|
card&.dig("toughness") || card&.toughness
|
@@ -38,6 +64,11 @@ module CwCardUtils
|
|
38
64
|
nil
|
39
65
|
end
|
40
66
|
|
67
|
+
# Public: Fetch power for card.
|
68
|
+
# 日本語: 指定カードのパワーを取得します。
|
69
|
+
#
|
70
|
+
# @param name [String]
|
71
|
+
# @return [String, Integer, nil]
|
41
72
|
def power_for_card(name)
|
42
73
|
card = find_card(name)
|
43
74
|
card&.dig("power") || card&.power
|
@@ -45,6 +76,11 @@ module CwCardUtils
|
|
45
76
|
nil
|
46
77
|
end
|
47
78
|
|
79
|
+
# Public: Fetch oracle text for card.
|
80
|
+
# 日本語: 指定カードのオラクルテキストを取得します。
|
81
|
+
#
|
82
|
+
# @param name [String]
|
83
|
+
# @return [String, nil]
|
48
84
|
def oracle_text_for_card(name)
|
49
85
|
card = find_card(name)
|
50
86
|
card&.dig("oracle_text") || card&.oracle_text
|
@@ -52,6 +88,11 @@ module CwCardUtils
|
|
52
88
|
nil
|
53
89
|
end
|
54
90
|
|
91
|
+
# Public: Fetch type line for card (nil for Lands).
|
92
|
+
# 日本語: 指定カードのタイプ行を取得します (土地は nil)。
|
93
|
+
#
|
94
|
+
# @param name [String]
|
95
|
+
# @return [String, nil]
|
55
96
|
def type_for_card(name)
|
56
97
|
card = find_card(name)
|
57
98
|
type = card&.dig("type_line") || card&.type_line
|
@@ -62,14 +103,24 @@ module CwCardUtils
|
|
62
103
|
end
|
63
104
|
end
|
64
105
|
|
65
|
-
# Example MongoDB data source implementation
|
66
|
-
#
|
106
|
+
# Public: Example MongoDB data source implementation.
|
107
|
+
# 日本語: MongoDB を用いた例示的なデータソース実装です。
|
67
108
|
class MongoCardDataSource < CardDataSource
|
109
|
+
# Public: Initialize with a Mongo-like collection.
|
110
|
+
# 日本語: Mongo 互換コレクションで初期化します。
|
111
|
+
#
|
112
|
+
# @param collection [#find_one]
|
113
|
+
# @return [void]
|
68
114
|
def initialize(collection)
|
69
115
|
super()
|
70
116
|
@collection = collection
|
71
117
|
end
|
72
118
|
|
119
|
+
# Public: Find a card by exact name.
|
120
|
+
# 日本語: カード名の完全一致で検索します。
|
121
|
+
#
|
122
|
+
# @param name [String]
|
123
|
+
# @return [Hash, nil]
|
73
124
|
def find_card(name)
|
74
125
|
@collection.find_one({ "name" => name })
|
75
126
|
rescue StandardError
|
@@ -77,44 +128,58 @@ module CwCardUtils
|
|
77
128
|
end
|
78
129
|
end
|
79
130
|
|
80
|
-
# Represents a card with Scryfall data
|
131
|
+
# Public: Represents a card with Scryfall data.
|
132
|
+
# 日本語: Scryfall データを持つカードの表現です。
|
81
133
|
class ScryfallCard
|
134
|
+
# Public: Wrap a Scryfall-backed card object.
|
135
|
+
# 日本語: Scryfall データに基づくカードをラップします。
|
136
|
+
#
|
137
|
+
# @param name [String]
|
138
|
+
# @param data_source [CwCardUtils::CardDataSource, nil]
|
139
|
+
# @return [void]
|
82
140
|
def initialize(name, data_source = nil)
|
83
141
|
@name = name
|
84
142
|
@data = (data_source || CwCardUtils.card_data_source).find_card(@name) || {}
|
85
143
|
end
|
86
144
|
|
145
|
+
# @return [Numeric, nil]
|
87
146
|
def cmc
|
88
147
|
@data["cmc"]
|
89
148
|
end
|
90
149
|
|
150
|
+
# @return [String, nil]
|
91
151
|
def type
|
92
152
|
@data["type_line"]
|
93
153
|
end
|
94
154
|
|
155
|
+
# @return [Array<String>, nil]
|
95
156
|
def keywords
|
96
157
|
@data["keywords"]
|
97
158
|
end
|
98
159
|
|
160
|
+
# @return [String, Integer, nil]
|
99
161
|
def power
|
100
162
|
@data["power"]
|
101
163
|
end
|
102
164
|
|
165
|
+
# @return [String, Integer, nil]
|
103
166
|
def toughness
|
104
167
|
@data["toughness"]
|
105
168
|
end
|
106
169
|
|
170
|
+
# @return [String, nil]
|
107
171
|
def oracle_text
|
108
172
|
@data["oracle_text"]
|
109
173
|
end
|
110
174
|
|
175
|
+
# @return [Array<String>, nil]
|
111
176
|
def color_identity
|
112
177
|
@data["color_identity"]
|
113
178
|
end
|
114
179
|
end
|
115
180
|
|
116
|
-
#
|
117
|
-
#
|
181
|
+
# Public: Singleton accessor for Scryfall-based CMC/type data.
|
182
|
+
# 日本語: Scryfall ベースの CMC/タイプ情報へのシングルトンアクセスを提供します。
|
118
183
|
class ScryfallCmcData < CardDataSource
|
119
184
|
include Singleton
|
120
185
|
|
@@ -125,6 +190,11 @@ module CwCardUtils
|
|
125
190
|
@found_cards = {}
|
126
191
|
end
|
127
192
|
|
193
|
+
# Public: Find a card hash by name, using index or linear search.
|
194
|
+
# 日本語: インデックスまたは線形探索でカード名からデータを検索します。
|
195
|
+
#
|
196
|
+
# @param name [String]
|
197
|
+
# @return [Hash, nil]
|
128
198
|
def find_card(name)
|
129
199
|
load_data_if_needed
|
130
200
|
@card_index[name] || @found_cards[name] ||= linear_search(name)
|
@@ -132,6 +202,18 @@ module CwCardUtils
|
|
132
202
|
nil
|
133
203
|
end
|
134
204
|
|
205
|
+
# Public: Full CMC dataset as parsed JSON array.
|
206
|
+
# 日本語: 解析済みの JSON 配列データ全体を返します。
|
207
|
+
#
|
208
|
+
# How it works (EN): Lazily loads the bundled JSON once, building an
|
209
|
+
# in-memory hash index by card name for fast lookups. Subsequent
|
210
|
+
# calls reuse the cached data.
|
211
|
+
#
|
212
|
+
# 仕組み (JA): バンドルされた JSON を遅延読み込みし、カード名をキー
|
213
|
+
# にしたインメモリのハッシュインデックスを構築します。以降の呼び出し
|
214
|
+
# ではキャッシュを再利用します。
|
215
|
+
#
|
216
|
+
# @return [Array<Hash>]
|
135
217
|
def cmc_data
|
136
218
|
load_data_if_needed
|
137
219
|
@data
|
@@ -139,6 +221,8 @@ module CwCardUtils
|
|
139
221
|
|
140
222
|
private
|
141
223
|
|
224
|
+
# Internal: Lazy-load JSON and build index once.
|
225
|
+
# 日本語: JSON を遅延読み込みし、インデックスを一度だけ構築します。
|
142
226
|
def load_data_if_needed
|
143
227
|
return if @data
|
144
228
|
|
@@ -146,6 +230,8 @@ module CwCardUtils
|
|
146
230
|
build_card_index
|
147
231
|
end
|
148
232
|
|
233
|
+
# Internal: Build hash from card name to card info.
|
234
|
+
# 日本語: カード名から情報へのハッシュインデックスを作成します。
|
149
235
|
def build_card_index
|
150
236
|
@card_index = {}
|
151
237
|
@data.each do |card|
|
@@ -153,6 +239,8 @@ module CwCardUtils
|
|
153
239
|
end
|
154
240
|
end
|
155
241
|
|
242
|
+
# Internal: Linear scan fallback by exact name.
|
243
|
+
# 日本語: 完全一致名での線形探索フォールバック。
|
156
244
|
def linear_search(name)
|
157
245
|
@data.find { |card| card["name"] == name }
|
158
246
|
end
|
@@ -1,14 +1,35 @@
|
|
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
34
|
targets = Array(target_names).uniq
|
14
35
|
draws_clamped = clamp_draws(draws)
|
@@ -19,6 +40,19 @@ module CwCardUtils
|
|
19
40
|
end
|
20
41
|
|
21
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
|
22
56
|
def prob_combo(target_names, draws)
|
23
57
|
targets = Array(target_names).uniq
|
24
58
|
case targets.size
|
@@ -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
|