draftomen 0.3.0__py3-none-any.whl
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.
- draftomen/__init__.py +17 -0
- draftomen/assets/draftomen.icns +0 -0
- draftomen/assets/draftomen.ico +0 -0
- draftomen/assets/draftomen_logo.png +0 -0
- draftomen/audit.py +606 -0
- draftomen/backtest.py +449 -0
- draftomen/benchmark.py +834 -0
- draftomen/carddb.py +1869 -0
- draftomen/cardimages.py +313 -0
- draftomen/cli.py +891 -0
- draftomen/config.py +133 -0
- draftomen/deckbuilder.py +2723 -0
- draftomen/events.py +772 -0
- draftomen/logfollow.py +451 -0
- draftomen/mock_session.py +745 -0
- draftomen/paths.py +120 -0
- draftomen/pickengine.py +1117 -0
- draftomen/pool.py +1259 -0
- draftomen/preferences.py +289 -0
- draftomen/qml/AboutDialog.qml +156 -0
- draftomen/qml/AppBar.qml +120 -0
- draftomen/qml/BacktestView.qml +316 -0
- draftomen/qml/BuildView.qml +1151 -0
- draftomen/qml/CardPreview.qml +434 -0
- draftomen/qml/DimensionalButton.qml +42 -0
- draftomen/qml/DimensionalComboBox.qml +182 -0
- draftomen/qml/DimensionalSurface.qml +118 -0
- draftomen/qml/DimensionalTabButton.qml +41 -0
- draftomen/qml/LiveDraftView.qml +468 -0
- draftomen/qml/Main.qml +175 -0
- draftomen/qml/NavigationRail.qml +226 -0
- draftomen/qml/PoolSummaryPanel.qml +322 -0
- draftomen/qml/PrivacyDialog.qml +96 -0
- draftomen/qml/RecentPickThumbnail.qml +83 -0
- draftomen/qml/RecentPicksGallery.qml +333 -0
- draftomen/qml/RecommendationRow.qml +404 -0
- draftomen/qml/SettingsSwitch.qml +141 -0
- draftomen/qml/SettingsView.qml +531 -0
- draftomen/qml/StateBanner.qml +189 -0
- draftomen/qml/StatusStrip.qml +84 -0
- draftomen/qml/Theme.qml +71 -0
- draftomen/qml/qmldir +23 -0
- draftomen/qt_adapter.py +884 -0
- draftomen/qt_gui.py +338 -0
- draftomen/qt_mock.py +63 -0
- draftomen/ranking.py +126 -0
- draftomen/replay.py +480 -0
- draftomen/session.py +3668 -0
- draftomen/setinfo.py +26 -0
- draftomen/seventeen.py +2766 -0
- draftomen/splash.py +617 -0
- draftomen/tui.py +4007 -0
- draftomen/watch.py +336 -0
- draftomen-0.3.0.dist-info/METADATA +156 -0
- draftomen-0.3.0.dist-info/RECORD +59 -0
- draftomen-0.3.0.dist-info/WHEEL +5 -0
- draftomen-0.3.0.dist-info/entry_points.txt +6 -0
- draftomen-0.3.0.dist-info/licenses/LICENSE +21 -0
- draftomen-0.3.0.dist-info/top_level.txt +1 -0
draftomen/splash.py
ADDED
|
@@ -0,0 +1,617 @@
|
|
|
1
|
+
"""Shared splash eligibility and mana-support assessment.
|
|
2
|
+
Keep third-color decisions separate from primary-pair commitment.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import re
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
|
|
10
|
+
from draftomen.carddb import CardDatabase, CardInfo
|
|
11
|
+
from draftomen.config import SPLASH, SplashConfig
|
|
12
|
+
from draftomen.seventeen import GRADE_LABELS, SeventeenLandsData
|
|
13
|
+
|
|
14
|
+
COLOR_ORDER = ("W", "U", "B", "R", "G")
|
|
15
|
+
MANA_SYMBOL_PATTERN = re.compile(r"\{([^}]+)\}")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass(frozen=True, slots=True)
|
|
19
|
+
class SplashState:
|
|
20
|
+
"""Current one-color splash plan inferred from the drafted pool.
|
|
21
|
+
Fixing counts include deterministic fixing lands usable by the base pair.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
enabled: bool
|
|
25
|
+
base_pair: str | None
|
|
26
|
+
active_color: str | None
|
|
27
|
+
picked_card_count: int
|
|
28
|
+
fixing_sources: tuple[tuple[str, int], ...]
|
|
29
|
+
aggressive: bool
|
|
30
|
+
|
|
31
|
+
def fixing_for(self, *, color: str) -> int:
|
|
32
|
+
return dict(self.fixing_sources).get(color, 0)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass(frozen=True, slots=True)
|
|
36
|
+
class SplashAssessment:
|
|
37
|
+
"""Explain whether and why one offered card participates in a splash.
|
|
38
|
+
Every field is serializable so live audit records preserve the decision.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
classification: str
|
|
42
|
+
splash_color: str | None
|
|
43
|
+
off_color_pips: int
|
|
44
|
+
picked_card_count: int
|
|
45
|
+
fixing_sources: int
|
|
46
|
+
planned_basic_sources: int
|
|
47
|
+
available_sources: int
|
|
48
|
+
required_sources: int
|
|
49
|
+
grade: str | None
|
|
50
|
+
score_advantage: float | None
|
|
51
|
+
aggressive: bool
|
|
52
|
+
reasons: tuple[str, ...]
|
|
53
|
+
|
|
54
|
+
@property
|
|
55
|
+
def is_splash_candidate(self) -> bool:
|
|
56
|
+
return self.classification in {"splash-ready", "splash-speculative"}
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def infer_splash_state(
|
|
60
|
+
*,
|
|
61
|
+
pool_grp_ids: tuple[int, ...],
|
|
62
|
+
card_database: CardDatabase,
|
|
63
|
+
ratings_data: SeventeenLandsData | None,
|
|
64
|
+
base_pair: str | None,
|
|
65
|
+
enabled: bool,
|
|
66
|
+
config: SplashConfig = SPLASH,
|
|
67
|
+
) -> SplashState:
|
|
68
|
+
"""Infer the active splash color and deterministic fixing from the pool.
|
|
69
|
+
An isolated elite third-color card establishes the plan after pair commitment.
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
fixing_sources = _fixing_counts(
|
|
73
|
+
pool_grp_ids=pool_grp_ids,
|
|
74
|
+
card_database=card_database,
|
|
75
|
+
base_pair=base_pair,
|
|
76
|
+
)
|
|
77
|
+
if not enabled or base_pair is None:
|
|
78
|
+
return SplashState(
|
|
79
|
+
enabled=enabled,
|
|
80
|
+
base_pair=base_pair,
|
|
81
|
+
active_color=None,
|
|
82
|
+
picked_card_count=0,
|
|
83
|
+
fixing_sources=tuple(fixing_sources.items()),
|
|
84
|
+
aggressive=False,
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
eligible_by_color: dict[str, list[tuple[float, int]]] = {
|
|
88
|
+
color: [] for color in COLOR_ORDER if color not in base_pair
|
|
89
|
+
}
|
|
90
|
+
for index, grp_id in enumerate(pool_grp_ids):
|
|
91
|
+
card = card_database.lookup(grp_id=grp_id)
|
|
92
|
+
splash_color, off_color_pips = splash_requirement(card=card, base_pair=base_pair)
|
|
93
|
+
if splash_color is None or off_color_pips > config.maximum_off_color_pips:
|
|
94
|
+
continue
|
|
95
|
+
|
|
96
|
+
grade = _global_grade(ratings_data=ratings_data, grp_id=grp_id)
|
|
97
|
+
if not grade_at_least(grade=grade, minimum=config.supported_minimum_grade):
|
|
98
|
+
continue
|
|
99
|
+
|
|
100
|
+
win_rate = _global_win_rate(ratings_data=ratings_data, grp_id=grp_id)
|
|
101
|
+
eligible_by_color[splash_color].append(
|
|
102
|
+
(0.0 if win_rate is None else win_rate, -index)
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
active_color = _active_splash_color(
|
|
106
|
+
eligible_by_color=eligible_by_color,
|
|
107
|
+
fixing_sources=fixing_sources,
|
|
108
|
+
config=config,
|
|
109
|
+
)
|
|
110
|
+
picked_card_count = (
|
|
111
|
+
0 if active_color is None else len(eligible_by_color[active_color])
|
|
112
|
+
)
|
|
113
|
+
return SplashState(
|
|
114
|
+
enabled=True,
|
|
115
|
+
base_pair=base_pair,
|
|
116
|
+
active_color=active_color,
|
|
117
|
+
picked_card_count=picked_card_count,
|
|
118
|
+
fixing_sources=tuple(fixing_sources.items()),
|
|
119
|
+
aggressive=_pool_is_aggressive(
|
|
120
|
+
pool_grp_ids=pool_grp_ids,
|
|
121
|
+
card_database=card_database,
|
|
122
|
+
base_pair=base_pair,
|
|
123
|
+
config=config,
|
|
124
|
+
),
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def assess_splash_card(
|
|
129
|
+
*,
|
|
130
|
+
card: CardInfo,
|
|
131
|
+
grade: str | None,
|
|
132
|
+
base_score: float,
|
|
133
|
+
best_on_color_score: float | None,
|
|
134
|
+
locked: bool,
|
|
135
|
+
state: SplashState,
|
|
136
|
+
config: SplashConfig = SPLASH,
|
|
137
|
+
) -> SplashAssessment:
|
|
138
|
+
"""Classify one card as ready, speculative, fixing, or ordinary off-color.
|
|
139
|
+
Hard eligibility gates run before the smaller scoring multipliers.
|
|
140
|
+
"""
|
|
141
|
+
|
|
142
|
+
if card.unknown:
|
|
143
|
+
return _assessment(
|
|
144
|
+
classification="unknown",
|
|
145
|
+
state=state,
|
|
146
|
+
grade=grade,
|
|
147
|
+
reasons=("card metadata is unavailable",),
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
base_pair = state.base_pair
|
|
151
|
+
if base_pair is None:
|
|
152
|
+
return _assessment(
|
|
153
|
+
classification="colorless" if not card.colors else "open",
|
|
154
|
+
state=state,
|
|
155
|
+
grade=grade,
|
|
156
|
+
reasons=(
|
|
157
|
+
("card is colorless",)
|
|
158
|
+
if not card.colors
|
|
159
|
+
else ("primary colors are still open",)
|
|
160
|
+
),
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
if _is_splash_fixer(card=card, state=state, config=config):
|
|
164
|
+
active_color = state.active_color
|
|
165
|
+
fixing_sources = (
|
|
166
|
+
0
|
|
167
|
+
if active_color is None
|
|
168
|
+
else state.fixing_for(color=active_color) + 1
|
|
169
|
+
)
|
|
170
|
+
required_sources = _required_sources(
|
|
171
|
+
splash_card_count=max(1, state.picked_card_count),
|
|
172
|
+
config=config,
|
|
173
|
+
)
|
|
174
|
+
return _assessment(
|
|
175
|
+
classification="splash-fixer",
|
|
176
|
+
state=state,
|
|
177
|
+
splash_color=active_color,
|
|
178
|
+
grade=grade,
|
|
179
|
+
fixing_sources=fixing_sources,
|
|
180
|
+
planned_basic_sources=min(
|
|
181
|
+
config.planned_basic_sources,
|
|
182
|
+
required_sources,
|
|
183
|
+
),
|
|
184
|
+
required_sources=required_sources,
|
|
185
|
+
reasons=("produces the active splash color",),
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
if not card.colors:
|
|
189
|
+
return _assessment(
|
|
190
|
+
classification="colorless",
|
|
191
|
+
state=state,
|
|
192
|
+
grade=grade,
|
|
193
|
+
reasons=("card is colorless",),
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
if card_is_castable_in_pair(card=card, base_pair=base_pair):
|
|
197
|
+
return _assessment(
|
|
198
|
+
classification="on-color",
|
|
199
|
+
state=state,
|
|
200
|
+
grade=grade,
|
|
201
|
+
reasons=("the card is castable with the primary pair",),
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
splash_color, off_color_pips = splash_requirement(
|
|
205
|
+
card=card,
|
|
206
|
+
base_pair=base_pair,
|
|
207
|
+
)
|
|
208
|
+
if not state.enabled:
|
|
209
|
+
return _assessment(
|
|
210
|
+
classification="off-color",
|
|
211
|
+
state=state,
|
|
212
|
+
splash_color=splash_color,
|
|
213
|
+
off_color_pips=off_color_pips,
|
|
214
|
+
grade=grade,
|
|
215
|
+
reasons=("splashing is disabled",),
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
if splash_color is None:
|
|
219
|
+
return _assessment(
|
|
220
|
+
classification="off-color",
|
|
221
|
+
state=state,
|
|
222
|
+
off_color_pips=off_color_pips,
|
|
223
|
+
grade=grade,
|
|
224
|
+
reasons=("card requires more than one color outside the primary pair",),
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
if off_color_pips > config.maximum_off_color_pips:
|
|
228
|
+
return _assessment(
|
|
229
|
+
classification="off-color",
|
|
230
|
+
state=state,
|
|
231
|
+
splash_color=splash_color,
|
|
232
|
+
off_color_pips=off_color_pips,
|
|
233
|
+
grade=grade,
|
|
234
|
+
reasons=("card has too many off-color mana pips",),
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
if state.active_color is not None and splash_color != state.active_color:
|
|
238
|
+
return _assessment(
|
|
239
|
+
classification="off-color",
|
|
240
|
+
state=state,
|
|
241
|
+
splash_color=splash_color,
|
|
242
|
+
off_color_pips=off_color_pips,
|
|
243
|
+
grade=grade,
|
|
244
|
+
reasons=(f"the active splash color is {state.active_color}",),
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
if state.picked_card_count >= config.maximum_cards:
|
|
248
|
+
return _assessment(
|
|
249
|
+
classification="off-color",
|
|
250
|
+
state=state,
|
|
251
|
+
splash_color=splash_color,
|
|
252
|
+
off_color_pips=off_color_pips,
|
|
253
|
+
grade=grade,
|
|
254
|
+
reasons=("the splash-card limit is already reached",),
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
score_advantage = (
|
|
258
|
+
None
|
|
259
|
+
if best_on_color_score is None
|
|
260
|
+
else base_score - best_on_color_score
|
|
261
|
+
)
|
|
262
|
+
if (
|
|
263
|
+
score_advantage is not None
|
|
264
|
+
and score_advantage < config.minimum_score_advantage
|
|
265
|
+
):
|
|
266
|
+
return _assessment(
|
|
267
|
+
classification="off-color",
|
|
268
|
+
state=state,
|
|
269
|
+
splash_color=splash_color,
|
|
270
|
+
off_color_pips=off_color_pips,
|
|
271
|
+
grade=grade,
|
|
272
|
+
score_advantage=score_advantage,
|
|
273
|
+
reasons=("card is not enough better than the best on-color option",),
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
prospective_count = state.picked_card_count + 1
|
|
277
|
+
required_sources = _required_sources(
|
|
278
|
+
splash_card_count=prospective_count,
|
|
279
|
+
config=config,
|
|
280
|
+
)
|
|
281
|
+
fixing_sources = state.fixing_for(color=splash_color)
|
|
282
|
+
planned_basics = min(config.planned_basic_sources, required_sources)
|
|
283
|
+
available_sources = fixing_sources + planned_basics
|
|
284
|
+
supported_grade = config.supported_minimum_grade
|
|
285
|
+
if state.aggressive:
|
|
286
|
+
supported_grade = config.speculative_minimum_grade
|
|
287
|
+
|
|
288
|
+
if (
|
|
289
|
+
available_sources >= required_sources
|
|
290
|
+
and grade_at_least(grade=grade, minimum=supported_grade)
|
|
291
|
+
):
|
|
292
|
+
return _assessment(
|
|
293
|
+
classification="splash-ready",
|
|
294
|
+
state=state,
|
|
295
|
+
splash_color=splash_color,
|
|
296
|
+
off_color_pips=off_color_pips,
|
|
297
|
+
grade=grade,
|
|
298
|
+
score_advantage=score_advantage,
|
|
299
|
+
fixing_sources=fixing_sources,
|
|
300
|
+
planned_basic_sources=planned_basics,
|
|
301
|
+
required_sources=required_sources,
|
|
302
|
+
reasons=("elite single-pip card has sufficient mana support",),
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
if (
|
|
306
|
+
not locked
|
|
307
|
+
and not state.aggressive
|
|
308
|
+
and grade_at_least(
|
|
309
|
+
grade=grade,
|
|
310
|
+
minimum=config.speculative_minimum_grade,
|
|
311
|
+
)
|
|
312
|
+
):
|
|
313
|
+
return _assessment(
|
|
314
|
+
classification="splash-speculative",
|
|
315
|
+
state=state,
|
|
316
|
+
splash_color=splash_color,
|
|
317
|
+
off_color_pips=off_color_pips,
|
|
318
|
+
grade=grade,
|
|
319
|
+
score_advantage=score_advantage,
|
|
320
|
+
fixing_sources=fixing_sources,
|
|
321
|
+
planned_basic_sources=planned_basics,
|
|
322
|
+
required_sources=required_sources,
|
|
323
|
+
reasons=("exceptional card can be supported by future fixing",),
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
reasons: list[str] = []
|
|
327
|
+
if not grade_at_least(grade=grade, minimum=supported_grade):
|
|
328
|
+
reasons.append(f"grade is below {supported_grade}")
|
|
329
|
+
if available_sources < required_sources:
|
|
330
|
+
reasons.append(
|
|
331
|
+
f"needs {required_sources - available_sources} more deterministic sources"
|
|
332
|
+
)
|
|
333
|
+
if locked:
|
|
334
|
+
reasons.append("speculative splashes are disabled after color lock")
|
|
335
|
+
if state.aggressive:
|
|
336
|
+
reasons.append("aggressive pools require supported exceptional splashes")
|
|
337
|
+
return _assessment(
|
|
338
|
+
classification="off-color",
|
|
339
|
+
state=state,
|
|
340
|
+
splash_color=splash_color,
|
|
341
|
+
off_color_pips=off_color_pips,
|
|
342
|
+
grade=grade,
|
|
343
|
+
score_advantage=score_advantage,
|
|
344
|
+
fixing_sources=fixing_sources,
|
|
345
|
+
planned_basic_sources=planned_basics,
|
|
346
|
+
required_sources=required_sources,
|
|
347
|
+
reasons=tuple(reasons) or ("splash requirements are not met",),
|
|
348
|
+
)
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
def splash_requirement(*, card: CardInfo, base_pair: str) -> tuple[str | None, int]:
|
|
352
|
+
"""Return one required off-pair color and its pip count when unambiguous.
|
|
353
|
+
Hybrid symbols castable using a base color do not create a splash requirement.
|
|
354
|
+
"""
|
|
355
|
+
|
|
356
|
+
outside_colors = tuple(
|
|
357
|
+
color for color in COLOR_ORDER if color in card.colors and color not in base_pair
|
|
358
|
+
)
|
|
359
|
+
if len(outside_colors) != 1:
|
|
360
|
+
return (None, len(outside_colors))
|
|
361
|
+
|
|
362
|
+
splash_color = outside_colors[0]
|
|
363
|
+
if not card.mana_cost:
|
|
364
|
+
return (splash_color, 1)
|
|
365
|
+
|
|
366
|
+
pip_count = 0
|
|
367
|
+
castable_hybrid = False
|
|
368
|
+
for symbol in MANA_SYMBOL_PATTERN.findall(card.mana_cost):
|
|
369
|
+
symbol_colors = tuple(color for color in COLOR_ORDER if color in symbol)
|
|
370
|
+
if not symbol_colors:
|
|
371
|
+
continue
|
|
372
|
+
if any(color in base_pair for color in symbol_colors):
|
|
373
|
+
if splash_color in symbol_colors:
|
|
374
|
+
castable_hybrid = True
|
|
375
|
+
continue
|
|
376
|
+
if splash_color in symbol_colors:
|
|
377
|
+
pip_count += 1
|
|
378
|
+
|
|
379
|
+
if pip_count == 0 and castable_hybrid:
|
|
380
|
+
return (None, 0)
|
|
381
|
+
|
|
382
|
+
return (splash_color, pip_count or 1)
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
def card_is_castable_in_pair(*, card: CardInfo, base_pair: str) -> bool:
|
|
386
|
+
"""Return whether a card can be cast using only the primary colors.
|
|
387
|
+
Hybrid symbols satisfied by a primary color count as on-color.
|
|
388
|
+
"""
|
|
389
|
+
|
|
390
|
+
if not card.colors or all(color in base_pair for color in card.colors):
|
|
391
|
+
return True
|
|
392
|
+
|
|
393
|
+
splash_color, off_color_pips = splash_requirement(
|
|
394
|
+
card=card,
|
|
395
|
+
base_pair=base_pair,
|
|
396
|
+
)
|
|
397
|
+
return splash_color is None and off_color_pips == 0
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
def grade_at_least(*, grade: str | None, minimum: str) -> bool:
|
|
401
|
+
if grade not in GRADE_LABELS or minimum not in GRADE_LABELS:
|
|
402
|
+
return False
|
|
403
|
+
|
|
404
|
+
return GRADE_LABELS.index(grade) >= GRADE_LABELS.index(minimum)
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
def _assessment(
|
|
408
|
+
*,
|
|
409
|
+
classification: str,
|
|
410
|
+
state: SplashState,
|
|
411
|
+
grade: str | None,
|
|
412
|
+
reasons: tuple[str, ...],
|
|
413
|
+
splash_color: str | None = None,
|
|
414
|
+
off_color_pips: int = 0,
|
|
415
|
+
score_advantage: float | None = None,
|
|
416
|
+
fixing_sources: int = 0,
|
|
417
|
+
planned_basic_sources: int = 0,
|
|
418
|
+
required_sources: int = 0,
|
|
419
|
+
) -> SplashAssessment:
|
|
420
|
+
return SplashAssessment(
|
|
421
|
+
classification=classification,
|
|
422
|
+
splash_color=splash_color,
|
|
423
|
+
off_color_pips=off_color_pips,
|
|
424
|
+
picked_card_count=state.picked_card_count,
|
|
425
|
+
fixing_sources=fixing_sources,
|
|
426
|
+
planned_basic_sources=planned_basic_sources,
|
|
427
|
+
available_sources=fixing_sources + planned_basic_sources,
|
|
428
|
+
required_sources=required_sources,
|
|
429
|
+
grade=grade,
|
|
430
|
+
score_advantage=score_advantage,
|
|
431
|
+
aggressive=state.aggressive,
|
|
432
|
+
reasons=reasons,
|
|
433
|
+
)
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
def _fixing_counts(
|
|
437
|
+
*,
|
|
438
|
+
pool_grp_ids: tuple[int, ...],
|
|
439
|
+
card_database: CardDatabase,
|
|
440
|
+
base_pair: str | None,
|
|
441
|
+
) -> dict[str, int]:
|
|
442
|
+
counts = {
|
|
443
|
+
color: 0
|
|
444
|
+
for color in COLOR_ORDER
|
|
445
|
+
if base_pair is None or color not in base_pair
|
|
446
|
+
}
|
|
447
|
+
if base_pair is None:
|
|
448
|
+
return counts
|
|
449
|
+
|
|
450
|
+
for grp_id in pool_grp_ids:
|
|
451
|
+
card = card_database.lookup(grp_id=grp_id)
|
|
452
|
+
if card.unknown or not card_is_castable_in_pair(
|
|
453
|
+
card=card,
|
|
454
|
+
base_pair=base_pair,
|
|
455
|
+
):
|
|
456
|
+
continue
|
|
457
|
+
if not _is_drafted_fixing_land(card=card):
|
|
458
|
+
continue
|
|
459
|
+
|
|
460
|
+
for color in card.produced_mana:
|
|
461
|
+
if color in counts:
|
|
462
|
+
counts[color] += 1
|
|
463
|
+
|
|
464
|
+
return counts
|
|
465
|
+
|
|
466
|
+
|
|
467
|
+
def _active_splash_color(
|
|
468
|
+
*,
|
|
469
|
+
eligible_by_color: dict[str, list[tuple[float, int]]],
|
|
470
|
+
fixing_sources: dict[str, int],
|
|
471
|
+
config: SplashConfig,
|
|
472
|
+
) -> str | None:
|
|
473
|
+
candidates = tuple(
|
|
474
|
+
color for color, cards in eligible_by_color.items() if cards
|
|
475
|
+
)
|
|
476
|
+
if not candidates:
|
|
477
|
+
return None
|
|
478
|
+
|
|
479
|
+
return max(
|
|
480
|
+
candidates,
|
|
481
|
+
key=lambda color: (
|
|
482
|
+
_supported_card_count(
|
|
483
|
+
card_count=len(eligible_by_color[color]),
|
|
484
|
+
fixing_sources=fixing_sources.get(color, 0),
|
|
485
|
+
config=config,
|
|
486
|
+
),
|
|
487
|
+
len(eligible_by_color[color]),
|
|
488
|
+
max(eligible_by_color[color]),
|
|
489
|
+
fixing_sources.get(color, 0),
|
|
490
|
+
-COLOR_ORDER.index(color),
|
|
491
|
+
),
|
|
492
|
+
)
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
def _supported_card_count(
|
|
496
|
+
*,
|
|
497
|
+
card_count: int,
|
|
498
|
+
fixing_sources: int,
|
|
499
|
+
config: SplashConfig,
|
|
500
|
+
) -> int:
|
|
501
|
+
available_sources = fixing_sources + config.planned_basic_sources
|
|
502
|
+
if available_sources >= config.multiple_card_sources:
|
|
503
|
+
return min(card_count, config.maximum_cards)
|
|
504
|
+
if available_sources >= config.single_card_sources:
|
|
505
|
+
return min(card_count, 1)
|
|
506
|
+
|
|
507
|
+
return 0
|
|
508
|
+
|
|
509
|
+
|
|
510
|
+
def _pool_is_aggressive(
|
|
511
|
+
*,
|
|
512
|
+
pool_grp_ids: tuple[int, ...],
|
|
513
|
+
card_database: CardDatabase,
|
|
514
|
+
base_pair: str,
|
|
515
|
+
config: SplashConfig,
|
|
516
|
+
) -> bool:
|
|
517
|
+
spells = tuple(
|
|
518
|
+
card_database.lookup(grp_id=grp_id)
|
|
519
|
+
for grp_id in pool_grp_ids
|
|
520
|
+
if _is_base_spell(
|
|
521
|
+
card=card_database.lookup(grp_id=grp_id),
|
|
522
|
+
base_pair=base_pair,
|
|
523
|
+
)
|
|
524
|
+
)
|
|
525
|
+
if len(spells) < config.aggressive_minimum_spells:
|
|
526
|
+
return False
|
|
527
|
+
|
|
528
|
+
mana_values = tuple(
|
|
529
|
+
card.mana_value for card in spells if card.mana_value is not None
|
|
530
|
+
)
|
|
531
|
+
if not mana_values:
|
|
532
|
+
return False
|
|
533
|
+
|
|
534
|
+
average_mana_value = sum(mana_values) / len(mana_values)
|
|
535
|
+
two_drop_ratio = sum(value == 2.0 for value in mana_values) / len(mana_values)
|
|
536
|
+
return (
|
|
537
|
+
average_mana_value <= config.aggressive_average_mana_value_max
|
|
538
|
+
and two_drop_ratio >= config.aggressive_minimum_two_drop_ratio
|
|
539
|
+
)
|
|
540
|
+
|
|
541
|
+
|
|
542
|
+
def _is_base_spell(*, card: CardInfo, base_pair: str) -> bool:
|
|
543
|
+
if card.unknown or any("Land" in type_line for type_line in card.types):
|
|
544
|
+
return False
|
|
545
|
+
|
|
546
|
+
return card_is_castable_in_pair(card=card, base_pair=base_pair)
|
|
547
|
+
|
|
548
|
+
|
|
549
|
+
def _is_splash_fixer(
|
|
550
|
+
*,
|
|
551
|
+
card: CardInfo,
|
|
552
|
+
state: SplashState,
|
|
553
|
+
config: SplashConfig,
|
|
554
|
+
) -> bool:
|
|
555
|
+
active_color = state.active_color
|
|
556
|
+
if active_color is None or active_color not in card.produced_mana:
|
|
557
|
+
return False
|
|
558
|
+
if not _is_drafted_fixing_land(card=card):
|
|
559
|
+
return False
|
|
560
|
+
|
|
561
|
+
base_pair = state.base_pair
|
|
562
|
+
if base_pair is None or not card_is_castable_in_pair(
|
|
563
|
+
card=card,
|
|
564
|
+
base_pair=base_pair,
|
|
565
|
+
):
|
|
566
|
+
return False
|
|
567
|
+
|
|
568
|
+
required_sources = _required_sources(
|
|
569
|
+
splash_card_count=max(1, state.picked_card_count),
|
|
570
|
+
config=config,
|
|
571
|
+
)
|
|
572
|
+
available_sources = (
|
|
573
|
+
state.fixing_for(color=active_color) + config.planned_basic_sources
|
|
574
|
+
)
|
|
575
|
+
return available_sources < required_sources
|
|
576
|
+
|
|
577
|
+
|
|
578
|
+
def _is_drafted_fixing_land(*, card: CardInfo) -> bool:
|
|
579
|
+
is_land = any("Land" in type_line for type_line in card.types)
|
|
580
|
+
is_basic = any(
|
|
581
|
+
"Basic" in type_line and "Land" in type_line
|
|
582
|
+
for type_line in card.types
|
|
583
|
+
)
|
|
584
|
+
return is_land and not is_basic
|
|
585
|
+
|
|
586
|
+
|
|
587
|
+
def _required_sources(
|
|
588
|
+
*,
|
|
589
|
+
splash_card_count: int,
|
|
590
|
+
config: SplashConfig,
|
|
591
|
+
) -> int:
|
|
592
|
+
if splash_card_count <= 1:
|
|
593
|
+
return config.single_card_sources
|
|
594
|
+
|
|
595
|
+
return config.multiple_card_sources
|
|
596
|
+
|
|
597
|
+
|
|
598
|
+
def _global_grade(
|
|
599
|
+
*,
|
|
600
|
+
ratings_data: SeventeenLandsData | None,
|
|
601
|
+
grp_id: int,
|
|
602
|
+
) -> str | None:
|
|
603
|
+
if ratings_data is None:
|
|
604
|
+
return None
|
|
605
|
+
|
|
606
|
+
return ratings_data.rating_for(grp_id=grp_id).letter_grade
|
|
607
|
+
|
|
608
|
+
|
|
609
|
+
def _global_win_rate(
|
|
610
|
+
*,
|
|
611
|
+
ratings_data: SeventeenLandsData | None,
|
|
612
|
+
grp_id: int,
|
|
613
|
+
) -> float | None:
|
|
614
|
+
if ratings_data is None:
|
|
615
|
+
return None
|
|
616
|
+
|
|
617
|
+
return ratings_data.rating_for(grp_id=grp_id).gih_win_rate
|