from __future__ import annotations from typing import List, Optional import pandas as pd from .phase0_core import BracketDefinition, BRACKET_DEFINITIONS # noqa: F401 """Phase 1: Commander & Tag Selection logic. Extracted from builder.py to reduce monolith size. All public method names and signatures preserved; DeckBuilder will delegate to these mixin-style functions. Provided functions expect `self` to be a DeckBuilder instance exposing: - input_func, output_func - load_commander_data, _auto_accept, _gather_candidates, _format_commander_pretty, _initialize_commander_dict - commander_name, commander_row, commander_tags, commander_dict - selected_tags, primary_tag, secondary_tag, tertiary_tag - bracket_definition related attributes for printing summary No side‑effect changes. """ # (Imports moved to top for lint compliance) class CommanderSelectionMixin: # --------------------------- # Commander Selection # --------------------------- def _normalize_commander_query(self, s: str) -> str: """Return a nicely capitalized search string (e.g., "inti, seneschal of the sun" -> "Inti, Seneschal of the Sun"). Keeps small words lowercase unless at a segment start, and capitalizes parts around hyphens/apostrophes. """ if not isinstance(s, str): return str(s) s = s.strip() if not s: return s small = { 'a','an','and','as','at','but','by','for','in','of','on','or','the','to','vs','v','with','from','into','over','per' } # Consider a new segment after these punctuation marks segment_breakers = {':',';','-','–','—','/','\\','(', '[', '{', '"', "'", ',', '.'} out_words: list[str] = [] start_of_segment = True for raw in s.lower().split(): word = raw # If preceding token ended with a breaker, reset segment if out_words: prev = out_words[-1] if prev and prev[-1] in segment_breakers: start_of_segment = True def cap_subparts(token: str) -> str: # Capitalize around hyphens and apostrophes def cap_piece(piece: str) -> str: return piece[:1].upper() + piece[1:] if piece else piece parts = [cap_piece(p) for p in token.split("'")] token2 = "'".join(parts) parts2 = [cap_piece(p) for p in token2.split('-')] return '-'.join(parts2) if start_of_segment or word not in small: fixed = cap_subparts(word) else: fixed = word out_words.append(fixed) # Next word is not start unless current ends with breaker start_of_segment = word[-1:] in segment_breakers # Post-process to ensure first character is capitalized if needed if out_words: out_words[0] = out_words[0][:1].upper() + out_words[0][1:] return ' '.join(out_words) def choose_commander(self) -> str: df = self.load_commander_data() names = df["name"].tolist() while True: query = self.input_func("Enter commander name: ").strip() query = self._normalize_commander_query(query) if not query: self.output_func("No input provided. Try again.") continue direct_hits = [n for n in names if self._auto_accept(query, n)] if len(direct_hits) == 1: candidate = direct_hits[0] self.output_func(f"(Auto match candidate) {candidate}") if self._present_commander_and_confirm(df, candidate): self.output_func(f"Confirmed: {candidate}") return candidate else: self.output_func("Not confirmed. Starting over.\n") continue candidates = self._gather_candidates(query, names) if not candidates: self.output_func("No close matches found. Try again.") continue self.output_func("\nTop matches:") for idx, (n, score) in enumerate(candidates, start=1): self.output_func(f" {idx}. {n} (score {score})") self.output_func("Enter number to inspect, 'r' to retry, or type a new name:") choice = self.input_func("Selection: ").strip() if choice.lower() == 'r': continue if choice.isdigit(): i = int(choice) if 1 <= i <= len(candidates): nm = candidates[i - 1][0] if self._present_commander_and_confirm(df, nm): self.output_func(f"Confirmed: {nm}") return nm else: self.output_func("Not confirmed. Search again.\n") continue else: self.output_func("Invalid index.") continue query = self._normalize_commander_query(choice) # treat as new (normalized) query def _present_commander_and_confirm(self, df: pd.DataFrame, name: str) -> bool: row = df[df["name"] == name].iloc[0] pretty = self._format_commander_pretty(row) self.output_func("\n" + pretty) while True: resp = self.input_func("Is this the commander you want? (y/n): ").strip().lower() if resp in ("y", "yes"): self._apply_commander_selection(row) return True if resp in ("n", "no"): return False self.output_func("Please enter y or n.") def _apply_commander_selection(self, row: pd.Series): self.commander_name = row["name"] self.commander_row = row tags_value = row.get("themeTags", []) self.commander_tags = list(tags_value) if tags_value is not None else [] self._initialize_commander_dict(row) # --------------------------- # Tag Prioritization # --------------------------- def select_commander_tags(self) -> List[str]: if not self.commander_name: self.output_func("No commander chosen yet. Selecting commander first...") self.choose_commander() tags = list(dict.fromkeys(self.commander_tags)) if not tags: self.output_func("Commander has no theme tags available.") self.selected_tags = [] self.primary_tag = self.secondary_tag = self.tertiary_tag = None self._update_commander_dict_with_selected_tags() return self.selected_tags self.output_func("\nAvailable Theme Tags:") for i, t in enumerate(tags, 1): self.output_func(f" {i}. {t}") self.selected_tags = [] self.primary_tag = self._prompt_tag_choice(tags, "Select PRIMARY tag (required):", allow_stop=False) self.selected_tags.append(self.primary_tag) remaining = [t for t in tags if t not in self.selected_tags] if remaining: self.secondary_tag = self._prompt_tag_choice(remaining, "Select SECONDARY tag (or 0 to stop here):", allow_stop=True) if self.secondary_tag: self.selected_tags.append(self.secondary_tag) remaining = [t for t in remaining if t != self.secondary_tag] if remaining and self.secondary_tag: self.tertiary_tag = self._prompt_tag_choice(remaining, "Select TERTIARY tag (or 0 to stop here):", allow_stop=True) if self.tertiary_tag: self.selected_tags.append(self.tertiary_tag) self.output_func("\nChosen Tags (in priority order):") if not self.selected_tags: self.output_func(" (None)") else: for idx, tag in enumerate(self.selected_tags, 1): label = ["Primary", "Secondary", "Tertiary"][idx - 1] if idx <= 3 else f"Tag {idx}" self.output_func(f" {idx}. {tag} ({label})") self._update_commander_dict_with_selected_tags() return self.selected_tags def _prompt_tag_choice(self, available: List[str], prompt_text: str, allow_stop: bool) -> Optional[str]: while True: self.output_func("\nCurrent options:") for i, t in enumerate(available, 1): self.output_func(f" {i}. {t}") if allow_stop: self.output_func(" 0. Stop (no further tags)") raw = self.input_func(f"{prompt_text} ").strip() if allow_stop and raw == "0": return None if raw.isdigit(): idx = int(raw) if 1 <= idx <= len(available): return available[idx - 1] matches = [t for t in available if t.lower() == raw.lower()] if matches: return matches[0] self.output_func("Invalid selection. Try again.") def _update_commander_dict_with_selected_tags(self): if not self.commander_dict and self.commander_row is not None: self._initialize_commander_dict(self.commander_row) if not self.commander_dict: return self.commander_dict["Primary Tag"] = self.primary_tag self.commander_dict["Secondary Tag"] = self.secondary_tag self.commander_dict["Tertiary Tag"] = self.tertiary_tag self.commander_dict["Selected Tags"] = self.selected_tags.copy() # --------------------------- # Power Bracket Selection # --------------------------- def select_power_bracket(self) -> BracketDefinition: if self.bracket_definition: return self.bracket_definition self.output_func("\nChoose Deck Power Bracket:") for bd in BRACKET_DEFINITIONS: self.output_func(f" {bd.level}. {bd.name} - {bd.short_desc}") while True: raw = self.input_func("Enter bracket number (1-5) or 'info' for details: ").strip().lower() if raw == "info": self._print_bracket_details() continue if raw.isdigit(): num = int(raw) match = next((bd for bd in BRACKET_DEFINITIONS if bd.level == num), None) if match: self.bracket_definition = match self.bracket_level = match.level self.bracket_name = match.name self.bracket_limits = match.limits.copy() self.output_func(f"\nSelected Bracket {match.level}: {match.name}") self._print_selected_bracket_summary() return match self.output_func("Invalid input. Type 1-5 or 'info'.") def _print_bracket_details(self): self.output_func("\nBracket Details:") for bd in BRACKET_DEFINITIONS: self.output_func(f"\n[{bd.level}] {bd.name}") self.output_func(bd.long_desc) self.output_func(self._format_limits(bd.limits)) def _print_selected_bracket_summary(self): self.output_func("\nBracket Constraints:") if self.bracket_limits: self.output_func(self._format_limits(self.bracket_limits)) __all__ = [ 'CommanderSelectionMixin' ]