"""CampaignProgress model for Mantimon TCG. This module defines the CampaignProgress model for single-player campaign state. Tracks player progression through clubs, medals, and in-game economy. Example: progress = CampaignProgress( user_id=user.id, current_club="grass_club", medals=["grass_medal"], defeated_npcs=["grass_trainer_1", "grass_trainer_2"], mantibucks=500 ) """ from typing import TYPE_CHECKING from uuid import UUID from sqlalchemy import ForeignKey, Integer, String, UniqueConstraint from sqlalchemy.dialects.postgresql import JSONB from sqlalchemy.orm import Mapped, mapped_column, relationship from app.db.base import Base if TYPE_CHECKING: from app.db.models.user import User class CampaignProgress(Base): """Single-player campaign progress. Stores all state for a user's campaign playthrough including: - Current location in the campaign - Medals earned from defeating Club Leaders - NPCs defeated (for reward tracking) - Win/loss statistics - In-game economy (booster packs, Mantibucks) Attributes: id: Unique identifier (UUID). user_id: Foreign key to the user (unique - one campaign per user). current_club: ID of the club player is currently at. medals: JSONB list of earned medal IDs. defeated_npcs: JSONB list of defeated NPC IDs. total_wins: Total campaign wins. total_losses: Total campaign losses. booster_packs: Number of unopened booster packs. mantibucks: In-game currency balance. created_at: Record creation timestamp. updated_at: Record update timestamp. Relationships: user: The user this progress belongs to. Notes: - Each user has exactly one CampaignProgress - Defeating NPCs grants booster packs (first time only) - Mantibucks can be used for card purchases - 8 medals required to face Grand Masters """ __tablename__ = "campaign_progress" # Ownership (one-to-one with User) user_id: Mapped[UUID] = mapped_column( ForeignKey("users.id", ondelete="CASCADE"), nullable=False, unique=True, doc="Foreign key to user (unique - one campaign per user)", ) # Current location current_club: Mapped[str] = mapped_column( String(50), default="grass_club", nullable=False, doc="ID of current club location", ) # Progress tracking (JSONB for flexible lists) medals: Mapped[list] = mapped_column( JSONB, default=list, nullable=False, doc="List of earned medal IDs", ) defeated_npcs: Mapped[list] = mapped_column( JSONB, default=list, nullable=False, doc="List of defeated NPC IDs (for reward tracking)", ) # Statistics total_wins: Mapped[int] = mapped_column( Integer, default=0, nullable=False, doc="Total campaign wins", ) total_losses: Mapped[int] = mapped_column( Integer, default=0, nullable=False, doc="Total campaign losses", ) # Economy booster_packs: Mapped[int] = mapped_column( Integer, default=0, nullable=False, doc="Number of unopened booster packs", ) mantibucks: Mapped[int] = mapped_column( Integer, default=0, nullable=False, doc="In-game currency balance", ) # Relationship user: Mapped["User"] = relationship( "User", back_populates="campaign_progress", ) # Constraints __table_args__ = (UniqueConstraint("user_id", name="uq_campaign_user"),) @property def medal_count(self) -> int: """Get number of medals earned. Returns: Number of medals in collection. """ return len(self.medals) if self.medals else 0 @property def can_face_grand_masters(self) -> bool: """Check if player has enough medals for Grand Masters. Returns: True if player has 8+ medals. """ return self.medal_count >= 8 @property def win_rate(self) -> float: """Calculate win rate percentage. Returns: Win rate as decimal (0.0 to 1.0), or 0 if no games played. """ total = self.total_wins + self.total_losses return self.total_wins / total if total > 0 else 0.0 def has_defeated(self, npc_id: str) -> bool: """Check if an NPC has been defeated. Args: npc_id: ID of the NPC to check. Returns: True if NPC is in defeated list. """ return npc_id in (self.defeated_npcs or []) def has_medal(self, medal_id: str) -> bool: """Check if a medal has been earned. Args: medal_id: ID of the medal to check. Returns: True if medal is in collection. """ return medal_id in (self.medals or []) def __repr__(self) -> str: return ( f"" )