mirror of
https://github.com/mwisnowski/mtg_python_deckbuilder.git
synced 2025-09-22 04:50:46 +02:00
153 lines
6.9 KiB
Python
153 lines
6.9 KiB
Python
from __future__ import annotations
|
|
|
|
from typing import List
|
|
|
|
from .. import builder_constants as bc
|
|
from .. import builder_utils as bu
|
|
|
|
|
|
class LandOptimizationMixin:
|
|
"""Mixin for Land Building Step 8: ETB Tapped Minimization / Optimization Pass.
|
|
|
|
Provides optimize_tapped_lands and run_land_step8 (moved from monolithic builder).
|
|
"""
|
|
|
|
def optimize_tapped_lands(self): # type: ignore[override]
|
|
df = getattr(self, '_combined_cards_df', None)
|
|
if df is None or df.empty:
|
|
return
|
|
bracket_level = getattr(self, 'bracket_level', None)
|
|
threshold_map = getattr(bc, 'TAPPED_LAND_MAX_THRESHOLDS', {5:6,4:8,3:10,2:12,1:14})
|
|
threshold = threshold_map.get(bracket_level, 10)
|
|
|
|
name_to_row = {}
|
|
for _, row in df.iterrows():
|
|
nm = str(row.get('name',''))
|
|
if nm and nm not in name_to_row:
|
|
name_to_row[nm] = row.to_dict()
|
|
|
|
tapped_info = [] # (name, penalty, tapped_flag)
|
|
total_tapped = 0
|
|
for name, entry in list(self.card_library.items()):
|
|
row = name_to_row.get(name)
|
|
if not row:
|
|
continue
|
|
tline = str(row.get('type', row.get('type_line',''))).lower()
|
|
if 'land' not in tline:
|
|
continue
|
|
text_field = str(row.get('text', row.get('oracleText',''))).lower()
|
|
tapped_flag, penalty = bu.tapped_land_penalty(tline, text_field)
|
|
if tapped_flag:
|
|
total_tapped += 1
|
|
tapped_info.append((name, penalty, tapped_flag))
|
|
|
|
if total_tapped <= threshold:
|
|
self.output_func(f"Tapped Optimization (Step 8): {total_tapped} tapped/conditional lands (threshold {threshold}); no changes.")
|
|
return
|
|
|
|
over = total_tapped - threshold
|
|
swap_min_penalty = getattr(bc, 'TAPPED_LAND_SWAP_MIN_PENALTY', 6)
|
|
tapped_info.sort(key=lambda x: x[1], reverse=True)
|
|
to_consider = [t for t in tapped_info if t[1] >= swap_min_penalty]
|
|
if not to_consider:
|
|
self.output_func(f"Tapped Optimization (Step 8): Over threshold ({total_tapped}>{threshold}) but no suitable swaps (penalties too low).")
|
|
return
|
|
|
|
replacement_candidates: List[str] = []
|
|
seen = set(self.card_library.keys())
|
|
colors = [c for c in getattr(self, 'color_identity', []) if c in ['W','U','B','R','G']]
|
|
for _, row in df.iterrows():
|
|
try:
|
|
name = str(row.get('name',''))
|
|
if not name or name in seen or name in replacement_candidates:
|
|
continue
|
|
tline = str(row.get('type', row.get('type_line',''))).lower()
|
|
if 'land' not in tline:
|
|
continue
|
|
text_field = str(row.get('text', row.get('oracleText',''))).lower()
|
|
if 'enters the battlefield tapped' in text_field and 'you may pay 2 life' not in text_field and 'unless you control' not in text_field:
|
|
continue
|
|
produces_color = any(sym in text_field for sym in ['{w}','{u}','{b}','{r}','{g}'])
|
|
basic_types = [b for b in ['plains','island','swamp','mountain','forest'] if b in tline]
|
|
mapped = set()
|
|
for b in basic_types:
|
|
if b == 'plains':
|
|
mapped.add('W')
|
|
elif b == 'island':
|
|
mapped.add('U')
|
|
elif b == 'swamp':
|
|
mapped.add('B')
|
|
elif b == 'mountain':
|
|
mapped.add('R')
|
|
elif b == 'forest':
|
|
mapped.add('G')
|
|
if not produces_color and not (mapped & set(colors)):
|
|
continue
|
|
replacement_candidates.append(name)
|
|
except Exception:
|
|
continue
|
|
|
|
def repl_rank(name: str) -> int:
|
|
row = name_to_row.get(name, {})
|
|
tline = str(row.get('type', row.get('type_line','')))
|
|
text_field = str(row.get('text', row.get('oracleText','')))
|
|
return bu.replacement_land_score(name, tline, text_field)
|
|
replacement_candidates.sort(key=repl_rank, reverse=True)
|
|
|
|
swaps_made = []
|
|
idx_rep = 0
|
|
for name, penalty, _ in to_consider:
|
|
if over <= 0:
|
|
break
|
|
if not self._decrement_card(name):
|
|
continue
|
|
replacement = None
|
|
while idx_rep < len(replacement_candidates):
|
|
cand = replacement_candidates[idx_rep]
|
|
idx_rep += 1
|
|
if cand in getattr(bc, 'GENERIC_FETCH_LANDS', []) or any(cand in lst for lst in getattr(bc, 'COLOR_TO_FETCH_LANDS', {}).values()):
|
|
fetch_cap = getattr(bc, 'FETCH_LAND_MAX_CAP', 99)
|
|
existing_fetches = bu.count_existing_fetches(self.card_library)
|
|
if existing_fetches >= fetch_cap:
|
|
continue
|
|
replacement = cand
|
|
break
|
|
if replacement is None:
|
|
basics = self._basic_land_names()
|
|
basic_counts = {b: self.card_library.get(b, {}).get('Count',0) for b in basics}
|
|
color_basic_map = {'W':'Plains','U':'Island','B':'Swamp','R':'Mountain','G':'Forest'}
|
|
usable_basics = [color_basic_map[c] for c in colors if color_basic_map[c] in basics]
|
|
usable_basics.sort(key=lambda b: basic_counts.get(b,0))
|
|
replacement = usable_basics[0] if usable_basics else 'Wastes'
|
|
self.add_card(
|
|
replacement,
|
|
card_type='Land',
|
|
role='optimized',
|
|
sub_role='swap-in',
|
|
added_by='lands_step8',
|
|
trigger_tag='tapped_optimization'
|
|
)
|
|
swaps_made.append((name, replacement))
|
|
over -= 1
|
|
|
|
if not swaps_made:
|
|
self.output_func(f"Tapped Optimization (Step 8): Could not perform swaps; over threshold {total_tapped}>{threshold}.")
|
|
return
|
|
self.output_func("\nTapped Optimization (Step 8) Swaps:")
|
|
for old, new in swaps_made:
|
|
self.output_func(f" Replaced {old} -> {new}")
|
|
new_tapped = 0
|
|
for name, entry in self.card_library.items():
|
|
row = name_to_row.get(name)
|
|
if not row:
|
|
continue
|
|
text_field = str(row.get('text', row.get('oracleText',''))).lower()
|
|
if 'enters the battlefield tapped' in text_field and 'you may pay 2 life' not in text_field:
|
|
new_tapped += 1
|
|
self.output_func(f" Tapped Lands After : {new_tapped} (threshold {threshold})")
|
|
|
|
def run_land_step8(self): # type: ignore[override]
|
|
self.optimize_tapped_lands()
|
|
self._enforce_land_cap(step_label="Tapped Opt (Step 8)")
|
|
if self.color_source_matrix_baseline is None:
|
|
self.color_source_matrix_baseline = self._compute_color_source_matrix()
|