# Ajouter un nouveau TCG

# Ajouter un nouveau TCG

> _Dernière mise à jour : 2026-05-10_

⭐ **Page critique** — procédure complète pour intégrer un TCG inédit (ex. Pokemon, Yu-Gi-Oh!, Star Wars Unlimited, Sorcery Contested Realm).

Référence : skill global `add-tcg` listé dans `~/.claude/skills/add-tcg/`. Ce skill fournit une checklist 6-axes ; cette page détaille les fichiers + snippets à toucher.

## Vue d'ensemble : 6 axes

| Axe | But | Fichiers principaux |
|---|---|---|
| 1 | Cartes (data + ingestion) | `services/cards/sources/<tcg>.ts`, `scripts/<tcg>-cards/` |
| 2 | Symboles UI inline | `frontend/src/lib/<tcg>-symbols.ts`, `frontend/public/<tcg>-icons/` |
| 3 | Decompose-query (synergy/deckbuilding) | `services/rag/retrieve/multi-query-<tcg>.ts` |
| 4 | Deckbuilding (pool + spec) | `services/rag/deckbuilding/{spec,pool,validate}.ts` |
| 5 | Set matching | `services/cards/set-aliases.ts` |
| 6 | Méta (tier list + tournois) | `services/meta/<tcg>.ts`, `cron/meta-sync.ts` |

Tous les axes ne sont pas obligatoires. Pour un TCG sans format constructed (ex. jeu de société), les axes 4 et 6 ne s'appliquent pas.

---

## Axe 1 — Cartes (data + ingestion)

### 1.1 Choisir la source

- **API CDN éditeur officiel** quand possible (FAB, MTG, Riftbound : tous trouvés sur le CDN officiel — vérifier `rules.<tcg>.com` ou équivalent)
- **JSON tiers MIT** sinon (Lorcana via LorcanaJSON, etc.)
- **Bulk download** (Scryfall MTG) ou **API live** (Riot Riftbound)
- **Package npm bundlé** (FAB) si la source est très stable et l'éditeur publie un package

### 1.2 Créer la source

`src/services/cards/sources/<tcg>.ts` doit implémenter l'interface `CardSource` :

```typescript
import { CardSource } from './types.js';

export const myTcgSource: CardSource = {
  collection: 'mytcg-cards',  // doit matcher le `hasCardDatabase` qu'on assignera aux jeux
  
  async load() {
    // Lit la source (file, API, package) → renvoie { cards: NormalizedCard[], hash: string }
    // Le hash sert au cards-sync pour détecter les changements
  },
  
  normalizeCard(rawCard) {
    // Transforme le format source → schema interne (Qdrant payload)
    return {
      id: ...,
      name: ...,
      // … champs communs + spécifiques au TCG
    };
  },
  
  getImageUrl(card) {
    // CDN éditeur si dispo, sinon chemin local
    return card.image_url ?? `/cards/<tcg>/${card.id}.png`;
  },
};
```

### 1.3 Enregistrer dans le registry

`src/services/cards/sources/registry.ts` :

```typescript
import { myTcgSource } from './<tcg>.js';

export const cardSources = new Map([
  ['magic-cards', magicSource],
  ['lorcana-cards', lorcanaSource],
  // ...
  ['mytcg-cards', myTcgSource],   // ← ajouter ici
]);
```

⚠️ Sans cet enregistrement, `cards-cache.ts` ne load pas la collection.

### 1.4 Scripts d'ingestion

Modèle à dupliquer : `scripts/riftbound-cards/`. Crée :

```
scripts/<tcg>-cards/
├── fetch-cards.ts       # Télécharge / API call → cache JSON
├── ingest.ts            # Push Qdrant via cards-sync
├── link-game.ts         # CLI pour lier un jeu à hasCardDatabase = '<tcg>-cards'
└── (test-retrieve.ts)   # Optionnel : debug retrieval
```

### 1.5 Commandes npm

`package.json` :

```json
{
  "scripts": {
    "cards:<tcg>:fetch": "tsx --env-file=.env scripts/<tcg>-cards/fetch-cards.ts",
    "cards:<tcg>:ingest": "tsx --env-file=.env scripts/<tcg>-cards/ingest.ts",
    "cards:<tcg>:link-game": "tsx --env-file=.env scripts/<tcg>-cards/link-game.ts"
  }
}
```

### 1.6 Variables d'env

⚠️ **3 fichiers en parallèle** :

1. `src/config.ts` :
   ```typescript
   MY_TCG_CARDS_DATA_DIR: z.string().default('/app/data/<tcg>-cards'),
   ```
2. `.env.example` : `MY_TCG_CARDS_DATA_DIR=/app/data/<tcg>-cards`
3. `unraid/boardgame-referee.xml` : ajouter le `<Config Type="Variable">...` correspondant

Sinon l'admin Unraid ne pourra pas la setter via l'UI.

### 1.7 Vérifier

```bash
docker exec boardgame-referee npm run cards:<tcg>:fetch
docker exec boardgame-referee npm run cards:<tcg>:ingest
# /admin/services doit montrer la collection avec count > 0
```

---

## Axe 2 — Symboles UI inline

### 2.1 Récupérer les PNG officiels

**Toujours vérifier le CDN officiel d'abord** — FAB/MTG/Riftbound ont tous les PNG officiels sur le site de règles ou press-kit (cf. mémoire `feedback_tcg_official_icons_cdn.md`).

Stocker dans `frontend/public/<tcg>-icons/`.

### 2.2 Helper de remplacement

Modèle : `frontend/src/lib/riftbound-symbols.ts`. Crée `<tcg>-symbols.ts` :

```typescript
const SYMBOL_RE = /\{([rpdhicut])\}/g;  // whitelist STRICTE — sinon collision avec d'autres TCG

export function replaceMyTcgTokens(html: string): string {
  return html.replace(SYMBOL_RE, (_, key) => {
    return `<img src="/<tcg>-icons/${key}.png" class="card-symbol" alt="${key}">`;
  });
}

export const myTcgEnabled = true;
```

⚠️ **Regex whitelist obligatoire** : `\{R\}` MTG (majuscule) collisione avec `\{r\}` FAB (minuscule). La regex doit cibler **exactement** les tokens du TCG en cours, jamais plus large.

### 2.3 Hook dans le markdown

`frontend/src/composables/useArbiterMarkdown.ts` :

```typescript
import { replaceMyTcgTokens, myTcgEnabled } from '../lib/<tcg>-symbols.js';

// Dans la fonction principale, AVANT `renderHtml` :
if (game.value?.hasCardDatabase === '<tcg>-cards' && myTcgEnabled) {
  html = replaceMyTcgTokens(html);
}
```

### 2.4 Vérifier

Poser une question dont la réponse contient un symbole. Vérifier le rendu image dans la bulle Oracle (DevTools → inspecter le DOM, vérifier que `<img>` apparaît).

---

## Axe 3 — Decompose-query (intent synergy / deckbuilding)

### 3.1 Multi-query TCG-specific

Modèle : `src/services/rag/retrieve/multi-query-mtg.ts`. Crée `multi-query-<tcg>.ts` :

```typescript
export async function decomposeMyTcgQuery(question: string): Promise<MyTcgSpec | null> {
  const prompt = `... (prompt Haiku qui sort un JSON structuré pour ce TCG)`;
  // Appel Haiku via promptStream avec timeout DECOMPOSE_TIMEOUT_MS
  // Parse JSON, valide via Zod
  // Retourne null si parse fail (le retrieval continuera sans filters spécifiques)
}
```

Le `MyTcgSpec` est un type Zod avec les champs structuraux du TCG (couleurs, format, hero, types, etc.).

### 3.2 Dispatch dans `synergy-expansion.ts`

```typescript
switch (game.hasCardDatabase) {
  case 'magic-cards':
    spec = await decomposeMtgQuery(question);
    break;
  case 'mytcg-cards':
    spec = await decomposeMyTcgQuery(question);  // ← ajouter
    break;
  // …
}
```

### 3.3 Vérifier

Poser une question synergy spécifique au TCG ("liste-moi les <type> de couleur <X>"). Vérifier dans `/admin/feedback/<id>` que les filters Qdrant matchent la décomposition.

---

## Axe 4 — Deckbuilding (pool + spec)

### 4.1 Spec defaults

`src/services/rag/deckbuilding/spec.ts` : ajouter une entrée dans le `DEFAULT_SPECS_BY_TCG` :

```typescript
'mytcg-cards': {
  format: 'standard',
  mainboard: 60,
  sideboard: 15,
  maxCopies: 3,
  // selon le TCG : runes, battlefields, equipment, etc.
}
```

### 4.2 Pool éligible

`src/services/rag/deckbuilding/pool.ts` : étendre `fetchEligibleCards` avec le filtre Qdrant approprié :

```typescript
case 'mytcg-cards':
  filter = {
    must: [{ key: 'card_<tcg>_legal_formats', match: { value: spec.format } }],
  };
  break;
```

Si le TCG n'a pas de `legal_formats` (collection mono-format), pas de filter Qdrant — fais un post-filtrage TS sur les champs structurels (couleurs, héro, etc.).

### 4.3 Validation

`src/services/rag/deckbuilding/validate.ts` : ajouter les contraintes spécifiques :

- Total exact par section
- Max copies (avec whitelist basic lands équivalente si applicable)
- Anchor cards présentes
- Champ singletons obligatoires (un héro FAB, un commandant Commander, etc.)

### 4.4 Vérifier

Poser "fais-moi une decklist <format> autour de <archétype>". Vérifier que le total + max copies sont respectés et que les anchor cards sont incluses.

---

## Axe 5 — Set matching

`src/services/cards/set-aliases.ts` : ajouter les alias FR/EN du nouveau TCG :

```typescript
export const SET_ALIASES = new Map([
  // MTG
  ['Strixhaven', 'STX'],
  // … existants
  
  // <tcg>
  ['Surge', 'SUR'],
  ['Nouveau set FR', 'NSF'],
]);
```

Permet à l'utilisateur de citer un set par son nom complet, le filtre Qdrant s'applique sur le code 3 lettres.

### Vérifier

Poser une question qui mentionne un set par son nom complet. Vérifier le filter dans `/admin/feedback/<id>`.

---

## Axe 6 — Méta (tier list + tournois)

### 6.1 Source méta stable

Identifier une source qui :
- Survit dans le temps (pas figée comme DotGG Lorcana)
- Expose une API ou un HTML scrapable de manière fiable
- Couvre un format pertinent

### 6.2 Service méta

Modèle : `services/meta/mobalytics-riftbound.ts` (scrape) ou `services/meta/mtgtop8.ts` (scrape avec rate-limit). Crée `services/meta/<tcg>.ts` :

```typescript
export async function syncMyTcgMeta(): Promise<MetaSnapshot> {
  // 1. Fetch / scrape la source
  // 2. Transformer en MetaSnapshot { tier_list, tournament_decks }
  // 3. Retourner pour ingest via services/meta/ingest.ts
}
```

### 6.3 Cron meta-sync

`src/cron/meta-sync.ts` : ajouter l'appel au sync à la fréquence configurée :

```typescript
if (config.META_MY_TCG_ENABLED) {
  await ingestSnapshot(await syncMyTcgMeta());
}
```

### 6.4 Variables d'env méta

Encore une fois, **3 fichiers en parallèle** :

```env
META_MY_TCG_ENABLED=false
META_MY_TCG_RATE_LIMIT_MS=1500
META_MY_TCG_USER_AGENT=Mozilla/5.0 (compatible; ...)
# … selon le TCG
```

### 6.5 Vérifier

```bash
docker exec boardgame-referee npm run meta:<tcg>:sync
```

Vérifier que des chunks `[META]` apparaissent dans Qdrant et sortent dans une question méta.

---

## Verification end-to-end

1. **Créer un jeu** via `/add-game` (ou `npm run cards:<tcg>:link-game`) avec `has_card_database = '<tcg>-cards'`
2. **Tester autocomplete** `@<carte>` → cartes ressortent
3. **Tester citation** `[[card:<Nom>]]` dans une réponse Oracle → bouton zoom OK
4. **Tester intent synergy** → diagnostics montrent les filters TCG
5. **Tester intent deckbuilding** → decklist respecte spec (total + max copies + anchors)
6. **Tester import deck** (si applicable) → mapping deck → stickyCardMentions

---

## Pièges classiques

| Piège | Conséquence | Fix |
|---|---|---|
| Oublier d'enregistrer dans `registry.ts` | Cards-cache ne load pas la collection | Ajouter dans `cardSources` |
| Oublier `additionalDirectories` dans le settings.json oracle | Vision Read échoue | Ajouter dans le settings.json + `permissions.allow` |
| `process.env.X` dans une route au lieu de `config.X` | Silencieusement vide en prod | Centraliser dans `src/config.ts` |
| Regex symboles trop large | Collision avec un autre TCG | Whitelist stricte caractère par caractère |
| Oublier `unraid/boardgame-referee.xml` | Admin Unraid ne peut pas setter la var | Mettre à jour les 3 fichiers (config.ts + .env.example + xml) |
| Forget `link-game` après ingest | Le jeu n'a pas `hasCardDatabase` | Lancer `npm run cards:<tcg>:link-game <gameId>` |
| Bundlée image (FAB-style) sans rebuild | Resync ne voit pas la nouvelle data | Build + redeploy AVANT resync |