# Mantimon TCG - Technical Architecture

This document provides a detailed technical overview of the Mantimon TCG architecture.

## Table of Contents

1. [System Overview](#system-overview)
2. [Frontend Architecture](#frontend-architecture)
3. [Backend Architecture](#backend-architecture)
4. [Database Schema](#database-schema)
5. [Real-Time Communication](#real-time-communication)
6. [Game Engine](#game-engine)
7. [AI System](#ai-system)
8. [Security Considerations](#security-considerations)
9. [Offline Standalone Fork](#offline-standalone-fork)

---

## System Overview

```
+------------------+     +------------------+     +------------------+
|     Browser      |     |     Browser      |     |     Browser      |
|  (Vue + Phaser)  |     |  (Vue + Phaser)  |     |  (Vue + Phaser)  |
+--------+---------+     +--------+---------+     +--------+---------+
         |                        |                        |
         +------------------------+------------------------+
                                  |
                         WebSocket + HTTP
                                  |
                    +-------------+-------------+
                    |                           |
           +--------v--------+         +--------v--------+
           |    FastAPI      |         |    FastAPI      |
           |   Instance 1    |         |   Instance 2    |
           +--------+--------+         +--------+--------+
                    |                           |
                    +-------------+-------------+
                                  |
              +-------------------+-------------------+
              |                   |                   |
      +-------v-------+   +-------v-------+   +------v------+
      |  PostgreSQL   |   |     Redis     |   |   S3/CDN    |
      |  (persistent) |   |   (realtime)  |   | (card art)  |
      +---------------+   +---------------+   +-------------+
```

---

## Frontend Architecture

### Technology Stack

- **Vue 3** with Composition API and `<script setup>` syntax
- **Phaser 3** for canvas-based game rendering
- **TypeScript** for type safety
- **Pinia** for state management
- **Tailwind CSS** for styling
- **Vite** for development and building

### Component Hierarchy

```
App.vue
├── layouts/
│   ├── DefaultLayout.vue      # Nav, footer for non-game pages
│   └── GameLayout.vue         # Minimal chrome for gameplay
├── pages/
│   ├── index.vue              # Landing/home
│   ├── login.vue              # Authentication
│   ├── collection.vue         # Card collection browser
│   ├── decks/
│   │   ├── index.vue          # Deck list
│   │   └── [id].vue           # Deck builder/editor
│   ├── play/
│   │   ├── index.vue          # Play menu (AI, PvP, Puzzle)
│   │   ├── match/[id].vue     # Active game (mounts Phaser)
│   │   └── puzzle/[id].vue    # Puzzle mode
│   ├── packs.vue              # Pack opening (mounts Phaser)
│   └── profile.vue            # User stats and settings
└── components/
    ├── cards/
    │   ├── CardDisplay.vue    # Single card render (DOM)
    │   ├── CardGrid.vue       # Grid of cards
    │   └── CardDetail.vue     # Full card modal
    ├── decks/
    │   ├── DeckList.vue       # List of user's decks
    │   └── DeckBuilder.vue    # Deck editing UI
    ├── game/
    │   └── PhaserContainer.vue # Mounts Phaser canvas
    └── common/
        ├── Modal.vue
        ├── Button.vue
        └── Loading.vue
```

### Vue + Phaser Integration

Phaser is mounted within a Vue component and communicates via an event bridge:

```typescript
// components/game/PhaserContainer.vue
<script setup lang="ts">
import { onMounted, onUnmounted, ref } from 'vue'
import Phaser from 'phaser'
import { MatchScene } from '@/game/scenes/MatchScene'
import { useGameStore } from '@/stores/game'

const props = defineProps<{
  gameId: string
}>()

const gameContainer = ref<HTMLDivElement>()
const phaserGame = ref<Phaser.Game>()
const gameStore = useGameStore()

onMounted(() => {
  phaserGame.value = new Phaser.Game({
    type: Phaser.AUTO,
    parent: gameContainer.value,
    width: 1280,
    height: 720,
    scene: [MatchScene],
    backgroundColor: '#1a1a2e',
  })
  
  // Pass initial state to Phaser
  phaserGame.value.registry.set('gameId', props.gameId)
  
  // Bridge: Vue -> Phaser
  gameStore.$onAction(({ name, args }) => {
    if (name === 'setGameState') {
      phaserGame.value?.events.emit('state:update', args[0])
    }
  })
  
  // Bridge: Phaser -> Vue
  phaserGame.value.events.on('player:action', (action) => {
    gameStore.sendAction(action)
  })
})

onUnmounted(() => {
  phaserGame.value?.destroy(true)
})
</script>

<template>
  <div ref="gameContainer" class="w-full h-full" />
</template>
```

### Phaser Scene Structure

```
game/
├── scenes/
│   ├── MatchScene.ts          # Main gameplay
│   ├── PackOpeningScene.ts    # Animated pack reveals
│   └── PuzzleScene.ts         # Puzzle mode
├── objects/
│   ├── Card.ts                # Card game object
│   ├── Hand.ts                # Hand container
│   ├── Battlefield.ts         # Active/bench zones
│   ├── Deck.ts                # Deck pile with animations
│   └── DamageCounter.ts       # Damage display
├── animations/
│   ├── cardAnimations.ts      # Draw, play, flip
│   ├── attackAnimations.ts    # Attack effects
│   └── shuffleAnimations.ts   # Deck shuffle
└── utils/
    └── tweenHelpers.ts        # Common tween patterns
```

### State Management (Pinia)

```typescript
// stores/game.ts
export const useGameStore = defineStore('game', () => {
  // State
  const gameState = ref<GameState | null>(null)
  const pendingAction = ref<Action | null>(null)
  const socket = ref<Socket | null>(null)
  
  // Getters
  const myHand = computed(() => gameState.value?.myHand ?? [])
  const isMyTurn = computed(() => 
    gameState.value?.currentPlayer === gameState.value?.myPlayerId
  )
  const canPlayCard = computed(() => 
    isMyTurn.value && gameState.value?.phase === 'main'
  )
  
  // Actions
  async function connectToGame(gameId: string) {
    socket.value = io('/game', { query: { gameId } })
    
    socket.value.on('game:state', (state) => {
      gameState.value = state
    })
    
    socket.value.on('game:error', (error) => {
      // Handle error
    })
  }
  
  function sendAction(action: Action) {
    socket.value?.emit('game:action', action)
  }
  
  return {
    gameState,
    myHand,
    isMyTurn,
    canPlayCard,
    connectToGame,
    sendAction,
  }
})
```

---

## Backend Architecture

### Technology Stack

- **FastAPI** for REST API and WebSocket handling
- **Python 3.11+** with async/await
- **SQLAlchemy 2.0** (async) for ORM
- **PostgreSQL** for persistent storage
- **Redis** for caching and real-time state
- **python-socketio** for WebSocket server
- **Pydantic v2** for validation

### Module Structure

```
backend/
├── app/
│   ├── main.py                 # FastAPI app, Socket.io setup
│   ├── config.py               # Settings (pydantic-settings)
│   ├── dependencies.py         # Dependency injection
│   │
│   ├── api/                    # REST endpoints
│   │   ├── auth.py             # Login, register, OAuth
│   │   ├── cards.py            # Card definitions
│   │   ├── collection.py       # User collection CRUD
│   │   ├── decks.py            # Deck CRUD
│   │   ├── packs.py            # Pack opening
│   │   ├── matchmaking.py      # Queue management
│   │   └── puzzles.py          # Puzzle definitions
│   │
│   ├── websocket/              # Real-time handlers
│   │   ├── handlers.py         # Socket.io event handlers
│   │   ├── connection_manager.py
│   │   └── events.py           # Event type definitions
│   │
│   ├── core/                   # Game engine
│   │   ├── game_engine.py      # Main orchestrator
│   │   ├── turn_manager.py     # Turn/phase state machine
│   │   ├── card_effects.py     # Effect resolution
│   │   ├── rules_validator.py  # Action legality
│   │   ├── win_conditions.py   # Win/loss detection
│   │   ├── state_manager.py    # In-memory game state
│   │   └── ai/                 # AI opponents
│   │       ├── base.py         # AI interface
│   │       ├── easy.py         # Random legal moves
│   │       ├── medium.py       # Heuristic-based
│   │       └── hard.py         # MCTS/minimax
│   │
│   ├── models/                 # Data models
│   │   ├── database.py         # SQLAlchemy models
│   │   └── schemas.py          # Pydantic schemas
│   │
│   ├── services/               # Business logic
│   │   ├── auth_service.py
│   │   ├── card_service.py
│   │   ├── collection_service.py
│   │   ├── deck_service.py
│   │   ├── pack_service.py
│   │   ├── game_service.py
│   │   └── puzzle_service.py
│   │
│   └── database/               # DB utilities
│       ├── session.py          # Async session factory
│       └── migrations/         # Alembic migrations
│
├── tests/
│   ├── test_game_engine.py
│   ├── test_card_effects.py
│   ├── test_ai.py
│   └── conftest.py
│
├── pyproject.toml
└── alembic.ini
```

### Request Flow

```
HTTP Request
     │
     v
FastAPI Router ─────────────────────────────┐
     │                                      │
     v                                      v
Dependency Injection              WebSocket Connection
(auth, db session)                      │
     │                                  v
     v                           Socket.io Handler
Service Layer                          │
     │                                 v
     v                           Game Engine
Repository/ORM                   (in-memory state)
     │                                 │
     v                                 v
PostgreSQL                          Redis
                                (state cache)
```

---

## Database Schema

### Entity Relationship Diagram

```
+---------------+       +------------------+       +---------------+
|    users      |       |   collections    |       |    cards      |
+---------------+       +------------------+       +---------------+
| id (PK)       |<──────| user_id (FK)     |       | id (PK)       |
| username      |       | card_id (FK)     |───────| name          |
| email         |       | quantity         |       | set_id        |
| password_hash |       +------------------+       | hp            |
| currency      |                                  | type          |
| created_at    |                                  | attacks (JSON)|
+---------------+                                  | abilities     |
       │                                           | rarity        |
       │                                           +---------------+
       │
       │         +------------------+
       │         |      decks       |
       │         +------------------+
       └────────>| id (PK)          |
                 | user_id (FK)     |
                 | name             |
                 | cards (JSON)     |
                 | is_valid         |
                 | created_at       |
                 +------------------+

+------------------+       +------------------+
|     matches      |       |  match_players   |
+------------------+       +------------------+
| id (PK)          |<──────| match_id (FK)    |
| status           |       | user_id (FK)     |
| winner_id (FK)   |       | deck_id (FK)     |
| started_at       |       | result           |
| ended_at         |       +------------------+
| game_log (JSON)  |
+------------------+

+------------------+       +------------------+
|      packs       |       |   pack_contents  |
+------------------+       +------------------+
| id (PK)          |<──────| pack_id (FK)     |
| name             |       | card_id (FK)     |
| cost             |       | rarity           |
| set_id           |       | weight           |
+------------------+       +------------------+

+------------------+
|     puzzles      |
+------------------+
| id (PK)          |
| name             |
| description      |
| difficulty       |
| initial_state    |
| solution         |
| hints (JSON)     |
+------------------+
```

### Key Models

```python
# models/database.py
from sqlalchemy import Column, Integer, String, JSON, ForeignKey, DateTime
from sqlalchemy.orm import relationship
from app.database.session import Base

class User(Base):
    __tablename__ = "users"
    
    id = Column(Integer, primary_key=True)
    username = Column(String(50), unique=True, nullable=False)
    email = Column(String(255), unique=True, nullable=False)
    password_hash = Column(String(255), nullable=False)
    currency = Column(Integer, default=0)
    created_at = Column(DateTime, server_default=func.now())
    
    collections = relationship("Collection", back_populates="user")
    decks = relationship("Deck", back_populates="user")


class Card(Base):
    __tablename__ = "cards"
    
    id = Column(String(50), primary_key=True)  # e.g., "pikachu_base_001"
    name = Column(String(100), nullable=False)
    set_id = Column(String(50), nullable=False)
    card_type = Column(String(20), nullable=False)  # pokemon, trainer, energy
    hp = Column(Integer, nullable=True)  # Only for Pokemon
    pokemon_type = Column(String(20), nullable=True)  # fire, water, etc.
    attacks = Column(JSON, default=[])
    abilities = Column(JSON, default=[])
    weakness = Column(JSON, nullable=True)
    resistance = Column(JSON, nullable=True)
    retreat_cost = Column(Integer, default=0)
    rarity = Column(String(20), nullable=False)
    image_url = Column(String(500), nullable=True)


class Deck(Base):
    __tablename__ = "decks"
    
    id = Column(Integer, primary_key=True)
    user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
    name = Column(String(100), nullable=False)
    cards = Column(JSON, nullable=False)  # [{"card_id": "...", "quantity": 2}, ...]
    is_valid = Column(Boolean, default=False)
    created_at = Column(DateTime, server_default=func.now())
    updated_at = Column(DateTime, onupdate=func.now())
    
    user = relationship("User", back_populates="decks")
```

---

## Real-Time Communication

### Socket.io Events

#### Client -> Server

| Event | Payload | Description |
|-------|---------|-------------|
| `game:action` | `{ type, ...params }` | Player performs action |
| `game:resign` | `{}` | Player resigns match |
| `matchmaking:join` | `{ deckId }` | Join matchmaking queue |
| `matchmaking:leave` | `{}` | Leave queue |

#### Server -> Client

| Event | Payload | Description |
|-------|---------|-------------|
| `game:state` | `VisibleGameState` | Full state update |
| `game:action_result` | `{ success, error? }` | Action confirmation |
| `game:ended` | `{ winner, reason }` | Match concluded |
| `matchmaking:found` | `{ gameId, opponent }` | Match found |

### Action Types

```python
class ActionType(str, Enum):
    PLAY_POKEMON = "play_pokemon"      # Play basic Pokemon to bench
    EVOLVE = "evolve"                   # Evolve a Pokemon
    ATTACH_ENERGY = "attach_energy"     # Attach energy card
    PLAY_TRAINER = "play_trainer"       # Play trainer card
    USE_ABILITY = "use_ability"         # Activate Pokemon ability
    ATTACK = "attack"                   # Declare attack
    RETREAT = "retreat"                 # Retreat active Pokemon
    PASS = "pass"                       # End turn / pass priority
```

### State Synchronization

```python
# websocket/handlers.py
@sio.on('game:action')
async def handle_action(sid, data: dict):
    session = await get_session(sid)
    game_id = session['game_id']
    player_id = session['player_id']
    
    game = await state_manager.get_game(game_id)
    
    # Validate
    result = await game_engine.validate_action(game, player_id, data)
    if not result.valid:
        await sio.emit('game:action_result', {
            'success': False,
            'error': result.reason
        }, to=sid)
        return
    
    # Execute
    new_state = await game_engine.execute_action(game, player_id, data)
    await state_manager.save_game(game_id, new_state)
    
    # Broadcast filtered state to each player
    for pid, psid in game.player_sockets.items():
        visible_state = game_engine.get_visible_state(new_state, pid)
        await sio.emit('game:state', visible_state, to=psid)
    
    # Check win conditions
    winner = game_engine.check_win_condition(new_state)
    if winner:
        await handle_game_end(game_id, winner)
```

---

## Game Engine

### Turn Structure

```
GAME START
    │
    v
┌─────────────────────────────────┐
│  SETUP PHASE                    │
│  - Draw 7 cards each            │
│  - Place active + bench         │
│  - Set aside 6 prize cards      │
│  - Flip coin for first turn     │
└─────────────────────────────────┘
    │
    v
┌─────────────────────────────────┐
│  TURN START                     │◄──────────────────┐
│  - Draw 1 card                  │                   │
└─────────────────────────────────┘                   │
    │                                                 │
    v                                                 │
┌─────────────────────────────────┐                   │
│  MAIN PHASE (any order)         │                   │
│  - Attach 1 energy (once)       │                   │
│  - Play basic Pokemon to bench  │                   │
│  - Evolve Pokemon               │                   │
│  - Play trainer cards           │                   │
│  - Use abilities                │                   │
│  - Retreat active (once)        │                   │
└─────────────────────────────────┘                   │
    │                                                 │
    v                                                 │
┌─────────────────────────────────┐                   │
│  ATTACK PHASE (optional)        │                   │
│  - Declare attack               │                   │
│  - Resolve damage + effects     │                   │
│  - Check knockouts              │                   │
│  - Take prize cards             │                   │
└─────────────────────────────────┘                   │
    │                                                 │
    v                                                 │
┌─────────────────────────────────┐                   │
│  END PHASE                      │                   │
│  - Apply end-of-turn effects    │                   │
│  - Switch to opponent           │───────────────────┘
└─────────────────────────────────┘
    │
    v
┌─────────────────────────────────┐
│  WIN CONDITION CHECK            │
│  - All prizes taken?            │
│  - Opponent has no Pokemon?     │
│  - Opponent can't draw?         │
│  - Custom home rules?           │
└─────────────────────────────────┘
```

### Card Effect System

Effects are data-driven with pluggable handlers:

```python
# core/card_effects.py
from typing import Protocol, Callable, Dict, Any

class EffectContext:
    game: GameState
    source_card: Card
    source_player: str
    target_card: Optional[Card]
    target_player: Optional[str]
    params: Dict[str, Any]

EffectHandler = Callable[[EffectContext], Awaitable[None]]

EFFECT_REGISTRY: Dict[str, EffectHandler] = {}

def effect_handler(name: str):
    """Decorator to register effect handlers."""
    def decorator(func: EffectHandler):
        EFFECT_REGISTRY[name] = func
        return func
    return decorator

# Example effects
@effect_handler("deal_damage")
async def deal_damage(ctx: EffectContext):
    amount = ctx.params["amount"]
    ctx.target_card.damage += amount

@effect_handler("heal")
async def heal(ctx: EffectContext):
    amount = ctx.params["amount"]
    ctx.target_card.damage = max(0, ctx.target_card.damage - amount)

@effect_handler("apply_status")
async def apply_status(ctx: EffectContext):
    status = ctx.params["status"]  # paralyzed, poisoned, asleep, etc.
    ctx.target_card.status_conditions.append(status)

@effect_handler("draw_cards")
async def draw_cards(ctx: EffectContext):
    count = ctx.params["count"]
    player = ctx.game.players[ctx.source_player]
    for _ in range(count):
        if player.deck:
            player.hand.append(player.deck.pop())

@effect_handler("coin_flip")
async def coin_flip(ctx: EffectContext):
    """Flip coin, execute sub-effect on heads."""
    result = random.choice(["heads", "tails"])
    if result == "heads" and "on_heads" in ctx.params:
        sub_effect = ctx.params["on_heads"]
        await resolve_effect(ctx.game, sub_effect, ctx)
```

---

## AI System

### Architecture

```python
# core/ai/base.py
from abc import ABC, abstractmethod

class AIPlayer(ABC):
    """Base class for AI opponents."""
    
    @abstractmethod
    async def choose_action(self, game_state: GameState) -> Action:
        """Given the current game state, return the best action."""
        pass
    
    def get_legal_actions(self, game_state: GameState) -> List[Action]:
        """Returns all legal actions for the AI player."""
        actions = []
        player = game_state.players[self.player_id]
        
        # Can attach energy?
        if not game_state.energy_attached_this_turn:
            for card in player.hand:
                if card.card_type == "energy":
                    for target in self.get_energy_targets(game_state):
                        actions.append(AttachEnergyAction(card.id, target.id))
        
        # Can play Pokemon?
        for card in player.hand:
            if card.card_type == "pokemon" and card.stage == "basic":
                if len(player.bench) < 5:
                    actions.append(PlayPokemonAction(card.id))
        
        # ... more action types
        
        actions.append(PassAction())  # Can always pass
        return actions
```

### Difficulty Implementations

```python
# core/ai/easy.py
class EasyAI(AIPlayer):
    """Random legal moves with basic priorities."""
    
    async def choose_action(self, game_state: GameState) -> Action:
        actions = self.get_legal_actions(game_state)
        
        # Slight preference: attack if possible
        attacks = [a for a in actions if isinstance(a, AttackAction)]
        if attacks and random.random() > 0.3:
            return random.choice(attacks)
        
        return random.choice(actions)


# core/ai/medium.py
class MediumAI(AIPlayer):
    """Heuristic-based decision making."""
    
    async def choose_action(self, game_state: GameState) -> Action:
        actions = self.get_legal_actions(game_state)
        
        # Score each action
        scored = [(self.score_action(game_state, a), a) for a in actions]
        scored.sort(reverse=True, key=lambda x: x[0])
        
        # Pick top action with some randomness
        top_actions = [a for s, a in scored[:3]]
        return random.choice(top_actions)
    
    def score_action(self, state: GameState, action: Action) -> float:
        score = 0.0
        
        if isinstance(action, AttackAction):
            # Prefer knockouts
            damage = self.calculate_damage(state, action)
            target = state.get_defending_pokemon()
            if damage >= target.hp - target.damage:
                score += 100  # Knockout!
            else:
                score += damage * 0.5
        
        elif isinstance(action, PlayPokemonAction):
            # Value bench presence
            score += 10
        
        # ... more heuristics
        
        return score


# core/ai/hard.py
class HardAI(AIPlayer):
    """Monte Carlo Tree Search for strong play."""
    
    def __init__(self, player_id: str, simulations: int = 1000):
        super().__init__(player_id)
        self.simulations = simulations
    
    async def choose_action(self, game_state: GameState) -> Action:
        root = MCTSNode(game_state, None, self.player_id)
        
        for _ in range(self.simulations):
            node = self.select(root)
            node = self.expand(node)
            result = self.simulate(node)
            self.backpropagate(node, result)
        
        # Return most visited child's action
        best_child = max(root.children, key=lambda c: c.visits)
        return best_child.action
```

---

## Security Considerations

### Hidden Information

**Never expose to clients:**
- Deck order (either player)
- Opponent's hand contents
- Unrevealed prize cards
- RNG seeds or future random results

```python
def get_visible_state(game: GameState, player_id: str) -> VisibleGameState:
    """Filter game state to only what player can see."""
    opponent_id = get_opponent(game, player_id)
    
    return VisibleGameState(
        # My full information
        my_hand=[card.to_dict() for card in game.players[player_id].hand],
        my_deck_count=len(game.players[player_id].deck),
        my_prizes=[
            card.to_dict() if card.revealed else {"hidden": True}
            for card in game.players[player_id].prizes
        ],
        
        # Opponent's hidden information
        opponent_hand_count=len(game.players[opponent_id].hand),
        opponent_deck_count=len(game.players[opponent_id].deck),
        opponent_prizes_remaining=len([
            p for p in game.players[opponent_id].prizes if not p.taken
        ]),
        
        # Public information
        battlefield=game.battlefield.to_dict(),
        discard_piles={
            pid: [card.to_dict() for card in player.discard]
            for pid, player in game.players.items()
        },
        current_turn=game.current_turn,
        phase=game.phase,
    )
```

### Server Authority

- All game logic runs server-side
- Client sends intentions, server validates and executes
- Never trust client-provided game state
- RNG uses `secrets` module for unpredictability

### Input Validation

```python
async def validate_action(game: GameState, player_id: str, action: dict) -> ValidationResult:
    # Is it this player's turn?
    if game.current_turn != player_id:
        return ValidationResult(False, "Not your turn")
    
    # Is this action type valid for current phase?
    action_type = action.get("type")
    if action_type not in VALID_ACTIONS_BY_PHASE[game.phase]:
        return ValidationResult(False, f"Cannot {action_type} during {game.phase}")
    
    # Validate specific action requirements
    validator = ACTION_VALIDATORS.get(action_type)
    if validator:
        return await validator(game, player_id, action)
    
    return ValidationResult(False, "Unknown action type")
```

---

## Offline Standalone Fork

> **Design Consideration**: The architecture intentionally supports forking as a completely offline, standalone experience for the RPG/campaign mode.

### Why This Matters

The primary gameplay experience is a single-player RPG campaign. While the live service provides multiplayer and cloud saves, many players may prefer:
- Offline play without internet dependency
- Local save files they fully control
- A self-contained executable with no account requirements
- Modding and customization freedom

### Architecture Principles for Fork Compatibility

The `backend/app/core/` game engine is designed to be **completely decoupled** from network and database concerns:

| Component | Live Service | Offline Fork |
|-----------|--------------|--------------|
| Game Engine (`core/`) | Same | Same (copy directly) |
| Card Definitions | PostgreSQL | Embedded JSON files |
| Save Data | PostgreSQL + Redis | Local JSON/SQLite files |
| Authentication | OAuth/JWT | None needed |
| Multiplayer | WebSocket/Socket.io | Removed entirely |
| RNG | SecureRandom | SeededRandom (for replays) |

### Core Engine Independence

The game engine has **zero dependencies** on:
- Database connections
- Network I/O
- Authentication/sessions
- Redis caching
- WebSocket communication

All it needs:
- `RulesConfig` (can be hardcoded or loaded from JSON)
- `CardDefinition` objects (can be embedded or loaded from files)
- `RandomProvider` (SeededRandom works offline)

### Potential Offline Package Structure

```
mantimon-offline/
├── game/
│   ├── core/              # Direct copy of backend/app/core/
│   ├── campaign/          # NPC definitions, dialog, progression
│   │   ├── clubs/         # Club data (NPCs, leaders, rewards)
│   │   ├── dialog/        # NPC dialog trees
│   │   └── progression.py # Campaign state machine
│   ├── cards/             # Card definitions as JSON
│   │   ├── base_set.json
│   │   └── expansion_1.json
│   └── saves/             # Local save directory
│       └── slot_1.json
├── ui/                    # Could reuse Phaser or use PyGame/Godot
├── assets/                # Card images, sounds
└── main.py                # Entry point
```

### Packaging Options for Distribution

| Approach | Bundle Size | Complexity | UI Reuse |
|----------|-------------|------------|----------|
| **Electron + Python** | ~150MB | Medium | Full web UI |
| **Tauri + Python** | ~30MB | High | Full web UI |
| **PyInstaller + PyGame** | ~50MB | Low | New UI needed |
| **Godot + GDScript port** | ~40MB | Medium | New UI needed |
| **Nuitka + embedded browser** | ~80MB | Medium | Phaser reuse |

### What Stays, What Goes

**Keep (copy to fork):**
- `backend/app/core/` - Entire game engine
- `frontend/src/game/` - Phaser scenes (if using web UI)
- Card definitions (export from DB to JSON)
- Campaign/NPC data

**Remove (not needed offline):**
- `backend/app/api/` - REST endpoints
- `backend/app/websocket/` - Multiplayer sync
- `backend/app/services/` - DB-backed services
- Authentication system
- Matchmaking/lobby

### Development Guidelines

To maintain fork compatibility, follow these rules when developing the core engine:

1. **No imports from `app.api`, `app.websocket`, or `app.services`** in `app.core`
2. **No database session dependencies** in core engine functions
3. **Card definitions passed in**, not fetched - `GameEngine.create_game()` receives a card registry
4. **RNG injected via protocol** - `RandomProvider` allows swapping implementations
5. **State is self-contained** - `GameState` includes everything needed to resume a game
6. **No async required** - Core logic is sync; async wrappers added at service layer

### Testing Offline Viability

The existing test suite validates offline compatibility:
- All `tests/core/` tests run without database
- `SeededRandom` enables deterministic testing
- Card fixtures are created in-memory, not loaded from DB

### Future: Official Offline Release

If demand exists, an official offline version could be released:
1. Export campaign data and card definitions to JSON
2. Bundle core engine with a lightweight UI wrapper
3. Distribute as standalone executable (itch.io, Steam, etc.)
4. Optional: Steam Workshop for community card sets
