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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 23145f32617794cf1af83523bf179d6d9260cc75251a24207ae6ea6ce0274599
4
- data.tar.gz: 7cd65b1d7e93b8f9c2438df235d77c44e294d543ff1f22e8b0aa16efb5e51192
3
+ metadata.gz: 8c4c153971824b05a57199518aa8143b81eb2b5d827484ad2a864d93423581cf
4
+ data.tar.gz: 753fb053d7348cac6b4b65dec309a95f94c98cf4340cc71a00380bf0ea650f02
5
5
  SHA512:
6
- metadata.gz: 556bc7d3c4dc4d87b752d2923eba061b23356a6e2330f33691e34ee215d7cdd9e6148cd8c4a9082ddbcda179ffd68bef162d0bc6e11c25db3bc05b95589f83d9
7
- data.tar.gz: e81abacff1c5dae3476a1ffb9e6a779105546f7a020edcc36fb463082842645a784e922d89660c2e779c4ceaa1d51dadf14b84ed4ae33d6326fe7188ab1135b7
6
+ metadata.gz: 2a99b206d6a6abd73a1118016d71979ead2205712da4c57eb1251a43c03d425a880fd6ff63068469cf1926a2c6b4dec1f8e6f95dc13675cc144de1a528723935
7
+ data.tar.gz: 97f303620f866c092048ffa473547e0e70ef610453b08194fc7bf30b732bd78ab043bf43118b53856e31fb5a0ccb5d1ef7e966a6f0b6d4b67777ffeee308e507
data/README.md CHANGED
@@ -2,6 +2,20 @@
2
2
 
3
3
  A Ruby gem for analyzing Magic: The Gathering decklists and calculating various metrics.
4
4
 
5
+ ## Features
6
+
7
+ - Parse decklists (MTGA/MTGO/Moxfield) into a rich `Deck` model
8
+ - Compute curves: raw, normalized, and collapsed-normalized
9
+ - Detect archetypes from tag ratios, average CMC, and color/tribe labels
10
+ - Tag cards using lightweight text/keyword heuristics (threat, interaction, ramp, synergy, tribal)
11
+ - Calculate synergy probabilities for single/pair/triple combos
12
+ - Compare two decks and produce on-play/on-draw matchup insights
13
+
14
+ ## Requirements
15
+
16
+ - Ruby 3.0+ (tested on 3.x)
17
+ - No external services required for defaults (bundled Scryfall JSON)
18
+
5
19
  ## Installation
6
20
 
7
21
  Add this line to your application's Gemfile:
@@ -16,6 +30,41 @@ And then execute:
16
30
  bundle install
17
31
  ```
18
32
 
33
+ Or install directly:
34
+
35
+ ```bash
36
+ gem install cw_card_utils
37
+ ```
38
+
39
+ ## Quickstart
40
+
41
+ ```ruby
42
+ require "cw_card_utils"
43
+
44
+ decklist_a = <<~DECK
45
+ 4 Lightning Bolt
46
+ 4 Monastery Swiftspear
47
+ 20 Mountain
48
+ DECK
49
+
50
+ decklist_b = <<~DECK
51
+ 4 Counterspell
52
+ 4 Memory Deluge
53
+ 4 Supreme Verdict
54
+ 24 Island
55
+ DECK
56
+
57
+ a = CwCardUtils::DecklistParser::Parser.new(decklist_a).parse
58
+ b = CwCardUtils::DecklistParser::Parser.new(decklist_b).parse
59
+
60
+ puts a.archetype # => e.g., "Mono-Red Aggro"
61
+ puts a.color_identity_string # => "Red"
62
+ puts a.collapsed_normalized_curve # => { "0-1"=>..., "2"=>..., ... }
63
+
64
+ cmp = CwCardUtils::DeckComparator.new(a, b)
65
+ pp cmp.compare[:on_play] # => win_rate_a, favored, notes, etc.
66
+ ```
67
+
19
68
  ## Configuration
20
69
 
21
70
  You can configure the card data source used by the library:
@@ -34,12 +83,11 @@ The default data source is `CwCardUtils::ScryfallCmcData.instance`, which loads
34
83
 
35
84
  ## Usage
36
85
 
37
- ### Basic Deck Parsing
86
+ ### Deck Parsing and Formats
38
87
 
39
88
  ```ruby
40
89
  require 'cw_card_utils'
41
90
 
42
- # Parse a decklist
43
91
  decklist = <<~DECK
44
92
  4 Lightning Bolt
45
93
  4 Mountain
@@ -48,12 +96,57 @@ DECK
48
96
 
49
97
  deck = CwCardUtils::DecklistParser::Parser.new(decklist).parse
50
98
 
51
- # Access deck information
99
+ # Common accessors
52
100
  puts deck.mainboard_size # => 10
53
101
  puts deck.color_identity # => ["R"]
54
- puts deck.archetype # => :aggro
102
+ puts deck.archetype # => "Mono-Red Aggro" (string label)
103
+ puts deck.format # => :standard, :modern, :commander (heuristic)
55
104
  ```
56
105
 
106
+ Parses common formats (MTGA, MTGO, Moxfield). Sideboard sections are detected (e.g., lines starting with "Sideboard").
107
+
108
+ ### Curves and Summaries
109
+
110
+ ```ruby
111
+ deck.curve # => { 0=>2, 1=>8, 2=>6, 3=>4, ... }
112
+ deck.normalized_curve # => { 1=>0.22, 2=>0.18, ... }
113
+ deck.collapsed_curve # => { "0-1"=>10, "2"=>6, "3"=>4, ... }
114
+ deck.collapsed_normalized_curve
115
+ ```
116
+
117
+ ### Archetype Detection and Tags
118
+
119
+ ```ruby
120
+ deck.archetype # => "Izzet Control", "Selesnya Midrange", etc.
121
+ deck.main.first.tags # => [:interaction, :draw, :etb, ...]
122
+ deck.color_identity_string# => "Azorius"
123
+ ```
124
+
125
+ ### Synergy Probabilities
126
+
127
+ ```ruby
128
+ sp = CwCardUtils::SynergyProbability.new(deck, deck_size: deck.size)
129
+ sp.prob_single(["Lightning Bolt"], 7) # => Float 0..1
130
+ sp.prob_combo(["Card A", "Card B"], 10) # => Float 0..1
131
+ ```
132
+
133
+ ### Deck Comparison (Matchup)
134
+
135
+ ```ruby
136
+ cmp = CwCardUtils::DeckComparator.new(deck_a, deck_b)
137
+ pp cmp.compare # => { on_play: {...}, on_draw: {...} }
138
+ ```
139
+
140
+ ### Generating API Docs (RDoc)
141
+
142
+ Bilingual (EN/JA) RDoc is embedded in the source. Generate HTML docs:
143
+
144
+ ```bash
145
+ rdoc -f darkfish -o doc lib README.md --title "Crackling Wit: Card Utilities" --exclude '\.json$'
146
+ ```
147
+
148
+ Then open `doc/index.html`.
149
+
57
150
  ### Custom Data Sources
58
151
 
59
152
  You can implement your own card data source by inheriting from `CwCardUtils::CardDataSource`:
@@ -76,4 +169,19 @@ After checking out the repo, run `bin/setup` to install dependencies. Then, run
76
169
 
77
170
  ## Contributing
78
171
 
79
- Bug reports and pull requests are welcome on GitHub at https://github.com/cracklingwit/cw_card_utils.
172
+ Bug reports and pull requests are welcome on GitHub at https://github.com/cracklingwit/cw_card_utils.
173
+
174
+ ## Data Sources
175
+
176
+ The default data source for this gem is an extraction from the wonderful [Scryfall Bulk Data][1] set. It is intended to be used only to test the functionality of the gem and serve as a fallback source of data.
177
+
178
+ You should use your own source, preferably backed by a database that conforms to our `card_data_source` API for your own production projects.
179
+
180
+ A huge, huge thank you to the wonderful folks at [Scryfall][2] for the hard work they put into keeping their data accurate, up to date, and free for the community to use. We are not endorsed or supported by [Scryfall][2] in any way.
181
+
182
+ ## Fan Content Policy
183
+
184
+ Crackling Wit's Card Utils gem is unofficial Fan Content permitted under the Fan Content Policy. It is not approved/endorsed by Wizards. Portions of the materials used are property of Wizards of the Coast. © Wizards of the Coast LLC.
185
+
186
+ [1]: https://scryfall.com/docs/api/bulk-data
187
+ [2]: https://scryfall.com
@@ -1,7 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module CwCardUtils
4
+ # Public: Computes mana curve distributions from a deck.
5
+ # 日本語: デッキからマナカーブ分布を計算します。
4
6
  class CurveCalculator
7
+ # Public: Initialize with a deck-like object.
8
+ # 日本語: デッキ相当のオブジェクトで初期化します。
9
+ #
10
+ # @param deck [CwCardUtils::DecklistParser::Deck]
5
11
  def initialize(deck)
6
12
  @deck = deck
7
13
  @deck_size = @deck.count_without_lands
@@ -11,6 +17,18 @@ module CwCardUtils
11
17
  @collapsed_curve = {}
12
18
  end
13
19
 
20
+ # Public: Collapsed curve counts (0-1,2,3,4,5,6+).
21
+ # 日本語: まとめられたカーブ分布(0-1,2,3,4,5,6+)。
22
+ #
23
+ # How it works (EN): Builds upon the raw curve, bucketing CMCs into
24
+ # coarse buckets for quick, human-readable summaries. Lands are
25
+ # excluded, and nil/zero CMC non-lands are treated as 0.
26
+ #
27
+ # 仕組み (JA): 素のカーブを基に、CMC を粗い区分へまとめて
28
+ # 人が読みやすい集計を生成します。土地は除外し、nil/0 の CMC
29
+ # (土地でない) は 0 として扱います。
30
+ #
31
+ # @return [Hash{String=>Integer}]
14
32
  def collapsed_curve
15
33
  return @collapsed_curve if @collapsed_curve.values.any?
16
34
 
@@ -37,16 +55,48 @@ module CwCardUtils
37
55
  @collapsed_curve
38
56
  end
39
57
 
58
+ # Public: Raw curve counts keyed by CMC bucket.
59
+ # 日本語: CMCバケットごとの素のカウント。
60
+ #
61
+ # How it works (EN): Iterates over non-land cards and increments a
62
+ # bucket equal to ceil(CMC). Cards with nil/0 CMC and non-land type
63
+ # are assigned to bucket 0; lands are skipped entirely.
64
+ #
65
+ # 仕組み (JA): 土地以外のカードを走査し、ceil(CMC) をバケットに
66
+ # 加算します。CMC が nil/0 で土地でない場合は 0 バケットへ。
67
+ # 土地はスキップします。
68
+ #
69
+ # @return [Hash{Integer=>Integer}]
40
70
  def curve
41
71
  calculate_curve if @raw_curve.empty?
42
72
  @raw_curve
43
73
  end
44
74
 
75
+ # Public: Normalized curve (fractions of non-land count).
76
+ # 日本語: 正規化されたカーブ (土地を除く総数に対する割合)。
77
+ #
78
+ # How it works (EN): Sorts buckets by CMC and divides each count by
79
+ # the number of non-land cards, rounding to 4 decimals.
80
+ #
81
+ # 仕組み (JA): CMC 順にソートし、各バケットを土地以外の総枚数で
82
+ # 割って 4 桁に丸めます。
83
+ #
84
+ # @return [Hash{Integer=>Float}]
45
85
  def normalized_curve
46
86
  normalize_curve if @normalized_curve.values.empty?
47
87
  @normalized_curve
48
88
  end
49
89
 
90
+ # Public: Collapsed and normalized curve distribution.
91
+ # 日本語: まとめかつ正規化したカーブ分布。
92
+ #
93
+ # How it works (EN): Applies the same normalization as above to the
94
+ # collapsed buckets, providing an at-a-glance shape of the deck.
95
+ #
96
+ # 仕組み (JA): まとめたバケットに対して同様の正規化を行い、
97
+ # デッキの形状を直感的に把握できるようにします。
98
+ #
99
+ # @return [Hash{String=>Float}]
50
100
  def collapsed_normalized_curve
51
101
  normalize_collapsed_curve if @collapsed_normalized_curve.values.empty?
52
102
  @collapsed_normalized_curve
@@ -1,5 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ ##
4
+ # Public: Compares two decks and produces matchup insights for play/draw.
5
+ # 日本語: 2つのデッキを比較し、先手/後手のマッチアップ分析を生成します。
3
6
  module CwCardUtils
4
7
  class DeckComparator
5
8
  attr_reader :deck_a, :deck_b, :analysis
@@ -11,12 +14,32 @@ module CwCardUtils
11
14
  interaction: 0.20,
12
15
  }.freeze
13
16
 
17
+ # Public: Create a comparator for two decks.
18
+ # 日本語: 2つのデッキを対象にコンパレータを作成します。
19
+ #
20
+ # @param deck_a [CwCardUtils::DecklistParser::Deck] 比較対象Aのデッキ
21
+ # @param deck_b [CwCardUtils::DecklistParser::Deck] 比較対象Bのデッキ
22
+ # @return [void]
14
23
  def initialize(deck_a, deck_b)
15
24
  @deck_a = deck_a
16
25
  @deck_b = deck_b
17
26
  @analysis = {}
18
27
  end
19
28
 
29
+ # Public: Compare decks for on-play and on-draw scenarios.
30
+ # 日本語: 先手/後手の両シナリオで比較します。
31
+ #
32
+ # Intent (EN): Produces two analyses (on the play / on the draw)
33
+ # combining archetype, curve segmentation, synergy hit-rate, and
34
+ # interaction density. A weighted heuristic yields Deck A's win
35
+ # rate and a favored label, plus explanatory notes.
36
+ #
37
+ # 意図 (JA): アーキタイプ、カーブ分割、シナジー成立率、
38
+ # インタラクション密度を統合し、先手/後手の 2 通りの分析を
39
+ # 生成します。重み付きヒューリスティックによりデッキAの勝率と
40
+ # 有利側のラベルを算出し、説明的なノートを付与します。
41
+ #
42
+ # @return [Hash] { on_play: Hash, on_draw: Hash }
20
43
  def compare
21
44
  results = {
22
45
  on_play: matchup_scenario(on_play: true),
@@ -27,6 +50,8 @@ module CwCardUtils
27
50
 
28
51
  private
29
52
 
53
+ # Internal: Build a single scenario result.
54
+ # 日本語: 単一シナリオの結果を作成します。
30
55
  def matchup_scenario(on_play:)
31
56
  a_info = analyze_deck(deck_a, on_play: on_play)
32
57
  b_info = analyze_deck(deck_b, on_play: !on_play)
@@ -51,6 +76,10 @@ module CwCardUtils
51
76
  }
52
77
  end
53
78
 
79
+ # Internal: Extract features used by the comparator from a deck.
80
+ # 日本語: デッキから比較用の特徴量を抽出します。
81
+ #
82
+ # @return [Hash]
54
83
  def analyze_deck(deck, on_play:)
55
84
  detector = CwCardUtils::DecklistParser::ArchetypeDetector.new(deck)
56
85
  archetype = detector.detect
@@ -76,6 +105,8 @@ module CwCardUtils
76
105
  }
77
106
  end
78
107
 
108
+ # Internal: Average combo hit-rate by turn 5 given play/draw.
109
+ # 日本語: 先手/後手を考慮した5ターン目までのシナジー成立率の平均。
79
110
  def calc_synergy_hit_rate(deck, on_play:)
80
111
  synergy_pairs = extract_synergy_pairs(deck)
81
112
  return 0 if synergy_pairs.empty?
@@ -88,6 +119,8 @@ module CwCardUtils
88
119
  (probs.sum / probs.size).round(2)
89
120
  end
90
121
 
122
+ # Internal: Return unordered pairs of synergy-tagged cards.
123
+ # 日本語: シナジー系タグのカード名ペアを返します。
91
124
  def extract_synergy_pairs(deck)
92
125
  synergy_cards = deck.main.select do |card|
93
126
  card.tags.intersect?([:synergistic_finisher, :tribal_synergy, :scaling_threat])
@@ -96,10 +129,14 @@ module CwCardUtils
96
129
  synergy_cards.map(&:name).combination(2).to_a
97
130
  end
98
131
 
132
+ # Internal: Cards seen by specified turn.
133
+ # 日本語: 指定ターンまでに見られるカード枚数。
99
134
  def draws_by_turn(turn, on_play:)
100
135
  on_play ? (7 + (turn - 1)) : (7 + turn)
101
136
  end
102
137
 
138
+ # Internal: Combine sub-scores into a single win-rate for Deck A.
139
+ # 日本語: 複数の部分スコアを統合し、デッキAの勝率を返します。
103
140
  def weighted_score(a, b)
104
141
  # Archetype advantage
105
142
  archetype_score = case predict_matchup(a, b)
@@ -143,6 +180,8 @@ module CwCardUtils
143
180
  ).round(3)
144
181
  end
145
182
 
183
+ # Internal: Heuristic label for who is favored and why.
184
+ # 日本語: どちらが有利かのヒューリスティックな説明を返します。
146
185
  def predict_matchup(a, b)
147
186
  return "Deck A (Aggro)" if is_aggro?(a) && is_control?(b)
148
187
  return "Deck B (Aggro)" if is_aggro?(b) && is_control?(a)
@@ -153,14 +192,20 @@ module CwCardUtils
153
192
  "Even Matchup"
154
193
  end
155
194
 
195
+ # Internal: Aggro heuristic.
196
+ # 日本語: アグロ判定のヒューリスティック。
156
197
  def is_aggro?(info)
157
198
  info[:archetype].downcase.include?("aggro") || info[:early_curve] > 0.5
158
199
  end
159
200
 
201
+ # Internal: Control heuristic.
202
+ # 日本語: コントロール判定のヒューリスティック。
160
203
  def is_control?(info)
161
204
  info[:archetype].downcase.include?("control") || info[:late_curve] > 0.4
162
205
  end
163
206
 
207
+ # Internal: Compare interaction tag density.
208
+ # 日本語: インタラクションの密度を比較します。
164
209
  def compare_interaction_density(a, b)
165
210
  a_interact = a[:tag_ratios][:interaction].to_f.round(2)
166
211
  b_interact = b[:tag_ratios][:interaction].to_f.round(2)
@@ -174,6 +219,8 @@ module CwCardUtils
174
219
  end
175
220
  end
176
221
 
222
+ # Internal: Produce human-readable notes for a scenario.
223
+ # 日本語: シナリオごとの人間に読みやすい注釈を生成します。
177
224
  def generate_notes(a, b, on_play:)
178
225
  notes = []
179
226
  notes << (on_play ? "Deck A is on the play" : "Deck B is on the play")
@@ -2,9 +2,16 @@
2
2
 
3
3
  module CwCardUtils
4
4
  module DecklistParser
5
+ # Public: Detects deck archetypes and color/tribe labels.
6
+ # 日本語: デッキのアーキタイプと色/部族ラベルを検出します。
5
7
  class ArchetypeDetector
6
8
  attr_reader :deck, :format, :tribe, :colors, :tag_counts, :tag_ratios
7
9
 
10
+ # Public: Initialize with a deck.
11
+ # 日本語: デッキで初期化します。
12
+ #
13
+ # @param deck [CwCardUtils::DecklistParser::Deck]
14
+ # @return [void]
8
15
  def initialize(deck)
9
16
  @deck = deck
10
17
  @format = deck.format.to_s.downcase # :standard, :edh, etc.
@@ -16,6 +23,21 @@ module CwCardUtils
16
23
  calculate_ratios
17
24
  end
18
25
 
26
+ # Public: Full label, e.g., "Azorius Cat Tribal Midrange".
27
+ # 日本語: 例 "Azorius Cat Tribal Midrange" のような完全ラベルを返します。
28
+ #
29
+ # Intent (EN): Converts detected color identity, dominant tribe, and
30
+ # archetype into a concise human-readable label. Color comes from
31
+ # identity resolution, tribe from majority subtype, and archetype
32
+ # from heuristics over tag counts/ratios and average CMC.
33
+ #
34
+ # 意図 (JA): 検出した色アイデンティティ、優勢トライブ、
35
+ # アーキタイプを人が読みやすい短いラベルへ変換します。色は
36
+ # アイデンティティ解決、トライブは多数派サブタイプ、
37
+ # アーキタイプはタグ頻度/比率と平均 CMC のヒューリスティックから
38
+ # 決定します。
39
+ #
40
+ # @return [String]
19
41
  def detect
20
42
  archetype = detect_archetype
21
43
  color_label = resolve_color_label(colors)
@@ -30,6 +52,8 @@ module CwCardUtils
30
52
 
31
53
  # === TAG COUNTING ===
32
54
 
55
+ # Internal: Count tag frequencies across the deck.
56
+ # 日本語: デッキ全体のタグ出現数をカウントします。
33
57
  def count_tags
34
58
  total_tags = 0
35
59
 
@@ -45,6 +69,8 @@ module CwCardUtils
45
69
  @tag_counts[:_total] = total_tags
46
70
  end
47
71
 
72
+ # Internal: Convert counts to ratios.
73
+ # 日本語: カウントを比率へ変換します。
48
74
  def calculate_ratios
49
75
  total = @tag_counts[:_total].to_f
50
76
  return if total.zero?
@@ -57,6 +83,8 @@ module CwCardUtils
57
83
 
58
84
  # === CMC UTILITY ===
59
85
 
86
+ # Internal: Average CMC across mainboard (lands excluded by caller logic).
87
+ # 日本語: メインボードの平均 CMC。
60
88
  def average_cmc
61
89
  @average_cmc ||= begin
62
90
  cmcs = deck.main.map { |card| card.respond_to?(:cmc) ? card.cmc.to_f : nil }.compact
@@ -66,6 +94,8 @@ module CwCardUtils
66
94
  end
67
95
 
68
96
  # === FORMAT-AWARE DETECTION ===
97
+ # Internal: Heuristic archetype classification.
98
+ # 日本語: アーキタイプのヒューリスティック分類。
69
99
  def detect_archetype
70
100
  if format == "edh"
71
101
  return :combo if tag_ratios[:combo_piece].to_f > 0.10 && tag_ratios[:draw].to_f > 0.08
@@ -105,10 +135,14 @@ module CwCardUtils
105
135
 
106
136
  # === COLOR IDENTITY ===
107
137
 
138
+ # Internal: Compute unique color identity from deck.
139
+ # 日本語: デッキから色アイデンティティを抽出します。
108
140
  def resolve_color_identity(deck)
109
141
  deck.main.flat_map { |c| c.respond_to?(:color_identity) ? c.color_identity : [] }.uniq.sort
110
142
  end
111
143
 
144
+ # Internal: Render color identity into label.
145
+ # 日本語: 色アイデンティティをラベルへ変換します。
112
146
  def resolve_color_label(identity)
113
147
  ColorIdentityResolver.resolve(identity)
114
148
  end
@@ -2,8 +2,16 @@
2
2
 
3
3
  module CwCardUtils
4
4
  module DecklistParser
5
- # A Card is a single card in a deck.
5
+ # Public: A single card entry in a deck.
6
+ # 日本語: デッキ内の1枚のカードを表します。
6
7
  class Card
8
+ # Public: Create a card entry.
9
+ # 日本語: カードエントリを作成します。
10
+ #
11
+ # @param name [String]
12
+ # @param count [Integer]
13
+ # @param cmc_data_source [CwCardUtils::CardDataSource, nil]
14
+ # @return [void]
7
15
  def initialize(name, count, cmc_data_source = nil)
8
16
  @name = name
9
17
  @count = count
@@ -11,45 +19,102 @@ module CwCardUtils
11
19
  @cmc_data_source = cmc_data_source || CwCardUtils.card_data_source
12
20
  end
13
21
 
14
- attr_reader :name, :count, :cmc_data_source
22
+ # Public: Card name.
23
+ # 日本語: カード名。
24
+ # @return [String]
25
+ attr_reader :name
26
+
27
+ # Public: Number of copies in the deck.
28
+ # 日本語: デッキ内の枚数。
29
+ # @return [Integer]
30
+ attr_reader :count
31
+
32
+ # Public: Data source used for lookup.
33
+ # 日本語: 参照に用いるデータソース。
34
+ # @return [CwCardUtils::CardDataSource, nil]
35
+ attr_reader :cmc_data_source
36
+
37
+ # Public: Tag symbols inferred for the card.
38
+ # 日本語: そのカードに付与されたタグ一覧。
39
+ # @return [Array<Symbol>]
15
40
  attr_accessor :tags
16
41
 
42
+ # Public: Converted mana cost from the data source.
43
+ # 日本語: データソースから取得した点数で見たマナ・コスト。
44
+ #
45
+ # @return [Numeric, nil]
17
46
  def cmc
18
47
  @cmc ||= @cmc_data_source&.cmc_for_card(@name)
19
48
  end
20
49
 
50
+ # Public: Type line from the data source.
51
+ # 日本語: データソースから取得したタイプ行。
52
+ #
53
+ # @return [String]
21
54
  def type
22
55
  @type ||= @cmc_data_source&.type_for_card(@name) || "Land"
23
56
  end
24
57
 
58
+ # Public: Keywords from the data source.
59
+ # 日本語: データソースから取得したキーワード。
60
+ #
61
+ # @return [Array<String>]
25
62
  def keywords
26
63
  @keywords ||= @cmc_data_source&.keywords_for_card(@name) || []
27
64
  end
28
65
 
66
+ # Public: Oracle text from the data source.
67
+ # 日本語: データソースから取得したオラクルテキスト。
68
+ #
69
+ # @return [String, nil]
29
70
  def oracle_text
30
71
  @oracle_text ||= @cmc_data_source&.oracle_text_for_card(@name)
31
72
  end
32
73
 
74
+ # Public: Power from the data source.
75
+ # 日本語: データソースから取得したパワー。
76
+ #
77
+ # @return [String, Integer, nil]
33
78
  def power
34
79
  @power ||= @cmc_data_source&.power_for_card(@name)
35
80
  end
36
81
 
82
+ # Public: Toughness from the data source.
83
+ # 日本語: データソースから取得したタフネス。
84
+ #
85
+ # @return [String, Integer, nil]
37
86
  def toughness
38
87
  @toughness ||= @cmc_data_source&.toughness_for_card(@name)
39
88
  end
40
89
 
90
+ # Public: Color identity array from the data source.
91
+ # 日本語: データソースから取得した色アイデンティティ配列。
92
+ #
93
+ # @return [Array<String>]
41
94
  def color_identity
42
95
  @color_identity ||= @cmc_data_source&.color_identity_for_card(@name) || []
43
96
  end
44
97
 
98
+ # Public: Human-readable summary.
99
+ # 日本語: 人間が読みやすい概要。
100
+ #
101
+ # @return [String]
45
102
  def inspect
46
103
  "<Card: #{@name} (#{@count}) #{cmc}>"
47
104
  end
48
105
 
106
+ # Public: Serialize to a Hash.
107
+ # 日本語: Hash へシリアライズします。
108
+ #
109
+ # @return [Hash]
49
110
  def to_h
50
111
  { name: @name, count: @count, cmc: cmc, type: type, keywords: keywords, power: power, toughness: toughness, oracle_text: oracle_text }
51
112
  end
52
113
 
114
+ # Public: JSON representation.
115
+ # 日本語: JSON 文字列を返します。
116
+ #
117
+ # @return [String]
53
118
  def to_json(*_args)
54
119
  to_h.to_json
55
120
  end
@@ -3,7 +3,15 @@
3
3
  module CwCardUtils
4
4
  module DecklistParser
5
5
 
6
+ # Public: Derives semantic tags from card text/type/keywords.
7
+ # 日本語: カードのテキスト/タイプ/キーワードから意味的なタグを導出します。
6
8
  class CardTagger
9
+ # Public: Initialize a tagger for a card within a deck context.
10
+ # 日本語: デッキ文脈でのカード用タグ付け器を初期化します。
11
+ #
12
+ # @param card [CwCardUtils::DecklistParser::Card]
13
+ # @param deck [CwCardUtils::DecklistParser::Deck]
14
+ # @return [void]
7
15
  def initialize(card, deck)
8
16
  @card = card
9
17
  @text = card.oracle_text.to_s.downcase
@@ -24,6 +32,22 @@ module CwCardUtils
24
32
  @tribe = deck.tribe&.to_s&.downcase
25
33
  end
26
34
 
35
+ # Public: Compute tags for a card in the context of a deck.
36
+ # 日本語: デッキ文脈でカードのタグを算出します。
37
+ #
38
+ # How it works (EN): Applies lightweight NLP-style regex heuristics
39
+ # over oracle text, type line, and keywords. Recognizes role tags
40
+ # such as :threat, :interaction, :ramp, and synergy/tribal patterns
41
+ # (e.g., token/ETB triggers, scaling effects, chosen type). Uses
42
+ # deck context (tribe) to infer tribal roles and finishers.
43
+ #
44
+ # 仕組み (JA): オラクルテキスト、タイプ行、キーワードへ簡易な
45
+ # 正規表現ベースのヒューリスティックを適用します。:threat、
46
+ # :interaction、:ramp などの役割タグや、トークン/ETB、スケール系、
47
+ # 選択タイプなどのシナジー・部族パターンを検出します。デッキの
48
+ # 文脈 (トライブ) を用いて部族ロールやフィニッシャーも推論します。
49
+ #
50
+ # @return [Array<Symbol>]
27
51
  def tags
28
52
  t = []
29
53
 
@@ -116,20 +140,28 @@ module CwCardUtils
116
140
 
117
141
  private
118
142
 
143
+ # Internal: Whether the card is a creature.
144
+ # 日本語: そのカードがクリーチャーかどうか。
119
145
  def creature?
120
146
  @type_line.include?("creature")
121
147
  end
122
148
 
149
+ # Internal: Whether the card is a planeswalker.
150
+ # 日本語: そのカードがプレインズウォーカーかどうか。
123
151
  def planeswalker?
124
152
  @type_line.include?("planeswalker")
125
153
  end
126
154
 
155
+ # Internal: Extract creature subtypes as array.
156
+ # 日本語: クリーチャーのサブタイプを配列として抽出します。
127
157
  def creature_subtypes
128
158
  return [] unless creature?
129
159
  raw = @type_line.split(/[—-]/).last.to_s.strip
130
160
  raw.split.map(&:downcase)
131
161
  end
132
162
 
163
+ # Internal: Heuristic for finisher classification.
164
+ # 日本語: フィニッシャー判定のヒューリスティック。
133
165
  def likely_finisher?
134
166
  return true if @cmc >= 6 && @text.match?(/flying|trample|indestructible|each opponent|you win|extra turn|double/i)
135
167
  return true if planeswalker? && @text.match?(/creatures.*get.*flying|you get an emblem|each opponent/i)
@@ -2,6 +2,8 @@
2
2
 
3
3
  module CwCardUtils
4
4
  module DecklistParser
5
+ # Public: Convert color identity arrays to human-readable labels.
6
+ # 日本語: 色アイデンティティの配列をわかりやすい名称へ変換します。
5
7
  class ColorIdentityResolver
6
8
  COLOR_NAMES = {
7
9
  %w[W] => "White",
@@ -42,6 +44,11 @@ module CwCardUtils
42
44
  [] => "Colorless",
43
45
  }.freeze
44
46
 
47
+ # Public: Resolve a color identity array to a label.
48
+ # 日本語: 色アイデンティティ配列をラベルへ変換します。
49
+ #
50
+ # @param identity [Array<String>] like ["U", "W"]
51
+ # @return [String]
45
52
  def self.resolve(identity)
46
53
  key = identity.sort
47
54
  COLOR_NAMES[key] || key.join("/")