Spaces:
Sleeping
Sleeping
| from dataclasses import dataclass | |
| from random import Random | |
| from typing import Sequence | |
| from primitives import Effect, PrimitiveId, School, primitive_allowed_for_school, render_effect | |
| MAX_ENERGY_COST = 5 | |
| MIRACLE_BUDGET_CAP = 8 | |
| class EffectPlan: | |
| primitive_id: PrimitiveId | |
| weight: int = 1 | |
| class CardSpec: | |
| name: str | |
| cost: int | |
| school: School | |
| theme: str | |
| effect_plans: tuple[EffectPlan, ...] | |
| flavor: str = "" | |
| art_prompt: str = "" | |
| miracle: bool = False | |
| class Card: | |
| name: str | |
| cost: int | |
| school: School | |
| theme: str | |
| effects: tuple[Effect, ...] | |
| flavor: str = "" | |
| art_prompt: str = "" | |
| art_uri: str = "" | |
| # Return fixed, parseable rules text for the card. | |
| def rules_text(self) -> str: | |
| return " ".join(render_effect(effect) for effect in self.effects) | |
| # Convert energy cost into point budget. | |
| def budget_for_energy(cost: int) -> int: | |
| if cost < 1 or cost > MAX_ENERGY_COST: | |
| raise ValueError(f"card cost must be 1..{MAX_ENERGY_COST}") | |
| return cost | |
| # Roll the bounded high-variance miracle budget. | |
| def miracle_budget(cost: int, rng: Random) -> int: | |
| return min(MIRACLE_BUDGET_CAP, budget_for_energy(cost) * rng.randint(1, 3)) | |
| # Build a fully costed card from model-chosen effect shapes. | |
| def cost_card(spec: CardSpec, rng: Random | None = None) -> Card: | |
| validate_card_spec(spec) | |
| point_budget = miracle_budget(spec.cost, rng or Random()) if spec.miracle else budget_for_energy(spec.cost) | |
| effects = cost_effects(point_budget, spec.effect_plans, spec.school) | |
| return Card(spec.name, spec.cost, spec.school, spec.theme, effects, spec.flavor, spec.art_prompt) | |
| # Build fully costed effects from weighted primitive plans. | |
| def cost_effects(point_budget: int, plans: Sequence[EffectPlan], school: School | None = None) -> tuple[Effect, ...]: | |
| usable_plans = tuple(plans[:point_budget]) | |
| total_weight = sum(plan.weight for plan in usable_plans) | |
| taxed = len(usable_plans) > 1 | |
| effects: list[Effect] = [] | |
| spent = 0 | |
| for index, plan in enumerate(usable_plans): | |
| remaining = point_budget - spent | |
| points = remaining if index == len(usable_plans) - 1 else max(1, point_budget * plan.weight // total_weight) | |
| spent += points | |
| effects.append(cost_effect(plan.primitive_id, points, taxed, school)) | |
| return tuple(effects) | |
| # Build one fully costed effect from an allocated point budget. | |
| def cost_effect(primitive_id: PrimitiveId, points: int, taxed: bool = False, school: School | None = None) -> Effect: | |
| if points < 1: | |
| raise ValueError("effect points must be positive") | |
| match primitive_id: | |
| case "deal": | |
| return Effect("deal", amount=damage_amount(points, taxed)) | |
| case "burn": | |
| return Effect("burn", amount=burn_amount(points, taxed), duration=2) | |
| case "bomb": | |
| return Effect("bomb", amount=damage_amount(points, taxed), delay=3) | |
| case "block": | |
| return Effect("block", amount=points * (2 if taxed else 3)) | |
| case "ward": | |
| return Effect("ward", amount=points) | |
| case "weak": | |
| return Effect("weak", amount=damage_amount(points, taxed), duration=1) | |
| case "draw": | |
| return Effect("draw", amount=max(1, points * 2 // 3)) | |
| case "energy": | |
| return Effect("energy", amount=max(1, points // 2)) | |
| case "initiative": | |
| return Effect("initiative", duration=1) | |
| case "multi_hit": | |
| return Effect("multi_hit", amount=max(1, points), hits=2 if points < 4 else 3) | |
| case "vulnerable": | |
| return Effect("vulnerable", amount=max(1, points), duration=1 if points < 4 else 2) | |
| case "conditional": | |
| return Effect("conditional", amount=conditional_amount(points, taxed), condition="opponent_below_half") | |
| case "scaling": | |
| return Effect("scaling", amount=points, condition=scaling_condition(school)) | |
| # Return direct damage for a point allocation. | |
| def damage_amount(points: int, taxed: bool) -> int: | |
| return max(1, points * 2 - (1 if taxed else 0)) | |
| # Return burn damage per tick for a point allocation. | |
| def burn_amount(points: int, taxed: bool) -> int: | |
| return max(1, points - (1 if taxed and points > 1 else 0)) | |
| # Return maximum scaled conditional damage for a point allocation. | |
| def conditional_amount(points: int, taxed: bool) -> int: | |
| return max(1, points - (1 if taxed and points > 1 else 0)) | |
| # Return the scaling counter used by a school. | |
| def scaling_condition(school: School | None) -> str: | |
| return "shield_charge" if school == "earth" else "cards_played" | |
| # Validate a model-authored card spec before costing it. | |
| def validate_card_spec(spec: CardSpec) -> None: | |
| budget_for_energy(spec.cost) | |
| if not spec.effect_plans: | |
| raise ValueError("card must have at least one effect") | |
| for plan in spec.effect_plans: | |
| validate_effect_plan(plan, spec.school) | |
| # Validate one model-chosen effect plan. | |
| def validate_effect_plan(plan: EffectPlan, school: School) -> None: | |
| if plan.weight < 1: | |
| raise ValueError("effect weight must be positive") | |
| if not primitive_allowed_for_school(plan.primitive_id, school): | |
| raise ValueError(f"{plan.primitive_id} is not allowed for {school}") | |