> ## Documentation Index
> Fetch the complete documentation index at: https://docs.pcm.game/llms.txt
> Use this file to discover all available pages before exploring further.

# Sprint 15

# Implementation Plan - Sprint 15: The Global Market & Gacha System (REVISED)

This document outlines the detailed, file-by-file architectural implementation plan for **Sprint 15 (The Global Market & Gacha System)** in the `pcmtg-core` monorepo. It establishes a secure, zero-trust, serverless-safe backend transaction ledger utilizing Google Cloud Memorystore (Redis) rate-limiting, coupled with a declared ideological faction system and a premium glassmorphic frontend interface.

***

## 🛠 Architectural Constraints & Rules

### 1. Atomic Economy (Zero-Trust)

To prevent double-spending and race conditions, all market purchases (items, policies, and card packs) must execute within a `prisma.$transaction` block.

* A pessimistic row lock is obtained on the player's wallet using raw SQL `FOR UPDATE` before any validation or balance checks.
* User balances must be validated to remain `>= 0` after deducting costs.
* The deduction of the cost, creation of the `OwnedCard` relation, and insertion of the `TransactionLog` receipt must occur atomically.

### 2. Cloud Run Horizontal Scaling (Redis Rate Limiter)

* **Problem:** Stateless Cloud Run containers scale horizontally; local in-memory Maps are useless across multiple container instances and allow Sybil attacks.
* **Solution:** Utilize the Google Cloud Memorystore (Redis) instance provisioned in Sprint 2. Implement a Redis sliding window rate-limiter keyed on `req.playerId` to enforce a strict limit of **5 requests per 10 seconds** across all `/market/*` endpoints.

### 3. Identity & The "Based Pill" Defection Mechanic

* **Problem:** Dynamically deriving a player's quadrant from ELO coordinates breaks the "Based Pill" ideological defection mechanic (Project Bible 3.2.5), which allows players to swap quadrants without resetting their coordinate footprint.
* **Solution:** Query the player's explicitly declared `quadrant` string field directly from the `PlayerAccount` table. Map native currency strictly based on this string:
  * `AUTH_RIGHT` $\rightarrow$ `dinars`
  * `AUTH_LEFT` $\rightarrow$ `labor`
  * `LIB_LEFT` $\rightarrow$ `pronouns`
  * `LIB_RIGHT` $\rightarrow$ `monke`

### 4. Asymmetrical Cross-Quadrant Gacha (The Meme Bazaar Economy)

* **Problem:** Generic 'STANDARD' and 'PREMIUM' packs violate the asymmetrical cross-quadrant economy (Project Bible 3.3.3).
* **Solution:** Refactor `POST /market/buy-pack` to accept `{ targetQuadrant }` in the payload:
  * **Native Synthesis Pack:** Cost is **20 units** of Native Currency if `targetQuadrant` matches the player's declared native `quadrant`.
  * **Defector Synthesis Pack:** Cost is **50 units** of Native Currency if `targetQuadrant` is different (representing buying into an opposing ideology).
  * **Query Filter:** The Gacha candidates query must strictly filter by `affinity = targetQuadrant` and `cardType = 'MEME'`.
  * **Cryptographic Gacha Math:** Drop rates are calculated server-side using the Node.js native `crypto` module (`crypto.randomInt`) to prevent client-side seed prediction.

***

## 🗺 Proposed Changes

### 1. Database & Schema Layer

#### \[MODIFY] [schema.prisma](file:///home/andrew/pcmtg-core/backend/prisma/schema.prisma)

Add the explicitly declared `quadrant` field to the `PlayerAccount` model so players can defect/swap quadrants without losing their coordinate footprints.

```prisma theme={null}
model PlayerAccount {
  id           String    @id @default(dbgenerated("uuid_generate_v4()")) @map("player_id") @db.Uuid
  username     String    @unique @db.VarChar(50)
  email        String    @unique @db.VarChar(255)
  elo          Int       @default(1000) @map("elo")
  elo_x_axis   Decimal   @default(0.0000) @db.Decimal(8, 4)
  elo_y_axis   Decimal   @default(0.0000) @db.Decimal(8, 4)
  elo_z_axis   Decimal   @default(0.0000) @db.Decimal(8, 4)
  
  // Declared ideological quadrant (Direct field for defection mechanics)
  quadrant     String    @default("AUTH_RIGHT") @map("quadrant")
  
  created_at   DateTime? @default(now()) @db.Timestamptz
  last_login   DateTime? @db.Timestamptz
  
  playerWallet PlayerWallet?
  transactions TransactionLog[] 
  ownedCards   OwnedCard[] 

  @@map("player_accounts")
}
```

> \[!NOTE]
> A corresponding migration (or raw SQL run during setup) will add this column:
> `ALTER TABLE player_accounts ADD COLUMN IF NOT EXISTS quadrant VARCHAR(20) DEFAULT 'AUTH_RIGHT';`

***

### 2. Backend Layer (The Transaction & Security Engine)

#### \[MODIFY] [package.json](file:///home/andrew/pcmtg-core/backend/package.json)

Install `ioredis` to manage high-throughput sliding-window rate limiting on Memorystore.

```json theme={null}
"dependencies": {
  ...
  "ioredis": "^5.4.1"
}
```

#### \[NEW] [redis.js](file:///home/andrew/pcmtg-core/backend/src/utils/redis.js)

Export a centralized Redis connection pool for connection reuse across serverless requests.

```javascript theme={null}
const Redis = require('ioredis');

const redis = new Redis({
  host: process.env.REDIS_HOST || 'localhost',
  port: parseInt(process.env.REDIS_PORT || '6379', 10),
  maxRetriesPerRequest: 3,
  retryStrategy(times) {
    const delay = Math.min(times * 100, 3000);
    return delay;
  }
});

redis.on('error', (err) => {
  console.error('[REDIS CLIENT ERROR]:', err.message);
});

module.exports = redis;
```

#### \[NEW] [rateLimiter.js](file:///home/andrew/pcmtg-core/backend/src/middleware/rateLimiter.js)

Atomic sliding window rate limiter middleware utilizing Redis multi-exec pipelines.

```javascript theme={null}
const redis = require('../utils/redis');

const marketRateLimiter = async (req, res, next) => {
  const playerId = req.playerId;
  if (!playerId) {
    return res.status(401).json({ error: 'Unauthorized: Missing player identity' });
  }

  const key = `rate_limit:market:${playerId}`;
  const now = Date.now();
  const windowMs = 10000; // 10 seconds
  const clearBefore = now - windowMs;

  try {
    const multi = redis.multi();
    multi.zremrangebyscore(key, 0, clearBefore);
    multi.zadd(key, now, now);
    multi.zcard(key);
    multi.expire(key, 10); // TTL cleanup

    const results = await multi.exec();
    
    // ZCARD command is at index 2 of the multi pipeline results
    const requestCount = results[2][1];

    if (requestCount > 5) {
      return res.status(429).json({
        error: 'Too Many Requests',
        message: 'Rate limit exceeded. You can only make 5 transactions/requests every 10 seconds.'
      });
    }

    next();
  } catch (error) {
    console.error('[RATE LIMITER ERROR - FAILING OPEN]:', error.message);
    // Fail-open to preserve playability if Redis encounters a temporary outage
    next();
  }
};

module.exports = marketRateLimiter;
```

#### \[NEW] [market.js](file:///home/andrew/pcmtg-core/backend/src/routes/market.js)

The core market controller router that handles purchasing single modifier items/policies and opening cross-quadrant Gacha packs.

* **Declared Quadrant helper:**
  ```javascript theme={null}
  const { QUADRANT_CURRENCY_MAP } = require('../utils/cardUtils');

  async function derivePlayerQuadrantAndCurrency(tx, playerId) {
    const player = await tx.playerAccount.findUnique({
      where: { id: playerId },
      select: { quadrant: true }
    });

    if (!player) {
      throw new Error('Player account not found');
    }

    const quadrant = player.quadrant || 'AUTH_RIGHT';
    const currency = QUADRANT_CURRENCY_MAP[quadrant];

    if (!currency) {
      throw new Error(`Invalid player quadrant alignment: ${quadrant}`);
    }

    return { quadrant, currency };
  }
  ```

* **`GET /market/spot`**
  * Fetches the active list of cards with `cardType` equal to `'ITEM'` or `'POLICY'` from the `Card` table.
  * Returns card items with their details, metadata, and costs across currencies.

* **`POST /market/buy-item`**
  * **Payload:** `{ cardId }`
  * **Prisma Interactive Transaction Flow:**
    1. Lock the player's wallet row using raw SQL `FOR UPDATE` to ensure atomic isolation:
       ```javascript theme={null}
       await tx.$executeRaw`SELECT * FROM player_wallets WHERE player_id = ${req.playerId}::uuid FOR UPDATE;`;
       ```
    2. Query player's declared quadrant and map their native wallet currency (e.g. `'AUTH_RIGHT'` $\rightarrow$ `'dinars'`).
    3. Fetch target modifier card by `cardId` and validate cost (e.g. if player currency is `dinars`, read `marketCostDinars` from the card). Cost must be $> 0$.
    4. Guard against duplicates: Check if the player already owns this specific item or policy in the `OwnedCard` table. If so, abort with `400 Bad Request` to prevent duplicate passive modifiers.
    5. Deduct the cost from the appropriate native currency column in the wallet, ensuring the final balance $\ge 0$.
    6. Mint the card to the user: Create record in `OwnedCard`.
    7. Write transaction log: Record entry in `TransactionLog` with type `'MARKET_BUY_ITEM'`.

* **`POST /market/buy-pack`**
  * **Payload:** `{ targetQuadrant }` (e.g. `'AUTH_RIGHT'`, `'AUTH_LEFT'`, `'LIB_LEFT'`, `'LIB_RIGHT'`)
  * **Prisma Interactive Transaction Flow:**
    1. Lock player's wallet row using raw SQL `FOR UPDATE`.
    2. Query player's declared native quadrant and currency mapping.
    3. Determine the Gacha pack cost based on cross-quadrant dynamics:
       * If `targetQuadrant === player.quadrant`: Cost is **20 units** (Native Synthesis Pack).
       * If `targetQuadrant !== player.quadrant`: Cost is **50 units** (Defector Synthesis Pack).
    4. Verify and deduct funds from the player's native currency field.
    5. **Cryptographic Gacha Rolling:**
       * Roll rarity for each of the 3 cards in the pack using server-side `crypto.randomInt(0, 100)`:
         * **Native/Defector Synthesis Drop Rates:**
           * `COMMON`: 60% (Roll 0-59)
           * `UNCOMMON`: 25% (Roll 60-84)
           * `RARE`: 10% (Roll 85-94)
           * `EPIC`: 4% (Roll 95-98)
           * `LEGENDARY`: 1% (Roll 99)
       * For each rolled rarity, query `Card` registry strictly filtering by:
         * `cardType = 'MEME'`
         * `affinity = targetQuadrant` (Mapped from QuadrantAffinity enum)
         * `rarity = rolledRarity`
       * Randomly select a card ID from the candidates using `crypto.randomInt(0, candidates.length)`.
       * *Graceful Fallback:* If no cards are found for the rolled rarity under the target quadrant, search for adjacent rarities of `targetQuadrant` or a default card to prevent empty roll errors.
    6. Create 3 `OwnedCard` entries in the database representing the opened cards.
    7. Log transaction in `TransactionLog` with type `'MARKET_BUY_PACK'`.
    8. Commit transaction and return full details of the 3 minted cards in the response.

#### \[MODIFY] [index.js](file:///home/andrew/pcmtg-core/backend/index.js)

* Import the new market router and mount it under `/market`.
* Import and apply the Redis `marketRateLimiter` middleware to protect `/market/*` routes.

***

### 3. Frontend Layer (Marketplace UI Shell)

#### \[NEW] [page.tsx](file:///home/andrew/pcmtg-core/frontend/app/market/page.tsx)

An extremely premium, responsive, glassmorphic marketplace interface structured with React state-driven components.

* **Key Features & Interactive State Flows:**
  * **Tabs:** Toggle between `'SPOT_MARKET'` (Modifier Items/Policies) and `'MEME_BAZAAR'` (Gacha Pack Synthesis).
  * **Active State Syncing:**
    * Displays real-time balances for the player's native currency with interactive visual shifts.
    * Synchronizes directly with the global `<WalletOverlay />` component via incrementing a `walletSyncTrigger` counter state upon a successful transaction, forcing a silent backend refresh of current funds.
  * **Spot Market Item Cards:**
    * Highlight items with glassmorphic cards.
    * If the user already owns a single-use modifier card, overlay a high-contrast `"OWNED"` banner and disable the purchase button.
  * **Meme Bazaar Pack Opening Overlay:**
    * Offers 4 synthesized pack options, representing the 4 political quadrants. Hovering dynamically highlights the pack cost (20 native currency if matching the player's declared native faction, 50 native currency if different, with a clean visual tooltip explaining the "Cross-Quadrant Ideological Premium").
    * Clicking buy triggers a cinematic opening overlay with:
      1. Shaking booster pack animation via `framer-motion`.
      2. Tearing pack wrapper clip-path animation.
      3. Revealing 3 cards positioned side-by-side, face down.
      4. Clicking each card triggers a realistic 3D Y-axis flip (`rotateY: 180`) to reveal the meme.
      5. Each card features an elegant, rarity-colored drop shadow glow:
         * `COMMON`: Subtle silver glow (`shadow-zinc-500/20`)
         * `UNCOMMON`: Emerald radiance (`shadow-emerald-500/35`)
         * `RARE`: Ocean cobalt glow (`shadow-blue-500/50`)
         * `EPIC`: Regal purple neon aura (`shadow-purple-500/70 shadow-[0_0_20px_rgba(168,85,247,0.4)]`)
         * `LEGENDARY`: Golden celestial fire (`shadow-amber-500/90 shadow-[0_0_35px_rgba(245,158,11,0.6)] animate-pulse`)

***

## 🔒 Exploitation Defense Guardrails

1. **Anti-Sybil Rate Limiting (Redis Sliding Window):**
   Stateless instances running under Cloud Run load-balancer pools read and write to Google Memorystore. Parallel "double-click" requests are instantly logged, evaluated, and throttled atomically across container instances.
2. **Double-Spend Row Locks:**
   Using `SELECT ... FOR UPDATE` row locks on the `PlayerWallet` within an interactive transaction prevents multi-threaded balance bypass hacks. No reads or state determinations are trusted until the exclusive lock is acquired.
3. **Declared Quadrant Enforcement:**
   Player quadrant values are read directly from the declared database record, safeguarding ideological faction structures and defection mechanics.
4. **Passive Duplicate Protection:**
   The backend transaction engine explicitly aborts card minting if a player already owns an item/policy modifier, sealing the game against duplicate passive buffs.

***

## 🧪 Verification & Testing Plan

### Automated Test Suite

We will construct an integration test script `backend/scripts/test-market.js` to execute and verify:

* **Rate Limiting Resilience:** Simulate a rapid-fire burst of 10 requests within 1 second. Verify that the first 5 requests return `200/201 OK`, and requests 6 through 10 are strictly blocked with `429 Too Many Requests`.
* **Race Condition Resistance:** Fire 5 concurrent item purchase requests for the same unique card item simultaneously using `Promise.all`. Verify that exactly **one** succeeds, while the other 4 return error codes indicating "Duplicate Item" or "Sufficient Row Locked" aborts.
* **Asymmetric Pricing Compliance:** Execute test mock purchases for players in different quadrants buying matching/different packs. Verify that native purchases charge 20 units and cross-quadrant purchases charge 50 units.
* **Gacha Distribution Math:** Open 100 Gacha packs in test mode and log the frequency distribution to ensure it closely maps to weighted drop rate probabilities.

### Manual Verification

* Verify high-fidelity rendering, responsive sizing, and fluid 3D card flips across desktop, tablet, and mobile breakpoints.
* Ensure visual indicators dynamically switch symbols based on user faction (e.g. đ, ⚒, ❀, 🐒).
* Confirm global `<WalletOverlay />` syncs immediately without requiring full page loads.
