Spaces:
Running
Running
| import base64 | |
| import re | |
| from pathlib import Path | |
| from random import Random | |
| from typing import Callable | |
| from budget import Card | |
| from generator import CardModelClient, CardPackClient, generate_card, generate_pack | |
| from primitives import Effect | |
| from primitives import School | |
| PackChooser = Callable[[tuple[Card, ...], tuple[Card, ...]], int] | |
| _CARD_ART = Path(__file__).parent / "assets" / "cards" | |
| _backbone_art_cache: dict[str, str] = {} | |
| # Return the pre-baked art data URI for a backbone card. The six backbone cards are | |
| # identical every run, so their art is generated once and bundled (no live SDXL call, | |
| # no shimmer). | |
| def prebaked_backbone_art(name: str, theme: str) -> str: | |
| world = re.sub(r"[^a-z0-9]+", "", theme.lower()) | |
| card = re.sub(r"[^a-z0-9]+", "_", name.lower()).strip("_") | |
| key = f"{world}__{card}" | |
| if key not in _backbone_art_cache: | |
| path = _CARD_ART / f"{key}.jpg" | |
| _backbone_art_cache[key] = ( | |
| "data:image/jpeg;base64," + base64.b64encode(path.read_bytes()).decode("ascii") if path.exists() else "" | |
| ) | |
| return _backbone_art_cache[key] | |
| BACKBONE_CARDS: tuple[tuple[str, int, Effect], ...] = ( | |
| ("Attack (weak)", 1, Effect("deal", amount=2)), | |
| ("Attack (medium)", 3, Effect("deal", amount=4)), | |
| ("Attack (strong)", 5, Effect("deal", amount=6)), | |
| ("Block (weak)", 2, Effect("block", amount=3)), | |
| ("Block (medium)", 3, Effect("block", amount=4)), | |
| ("Draw (small)", 2, Effect("draw", amount=1)), | |
| ) | |
| # Build the six standardized backbone cards for a run. | |
| def backbone_deck(school: School, theme: str) -> tuple[Card, ...]: | |
| return tuple(backbone_card(name, cost, effect, school, theme) for name, cost, effect in BACKBONE_CARDS) | |
| # Build one standardized backbone card, with pre-baked art so it never shimmers. | |
| def backbone_card(name: str, cost: int, effect: Effect, school: School, theme: str) -> Card: | |
| return Card(name, cost, school, theme, (effect,), flavor=f"Standard {name.lower()}.", art_uri=prebaked_backbone_art(name, theme)) | |
| # Build a full 15-card text deck with nine generated synergy cards. | |
| def draft_deck(client: CardModelClient, school: School, theme: str, costs: tuple[int, ...]) -> tuple[Card, ...]: | |
| if len(costs) != 9: | |
| raise ValueError("draft needs exactly nine synergy costs") | |
| deck = list(backbone_deck(school, theme)) | |
| for cost in costs: | |
| deck.append(generate_card(client, school, theme, deck, cost)) | |
| return tuple(deck) | |
| # Build a full deck by drafting one card from each generated pack. | |
| def draft_deck_from_packs( | |
| client: CardPackClient, | |
| school: School, | |
| theme: str, | |
| costs: tuple[int, ...], | |
| choose_index: PackChooser, | |
| rng: Random | None = None, | |
| ) -> tuple[Card, ...]: | |
| if len(costs) != 9: | |
| raise ValueError("draft needs exactly nine synergy costs") | |
| deck = list(backbone_deck(school, theme)) | |
| anchors: list[Card] = [] | |
| anchor_indexes = draft_anchor_indexes(len(costs), rng or Random()) | |
| for index in draft_order(len(costs), anchor_indexes): | |
| pack = generate_pack(client, school, theme, deck, costs[index], draft_anchors=anchors) | |
| selected = pack[validated_pack_choice(choose_index(tuple(deck), pack), pack)] | |
| deck.append(selected) | |
| if index in anchor_indexes: | |
| anchors.append(selected) | |
| return tuple(deck) | |
| # Return random synergy-card indexes that anchor the rest of the draft. | |
| def draft_anchor_indexes(count: int, rng: Random) -> frozenset[int]: | |
| if count < 2: | |
| raise ValueError("draft needs at least two anchor candidates") | |
| return frozenset(rng.sample(range(count), 2)) | |
| # Return anchor picks first, then the remaining picks in cost order. | |
| def draft_order(count: int, anchor_indexes: frozenset[int]) -> tuple[int, ...]: | |
| anchors = tuple(sorted(anchor_indexes)) | |
| remaining = tuple(index for index in range(count) if index not in anchor_indexes) | |
| return anchors + remaining | |
| # Return a valid draft pack index or reject it. | |
| def validated_pack_choice(index: int, pack: tuple[Card, ...]) -> int: | |
| if index < 0 or index >= len(pack): | |
| raise ValueError("draft choice is outside the pack") | |
| return index | |