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
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8c4c153971824b05a57199518aa8143b81eb2b5d827484ad2a864d93423581cf
|
4
|
+
data.tar.gz: 753fb053d7348cac6b4b65dec309a95f94c98cf4340cc71a00380bf0ea650f02
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
###
|
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
|
-
#
|
99
|
+
# Common accessors
|
52
100
|
puts deck.mainboard_size # => 10
|
53
101
|
puts deck.color_identity # => ["R"]
|
54
|
-
puts deck.archetype
|
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
|
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
|
-
|
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("/")
|