mirror of
https://github.com/mwisnowski/mtg_python_deckbuilder.git
synced 2025-12-16 15:40:12 +01:00
With assistance from Github CoPilot, massively overhauled the builder functionality, splitting it into smaller modules to provide a better step-by-step focus and drastically reduce the overall size of the core builder module
This commit is contained in:
parent
ff1912f979
commit
760c36d75d
17 changed files with 3044 additions and 2602 deletions
613
code/deck_builder/phases/phase4_spells.py
Normal file
613
code/deck_builder/phases/phase4_spells.py
Normal file
|
|
@ -0,0 +1,613 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
from typing import List, Dict
|
||||
|
||||
from .. import builder_utils as bu
|
||||
from .. import builder_constants as bc
|
||||
import logging_util
|
||||
|
||||
logger = logging_util.logging.getLogger(__name__)
|
||||
|
||||
class SpellAdditionMixin:
|
||||
"""Phase 4: Non-creature spell additions (ramp, removal, wipes, draw, protection, thematic filler).
|
||||
|
||||
Extracted intact from monolithic builder. Logic intentionally unchanged; future refinements
|
||||
(e.g., further per-category sub-mixins) can split this class if complexity grows.
|
||||
"""
|
||||
|
||||
# ---------------------------
|
||||
# Ramp
|
||||
# ---------------------------
|
||||
def add_ramp(self): # noqa: C901
|
||||
"""Add ramp pieces in three phases: mana rocks (~1/3), mana dorks (~1/4), then general/other.
|
||||
|
||||
Selection is deterministic priority based: lowest edhrecRank then lowest mana value.
|
||||
No theme weighting – simple best-available filtering while avoiding duplicates.
|
||||
"""
|
||||
if not self._combined_cards_df is not None: # preserve original logic
|
||||
return
|
||||
target_total = self.ideal_counts.get('ramp', 0)
|
||||
if target_total <= 0:
|
||||
return
|
||||
already = {n.lower() for n in self.card_library.keys()}
|
||||
df = self._combined_cards_df
|
||||
if 'name' not in df.columns:
|
||||
return
|
||||
|
||||
work = df.copy()
|
||||
work['_ltags'] = work.get('themeTags', []).apply(bu.normalize_tag_cell)
|
||||
work = work[work['_ltags'].apply(lambda tags: any('ramp' in t for t in tags))]
|
||||
if work.empty:
|
||||
self.output_func('No ramp-tagged cards found in dataset.')
|
||||
return
|
||||
existing_ramp = 0
|
||||
for name, entry in self.card_library.items():
|
||||
if any(isinstance(t, str) and 'ramp' in t.lower() for t in entry.get('Tags', [])):
|
||||
existing_ramp += 1
|
||||
to_add, _bonus = bu.compute_adjusted_target('Ramp', target_total, existing_ramp, self.output_func, plural_word='ramp spells')
|
||||
if existing_ramp >= target_total and to_add == 0:
|
||||
return
|
||||
if existing_ramp < target_total:
|
||||
target_total = to_add
|
||||
else:
|
||||
target_total = to_add
|
||||
work = work[~work['type'].fillna('').str.contains('Land', case=False, na=False)]
|
||||
commander_name = getattr(self, 'commander', None)
|
||||
if commander_name:
|
||||
work = work[work['name'] != commander_name]
|
||||
work = bu.sort_by_priority(work, ['edhrecRank','manaValue'])
|
||||
|
||||
rocks_target = min(target_total, math.ceil(target_total/3))
|
||||
dorks_target = min(target_total - rocks_target, math.ceil(target_total/4))
|
||||
|
||||
added_rocks: List[str] = []
|
||||
added_dorks: List[str] = []
|
||||
added_general: List[str] = []
|
||||
|
||||
def add_from_pool(pool, remaining_needed, added_list, phase_name):
|
||||
added_now = 0
|
||||
for _, r in pool.iterrows():
|
||||
nm = r['name']
|
||||
if nm.lower() in already:
|
||||
continue
|
||||
self.add_card(
|
||||
nm,
|
||||
card_type=r.get('type',''),
|
||||
mana_cost=r.get('manaCost',''),
|
||||
mana_value=r.get('manaValue', r.get('cmc','')),
|
||||
tags=r.get('themeTags', []) if isinstance(r.get('themeTags', []), list) else [],
|
||||
role='ramp',
|
||||
sub_role=phase_name.lower(),
|
||||
added_by='spell_ramp'
|
||||
)
|
||||
already.add(nm.lower())
|
||||
added_list.append(nm)
|
||||
added_now += 1
|
||||
if added_now >= remaining_needed:
|
||||
break
|
||||
if added_now:
|
||||
self.output_func(f"Ramp phase {phase_name}: added {added_now}/{remaining_needed} target.")
|
||||
return added_now
|
||||
|
||||
rocks_pool = work[work['type'].fillna('').str.contains('Artifact', case=False, na=False)]
|
||||
if rocks_target > 0:
|
||||
add_from_pool(rocks_pool, rocks_target, added_rocks, 'Rocks')
|
||||
|
||||
dorks_pool = work[work['type'].fillna('').str.contains('Creature', case=False, na=False)]
|
||||
if dorks_target > 0:
|
||||
add_from_pool(dorks_pool, dorks_target, added_dorks, 'Dorks')
|
||||
|
||||
current_total = len(added_rocks) + len(added_dorks)
|
||||
remaining = target_total - current_total
|
||||
if remaining > 0:
|
||||
general_pool = work[~work['name'].isin(added_rocks + added_dorks)]
|
||||
add_from_pool(general_pool, remaining, added_general, 'General')
|
||||
|
||||
total_added_now = len(added_rocks)+len(added_dorks)+len(added_general)
|
||||
self.output_func(f"Total Ramp Added This Pass: {total_added_now}/{target_total}")
|
||||
if total_added_now < target_total:
|
||||
self.output_func('Ramp shortfall due to limited dataset.')
|
||||
if total_added_now:
|
||||
self.output_func("Ramp Cards Added:")
|
||||
for nm in added_rocks:
|
||||
self.output_func(f" [Rock] {nm}")
|
||||
for nm in added_dorks:
|
||||
self.output_func(f" [Dork] {nm}")
|
||||
for nm in added_general:
|
||||
self.output_func(f" [General] {nm}")
|
||||
|
||||
# ---------------------------
|
||||
# Removal
|
||||
# ---------------------------
|
||||
def add_removal(self): # noqa: C901
|
||||
target = self.ideal_counts.get('removal', 0)
|
||||
if target <= 0 or self._combined_cards_df is None:
|
||||
return
|
||||
already = {n.lower() for n in self.card_library.keys()}
|
||||
df = self._combined_cards_df.copy()
|
||||
if 'name' not in df.columns:
|
||||
return
|
||||
df['_ltags'] = df.get('themeTags', []).apply(bu.normalize_tag_cell)
|
||||
def is_removal(tags):
|
||||
return any('removal' in t or 'spot removal' in t for t in tags)
|
||||
def is_wipe(tags):
|
||||
return any('board wipe' in t or 'mass removal' in t for t in tags)
|
||||
pool = df[df['_ltags'].apply(is_removal) & ~df['_ltags'].apply(is_wipe)]
|
||||
pool = pool[~pool['type'].fillna('').str.contains('Land', case=False, na=False)]
|
||||
commander_name = getattr(self, 'commander', None)
|
||||
if commander_name:
|
||||
pool = pool[pool['name'] != commander_name]
|
||||
pool = bu.sort_by_priority(pool, ['edhrecRank','manaValue'])
|
||||
existing = 0
|
||||
for name, entry in self.card_library.items():
|
||||
lt = [str(t).lower() for t in entry.get('Tags', [])]
|
||||
if any(('removal' in t or 'spot removal' in t) for t in lt) and not any(('board wipe' in t or 'mass removal' in t) for t in lt):
|
||||
existing += 1
|
||||
to_add, _bonus = bu.compute_adjusted_target('Removal', target, existing, self.output_func, plural_word='removal spells')
|
||||
if existing >= target and to_add == 0:
|
||||
return
|
||||
target = to_add if existing < target else to_add
|
||||
added = 0
|
||||
added_names: List[str] = []
|
||||
for _, r in pool.iterrows():
|
||||
if added >= target:
|
||||
break
|
||||
nm = r['name']
|
||||
if nm.lower() in already:
|
||||
continue
|
||||
self.add_card(
|
||||
nm,
|
||||
card_type=r.get('type',''),
|
||||
mana_cost=r.get('manaCost',''),
|
||||
mana_value=r.get('manaValue', r.get('cmc','')),
|
||||
tags=r.get('themeTags', []) if isinstance(r.get('themeTags', []), list) else [],
|
||||
role='removal',
|
||||
sub_role='spot',
|
||||
added_by='spell_removal'
|
||||
)
|
||||
already.add(nm.lower())
|
||||
added += 1
|
||||
added_names.append(nm)
|
||||
self.output_func(f"Added Spot Removal This Pass: {added}/{target}{' (dataset shortfall)' if added < target else ''}")
|
||||
if added_names:
|
||||
self.output_func('Removal Cards Added:')
|
||||
for nm in added_names:
|
||||
self.output_func(f" - {nm}")
|
||||
|
||||
# ---------------------------
|
||||
# Board Wipes
|
||||
# ---------------------------
|
||||
def add_board_wipes(self):
|
||||
target = self.ideal_counts.get('wipes', 0)
|
||||
if target <= 0 or self._combined_cards_df is None:
|
||||
return
|
||||
already = {n.lower() for n in self.card_library.keys()}
|
||||
df = self._combined_cards_df.copy()
|
||||
df['_ltags'] = df.get('themeTags', []).apply(bu.normalize_tag_cell)
|
||||
def is_wipe(tags):
|
||||
return any('board wipe' in t or 'mass removal' in t for t in tags)
|
||||
pool = df[df['_ltags'].apply(is_wipe)]
|
||||
pool = pool[~pool['type'].fillna('').str.contains('Land', case=False, na=False)]
|
||||
commander_name = getattr(self, 'commander', None)
|
||||
if commander_name:
|
||||
pool = pool[pool['name'] != commander_name]
|
||||
pool = bu.sort_by_priority(pool, ['edhrecRank','manaValue'])
|
||||
existing = 0
|
||||
for name, entry in self.card_library.items():
|
||||
tags = [str(t).lower() for t in entry.get('Tags', [])]
|
||||
if any(('board wipe' in t or 'mass removal' in t) for t in tags):
|
||||
existing += 1
|
||||
to_add, _bonus = bu.compute_adjusted_target('Board wipe', target, existing, self.output_func, plural_word='wipes')
|
||||
if existing >= target and to_add == 0:
|
||||
return
|
||||
target = to_add if existing < target else to_add
|
||||
added = 0
|
||||
added_names: List[str] = []
|
||||
for _, r in pool.iterrows():
|
||||
if added >= target:
|
||||
break
|
||||
nm = r['name']
|
||||
if nm.lower() in already:
|
||||
continue
|
||||
self.add_card(
|
||||
nm,
|
||||
card_type=r.get('type',''),
|
||||
mana_cost=r.get('manaCost',''),
|
||||
mana_value=r.get('manaValue', r.get('cmc','')),
|
||||
tags=r.get('themeTags', []) if isinstance(r.get('themeTags', []), list) else [],
|
||||
role='wipe',
|
||||
sub_role='board',
|
||||
added_by='spell_wipe'
|
||||
)
|
||||
already.add(nm.lower())
|
||||
added += 1
|
||||
added_names.append(nm)
|
||||
self.output_func(f"Added Board Wipes This Pass: {added}/{target}{' (dataset shortfall)' if added < target else ''}")
|
||||
if added_names:
|
||||
self.output_func('Board Wipes Added:')
|
||||
for nm in added_names:
|
||||
self.output_func(f" - {nm}")
|
||||
|
||||
# ---------------------------
|
||||
# Card Advantage
|
||||
# ---------------------------
|
||||
def add_card_advantage(self): # noqa: C901
|
||||
total_target = self.ideal_counts.get('card_advantage', 0)
|
||||
if total_target <= 0 or self._combined_cards_df is None:
|
||||
return
|
||||
existing = 0
|
||||
for name, entry in self.card_library.items():
|
||||
tags = [str(t).lower() for t in entry.get('Tags', [])]
|
||||
if any(('draw' in t) or ('card advantage' in t) for t in tags):
|
||||
existing += 1
|
||||
to_add_total, _bonus = bu.compute_adjusted_target('Card advantage', total_target, existing, self.output_func, plural_word='draw spells')
|
||||
if existing >= total_target and to_add_total == 0:
|
||||
return
|
||||
total_target = to_add_total if existing < total_target else to_add_total
|
||||
conditional_target = min(total_target, math.ceil(total_target * 0.2))
|
||||
already = {n.lower() for n in self.card_library.keys()}
|
||||
df = self._combined_cards_df.copy()
|
||||
df['_ltags'] = df.get('themeTags', []).apply(bu.normalize_tag_cell)
|
||||
def is_draw(tags):
|
||||
return any(('draw' in t) or ('card advantage' in t) for t in tags)
|
||||
df = df[df['_ltags'].apply(is_draw)]
|
||||
df = df[~df['type'].fillna('').str.contains('Land', case=False, na=False)]
|
||||
commander_name = getattr(self, 'commander', None)
|
||||
if commander_name:
|
||||
df = df[df['name'] != commander_name]
|
||||
CONDITIONAL_KEYS = ['conditional', 'situational', 'attacks', 'combat damage', 'when you cast']
|
||||
def is_conditional(tags):
|
||||
return any(any(k in t for k in CONDITIONAL_KEYS) for t in tags)
|
||||
conditional_df = df[df['_ltags'].apply(is_conditional)]
|
||||
unconditional_df = df[~df.index.isin(conditional_df.index)]
|
||||
def sortit(d):
|
||||
return bu.sort_by_priority(d, ['edhrecRank','manaValue'])
|
||||
conditional_df = sortit(conditional_df)
|
||||
unconditional_df = sortit(unconditional_df)
|
||||
added_cond = 0
|
||||
added_cond_names: List[str] = []
|
||||
for _, r in conditional_df.iterrows():
|
||||
if added_cond >= conditional_target:
|
||||
break
|
||||
nm = r['name']
|
||||
if nm.lower() in already:
|
||||
continue
|
||||
self.add_card(
|
||||
nm,
|
||||
card_type=r.get('type',''),
|
||||
mana_cost=r.get('manaCost',''),
|
||||
mana_value=r.get('manaValue', r.get('cmc','')),
|
||||
tags=r.get('themeTags', []) if isinstance(r.get('themeTags', []), list) else [],
|
||||
role='card_advantage',
|
||||
sub_role='conditional',
|
||||
added_by='spell_draw'
|
||||
)
|
||||
already.add(nm.lower())
|
||||
added_cond += 1
|
||||
added_cond_names.append(nm)
|
||||
remaining = total_target - added_cond
|
||||
added_uncond = 0
|
||||
added_uncond_names: List[str] = []
|
||||
if remaining > 0:
|
||||
for _, r in unconditional_df.iterrows():
|
||||
if added_uncond >= remaining:
|
||||
break
|
||||
nm = r['name']
|
||||
if nm.lower() in already:
|
||||
continue
|
||||
self.add_card(
|
||||
nm,
|
||||
card_type=r.get('type',''),
|
||||
mana_cost=r.get('manaCost',''),
|
||||
mana_value=r.get('manaValue', r.get('cmc','')),
|
||||
tags=r.get('themeTags', []) if isinstance(r.get('themeTags', []), list) else [],
|
||||
role='card_advantage',
|
||||
sub_role='unconditional',
|
||||
added_by='spell_draw'
|
||||
)
|
||||
already.add(nm.lower())
|
||||
added_uncond += 1
|
||||
added_uncond_names.append(nm)
|
||||
self.output_func(f"Added Card Advantage This Pass: conditional {added_cond}/{conditional_target}, total {(added_cond+added_uncond)}/{total_target}{' (dataset shortfall)' if (added_cond+added_uncond) < total_target else ''}")
|
||||
if added_cond_names or added_uncond_names:
|
||||
self.output_func('Card Advantage Cards Added:')
|
||||
for nm in added_cond_names:
|
||||
self.output_func(f" [Conditional] {nm}")
|
||||
for nm in added_uncond_names:
|
||||
self.output_func(f" [Unconditional] {nm}")
|
||||
|
||||
# ---------------------------
|
||||
# Protection
|
||||
# ---------------------------
|
||||
def add_protection(self):
|
||||
target = self.ideal_counts.get('protection', 0)
|
||||
if target <= 0 or self._combined_cards_df is None:
|
||||
return
|
||||
already = {n.lower() for n in self.card_library.keys()}
|
||||
df = self._combined_cards_df.copy()
|
||||
df['_ltags'] = df.get('themeTags', []).apply(bu.normalize_tag_cell)
|
||||
pool = df[df['_ltags'].apply(lambda tags: any('protection' in t for t in tags))]
|
||||
pool = pool[~pool['type'].fillna('').str.contains('Land', case=False, na=False)]
|
||||
commander_name = getattr(self, 'commander', None)
|
||||
if commander_name:
|
||||
pool = pool[pool['name'] != commander_name]
|
||||
pool = bu.sort_by_priority(pool, ['edhrecRank','manaValue'])
|
||||
existing = 0
|
||||
for name, entry in self.card_library.items():
|
||||
tags = [str(t).lower() for t in entry.get('Tags', [])]
|
||||
if any('protection' in t for t in tags):
|
||||
existing += 1
|
||||
to_add, _bonus = bu.compute_adjusted_target('Protection', target, existing, self.output_func, plural_word='protection spells')
|
||||
if existing >= target and to_add == 0:
|
||||
return
|
||||
target = to_add if existing < target else to_add
|
||||
added = 0
|
||||
added_names: List[str] = []
|
||||
for _, r in pool.iterrows():
|
||||
if added >= target:
|
||||
break
|
||||
nm = r['name']
|
||||
if nm.lower() in already:
|
||||
continue
|
||||
self.add_card(
|
||||
nm,
|
||||
card_type=r.get('type',''),
|
||||
mana_cost=r.get('manaCost',''),
|
||||
mana_value=r.get('manaValue', r.get('cmc','')),
|
||||
tags=r.get('themeTags', []) if isinstance(r.get('themeTags', []), list) else [],
|
||||
role='protection',
|
||||
added_by='spell_protection'
|
||||
)
|
||||
already.add(nm.lower())
|
||||
added += 1
|
||||
added_names.append(nm)
|
||||
self.output_func(f"Added Protection This Pass: {added}/{target}{' (dataset shortfall)' if added < target else ''}")
|
||||
if added_names:
|
||||
self.output_func('Protection Cards Added:')
|
||||
for nm in added_names:
|
||||
self.output_func(f" - {nm}")
|
||||
|
||||
# ---------------------------
|
||||
# Theme Spell Filler to 100
|
||||
# ---------------------------
|
||||
def fill_remaining_theme_spells(self): # noqa: C901
|
||||
total_cards = sum(entry.get('Count', 1) for entry in self.card_library.values())
|
||||
remaining = 100 - total_cards
|
||||
if remaining <= 0:
|
||||
return
|
||||
df = getattr(self, '_combined_cards_df', None)
|
||||
if df is None or df.empty or 'type' not in df.columns:
|
||||
return
|
||||
themes_ordered: List[tuple[str, str]] = []
|
||||
if self.primary_tag:
|
||||
themes_ordered.append(('primary', self.primary_tag))
|
||||
if self.secondary_tag:
|
||||
themes_ordered.append(('secondary', self.secondary_tag))
|
||||
if self.tertiary_tag:
|
||||
themes_ordered.append(('tertiary', self.tertiary_tag))
|
||||
if not themes_ordered:
|
||||
return
|
||||
n_themes = len(themes_ordered)
|
||||
if n_themes == 1:
|
||||
base_map = {'primary': 1.0}
|
||||
elif n_themes == 2:
|
||||
base_map = {'primary': 0.6, 'secondary': 0.4}
|
||||
else:
|
||||
base_map = {'primary': 0.5, 'secondary': 0.3, 'tertiary': 0.2}
|
||||
weights: Dict[str, float] = {}
|
||||
boosted: set[str] = set()
|
||||
if n_themes > 1:
|
||||
for role, tag in themes_ordered:
|
||||
w = base_map.get(role, 0.0)
|
||||
lt = tag.lower()
|
||||
if 'kindred' in lt or 'tribal' in lt:
|
||||
mult = getattr(bc, 'WEIGHT_ADJUSTMENT_FACTORS', {}).get(f'kindred_{role}', 1.0)
|
||||
w *= mult
|
||||
boosted.add(role)
|
||||
weights[role] = w
|
||||
tot = sum(weights.values())
|
||||
if tot > 1.0:
|
||||
for r in weights:
|
||||
weights[r] /= tot
|
||||
else:
|
||||
rem = 1.0 - tot
|
||||
base_sum_unboosted = sum(base_map[r] for r, _ in themes_ordered if r not in boosted)
|
||||
if rem > 1e-6 and base_sum_unboosted > 0:
|
||||
for r, _ in themes_ordered:
|
||||
if r not in boosted:
|
||||
weights[r] += rem * (base_map[r] / base_sum_unboosted)
|
||||
else:
|
||||
weights['primary'] = 1.0
|
||||
spells_df = df[
|
||||
~df['type'].str.contains('Land', case=False, na=False)
|
||||
& ~df['type'].str.contains('Creature', case=False, na=False)
|
||||
].copy()
|
||||
if spells_df.empty:
|
||||
return
|
||||
selected_tags_lower = [t.lower() for _r, t in themes_ordered]
|
||||
if '_parsedThemeTags' not in spells_df.columns:
|
||||
spells_df['_parsedThemeTags'] = spells_df['themeTags'].apply(bu.normalize_tag_cell)
|
||||
spells_df['_normTags'] = spells_df['_parsedThemeTags']
|
||||
spells_df['_multiMatch'] = spells_df['_normTags'].apply(
|
||||
lambda lst: sum(1 for t in selected_tags_lower if t in lst)
|
||||
)
|
||||
base_top = 40
|
||||
top_n = int(base_top * getattr(bc, 'THEME_POOL_SIZE_MULTIPLIER', 2.0))
|
||||
synergy_bonus = getattr(bc, 'THEME_PRIORITY_BONUS', 1.2)
|
||||
per_theme_added: Dict[str, List[str]] = {r: [] for r, _t in themes_ordered}
|
||||
total_added = 0
|
||||
for role, tag in themes_ordered:
|
||||
if remaining - total_added <= 0:
|
||||
break
|
||||
w = weights.get(role, 0.0)
|
||||
if w <= 0:
|
||||
continue
|
||||
target = int(math.ceil(remaining * w * self._get_rng().uniform(1.0, 1.1)))
|
||||
target = min(target, remaining - total_added)
|
||||
if target <= 0:
|
||||
continue
|
||||
tnorm = tag.lower()
|
||||
subset = spells_df[
|
||||
spells_df['_normTags'].apply(
|
||||
lambda lst, tn=tnorm: (tn in lst) or any(tn in x for x in lst)
|
||||
)
|
||||
]
|
||||
if subset.empty:
|
||||
continue
|
||||
if 'edhrecRank' in subset.columns:
|
||||
subset = subset.sort_values(
|
||||
by=['_multiMatch', 'edhrecRank', 'manaValue'],
|
||||
ascending=[False, True, True],
|
||||
na_position='last',
|
||||
)
|
||||
elif 'manaValue' in subset.columns:
|
||||
subset = subset.sort_values(
|
||||
by=['_multiMatch', 'manaValue'],
|
||||
ascending=[False, True],
|
||||
na_position='last',
|
||||
)
|
||||
pool = subset.head(top_n).copy()
|
||||
pool = pool[~pool['name'].isin(self.card_library.keys())]
|
||||
if pool.empty:
|
||||
continue
|
||||
weighted_pool = [ (nm, (synergy_bonus if mm >= 2 else 1.0)) for nm, mm in zip(pool['name'], pool['_multiMatch']) ]
|
||||
chosen = bu.weighted_sample_without_replacement(weighted_pool, target)
|
||||
for nm in chosen:
|
||||
row = pool[pool['name'] == nm].iloc[0]
|
||||
self.add_card(
|
||||
nm,
|
||||
card_type=row.get('type', ''),
|
||||
mana_cost=row.get('manaCost', ''),
|
||||
mana_value=row.get('manaValue', row.get('cmc', '')),
|
||||
tags=row.get('themeTags', []) if isinstance(row.get('themeTags', []), list) else [],
|
||||
role='theme_spell',
|
||||
sub_role=role,
|
||||
added_by='spell_theme_fill',
|
||||
trigger_tag=tag,
|
||||
synergy=int(row.get('_multiMatch', 0)) if '_multiMatch' in row else None
|
||||
)
|
||||
per_theme_added[role].append(nm)
|
||||
total_added += 1
|
||||
if total_added >= remaining:
|
||||
break
|
||||
if total_added < remaining:
|
||||
need = remaining - total_added
|
||||
multi_pool = spells_df[~spells_df['name'].isin(self.card_library.keys())].copy()
|
||||
multi_pool = multi_pool[multi_pool['_multiMatch'] > 0]
|
||||
if not multi_pool.empty:
|
||||
if 'edhrecRank' in multi_pool.columns:
|
||||
multi_pool = multi_pool.sort_values(
|
||||
by=['_multiMatch', 'edhrecRank', 'manaValue'],
|
||||
ascending=[False, True, True],
|
||||
na_position='last',
|
||||
)
|
||||
elif 'manaValue' in multi_pool.columns:
|
||||
multi_pool = multi_pool.sort_values(
|
||||
by=['_multiMatch', 'manaValue'],
|
||||
ascending=[False, True],
|
||||
na_position='last',
|
||||
)
|
||||
fill = multi_pool['name'].tolist()[:need]
|
||||
for nm in fill:
|
||||
row = multi_pool[multi_pool['name'] == nm].iloc[0]
|
||||
self.add_card(
|
||||
nm,
|
||||
card_type=row.get('type', ''),
|
||||
mana_cost=row.get('manaCost', ''),
|
||||
mana_value=row.get('manaValue', row.get('cmc', '')),
|
||||
tags=row.get('themeTags', []) if isinstance(row.get('themeTags', []), list) else [],
|
||||
role='theme_spell',
|
||||
sub_role='fill_multi',
|
||||
added_by='spell_theme_fill',
|
||||
synergy=int(row.get('_multiMatch', 0)) if '_multiMatch' in row else None
|
||||
)
|
||||
total_added += 1
|
||||
if total_added >= remaining:
|
||||
break
|
||||
if total_added < remaining:
|
||||
extra_needed = remaining - total_added
|
||||
leftover = spells_df[~spells_df['name'].isin(self.card_library.keys())].copy()
|
||||
if not leftover.empty:
|
||||
if '_normTags' not in leftover.columns:
|
||||
leftover['_normTags'] = leftover['themeTags'].apply(
|
||||
lambda x: [str(t).lower() for t in x] if isinstance(x, list) else []
|
||||
)
|
||||
def has_any(tag_list, needles):
|
||||
return any(any(nd in t for nd in needles) for t in tag_list)
|
||||
def classify(row):
|
||||
tags = row['_normTags']
|
||||
if has_any(tags, ['ramp']):
|
||||
return 'ramp'
|
||||
if has_any(tags, ['card advantage', 'draw']):
|
||||
return 'card_advantage'
|
||||
if has_any(tags, ['protection']):
|
||||
return 'protection'
|
||||
if has_any(tags, ['board wipe', 'mass removal']):
|
||||
return 'board_wipe'
|
||||
if has_any(tags, ['removal']):
|
||||
return 'removal'
|
||||
return ''
|
||||
leftover['_fillerCat'] = leftover.apply(classify, axis=1)
|
||||
random_added: List[str] = []
|
||||
for _ in range(extra_needed):
|
||||
candidates_by_cat: Dict[str, any] = {}
|
||||
for cat in ['ramp','card_advantage','protection','board_wipe','removal']:
|
||||
subset = leftover[leftover['_fillerCat'] == cat]
|
||||
if not subset.empty:
|
||||
candidates_by_cat[cat] = subset
|
||||
if not candidates_by_cat:
|
||||
subset = leftover
|
||||
else:
|
||||
cat_choice = self._get_rng().choice(list(candidates_by_cat.keys()))
|
||||
subset = candidates_by_cat[cat_choice]
|
||||
if 'edhrecRank' in subset.columns:
|
||||
subset = subset.sort_values(by=['edhrecRank','manaValue'], ascending=[True, True], na_position='last')
|
||||
elif 'manaValue' in subset.columns:
|
||||
subset = subset.sort_values(by=['manaValue'], ascending=[True], na_position='last')
|
||||
row = subset.head(1)
|
||||
if row.empty:
|
||||
break
|
||||
r0 = row.iloc[0]
|
||||
nm = r0['name']
|
||||
self.add_card(
|
||||
nm,
|
||||
card_type=r0.get('type',''),
|
||||
mana_cost=r0.get('manaCost',''),
|
||||
mana_value=r0.get('manaValue', r0.get('cmc','')),
|
||||
tags=r0.get('themeTags', []) if isinstance(r0.get('themeTags', []), list) else [],
|
||||
role='filler',
|
||||
sub_role=r0.get('_fillerCat',''),
|
||||
added_by='spell_general_filler'
|
||||
)
|
||||
random_added.append(nm)
|
||||
leftover = leftover[leftover['name'] != nm]
|
||||
total_added += 1
|
||||
if total_added >= remaining:
|
||||
break
|
||||
if random_added:
|
||||
self.output_func(" General Utility Filler Added:")
|
||||
for nm in random_added:
|
||||
self.output_func(f" - {nm}")
|
||||
if total_added:
|
||||
self.output_func("\nFinal Theme Spell Fill:")
|
||||
for role, tag in themes_ordered:
|
||||
lst = per_theme_added.get(role, [])
|
||||
if lst:
|
||||
self.output_func(f" {role.title()} '{tag}': {len(lst)}")
|
||||
for nm in lst:
|
||||
self.output_func(f" - {nm}")
|
||||
self.output_func(f" Total Theme Spells Added: {total_added}")
|
||||
|
||||
# ---------------------------
|
||||
# Orchestrator
|
||||
# ---------------------------
|
||||
def add_non_creature_spells(self):
|
||||
"""Convenience orchestrator calling remaining non-creature spell categories then thematic fill."""
|
||||
self.add_ramp()
|
||||
self.add_removal()
|
||||
self.add_board_wipes()
|
||||
self.add_card_advantage()
|
||||
self.add_protection()
|
||||
self.fill_remaining_theme_spells()
|
||||
self.print_type_summary()
|
||||
Loading…
Add table
Add a link
Reference in a new issue