mtg_python_deckbuilder/code/deck_builder/phases/phase2_lands_kindred.py

158 lines
7.2 KiB
Python
Raw Normal View History

from __future__ import annotations
from typing import List, Dict
from .. import builder_constants as bc
"""Phase 2 (part 3): Kindred / tribal land additions (Land Step 3).
Extracted from `builder.py` to reduce monolith size. Focuses on lands that care
about creature types or tribal synergies when a selected tag includes 'Kindred' or 'Tribal'.
Provided by LandKindredMixin:
- add_kindred_lands()
- run_land_step3()
Host DeckBuilder must provide:
- attributes: selected_tags, commander_tags, color_identity, ideal_counts, commander_row,
card_library, _full_cards_df
- methods: determine_color_identity(), setup_dataframes(), add_card(), _current_land_count(),
_basic_floor(), _count_basic_lands(), _choose_basic_to_trim(), _decrement_card(),
_enforce_land_cap(), output_func
"""
class LandKindredMixin:
def add_kindred_lands(self): # type: ignore[override]
"""Add kindred-oriented lands ONLY if a selected tag includes 'Kindred' or 'Tribal'.
Baseline inclusions on kindred focus:
- Path of Ancestry (always when kindred)
- Cavern of Souls (<=4 colors)
- Three Tree City (>=2 colors)
Dynamic tribe-specific lands: derived only from selected tags (not all commander tags).
Capacity: may swap excess basics (above 90% floor) similar to other steps.
"""
if not getattr(self, 'files_to_load', []):
try:
self.determine_color_identity()
self.setup_dataframes()
except Exception as e: # pragma: no cover - defensive
self.output_func(f"Cannot add kindred lands until color identity resolved: {e}")
return
if not any(('kindred' in t.lower() or 'tribal' in t.lower()) for t in (getattr(self, 'selected_tags', []) or [])):
self.output_func("Kindred Lands: No selected kindred/tribal tag; skipping.")
return
if hasattr(self, 'ideal_counts') and getattr(self, 'ideal_counts'):
land_target = self.ideal_counts.get('lands', getattr(bc, 'DEFAULT_LAND_COUNT', 35)) # type: ignore[attr-defined]
else:
land_target = getattr(bc, 'DEFAULT_LAND_COUNT', 35)
min_basic_cfg = getattr(bc, 'DEFAULT_BASIC_LAND_COUNT', 20)
if hasattr(self, 'ideal_counts') and getattr(self, 'ideal_counts'):
min_basic_cfg = self.ideal_counts.get('basic_lands', min_basic_cfg) # type: ignore[attr-defined]
basic_floor = self._basic_floor(min_basic_cfg) # type: ignore[attr-defined]
def ensure_capacity() -> bool:
if self._current_land_count() < land_target: # type: ignore[attr-defined]
return True
if self._count_basic_lands() <= basic_floor: # type: ignore[attr-defined]
return False
target_basic = self._choose_basic_to_trim() # type: ignore[attr-defined]
if not target_basic:
return False
if not self._decrement_card(target_basic): # type: ignore[attr-defined]
return False
return self._current_land_count() < land_target # type: ignore[attr-defined]
colors = getattr(self, 'color_identity', []) or []
added: List[str] = []
reasons: Dict[str, str] = {}
def try_add(name: str, reason: str):
if name in self.card_library: # type: ignore[attr-defined]
return
if not ensure_capacity():
return
self.add_card(
name,
card_type='Land',
role='kindred',
sub_role='baseline' if reason.startswith('kindred focus') else 'tribe-specific',
added_by='lands_step3',
trigger_tag='Kindred/Tribal'
) # type: ignore[attr-defined]
added.append(name)
reasons[name] = reason
# Baseline inclusions
try_add('Path of Ancestry', 'kindred focus')
if len(colors) <= 4:
try_add('Cavern of Souls', f"kindred focus ({len(colors)} colors)")
if len(colors) >= 2:
try_add('Three Tree City', f"kindred focus ({len(colors)} colors)")
# Dynamic tribe extraction
tribe_terms: set[str] = set()
for tag in (getattr(self, 'selected_tags', []) or []):
lower = tag.lower()
if 'kindred' in lower:
base = lower.replace('kindred', '').strip()
if base:
tribe_terms.add(base.split()[0])
elif 'tribal' in lower:
base = lower.replace('tribal', '').strip()
if base:
tribe_terms.add(base.split()[0])
snapshot = getattr(self, '_full_cards_df', None)
if snapshot is not None and not snapshot.empty and tribe_terms:
dynamic_limit = 5
for tribe in sorted(tribe_terms):
if self._current_land_count() >= land_target or dynamic_limit <= 0: # type: ignore[attr-defined]
break
tribe_lower = tribe.lower()
matches: List[str] = []
for _, row in snapshot.iterrows():
try:
nm = str(row.get('name', ''))
if not nm or nm in self.card_library: # type: ignore[attr-defined]
continue
tline = str(row.get('type', row.get('type_line', ''))).lower()
if 'land' not in tline:
continue
text_field = row.get('text', row.get('oracleText', ''))
text_str = str(text_field).lower() if text_field is not None else ''
nm_lower = nm.lower()
if (tribe_lower in nm_lower or f" {tribe_lower}" in text_str or f"{tribe_lower} " in text_str or f"{tribe_lower}s" in text_str):
matches.append(nm)
except Exception:
continue
for nm in matches[:2]:
if self._current_land_count() >= land_target or dynamic_limit <= 0: # type: ignore[attr-defined]
break
if nm in added or nm in getattr(bc, 'BASIC_LANDS', []):
continue
try_add(nm, f"text/name references '{tribe}'")
dynamic_limit -= 1
self.output_func("\nKindred Lands Added (Step 3):")
if not added:
self.output_func(" (None added)")
else:
width = max(len(n) for n in added)
for n in added:
self.output_func(f" {n.ljust(width)} : 1 ({reasons.get(n,'')})")
self.output_func(f" Land Count Now : {self._current_land_count()} / {land_target}") # type: ignore[attr-defined]
def run_land_step3(self): # type: ignore[override]
"""Public wrapper to add kindred-focused lands."""
self.add_kindred_lands()
self._enforce_land_cap(step_label="Kindred (Step 3)") # type: ignore[attr-defined]
try:
from .. import builder_utils as _bu
_bu.export_current_land_pool(self, '3')
except Exception:
pass
__all__ = [
'LandKindredMixin'
]