# 🎰 Casino Plugin Development Guide
> Complete reference for building a new game plugin — backend (PHP/SQL) + frontend (React or HTML) + admin panel.

---

## 1. System Architecture Overview

```
casino/
├── backend/
│   ├── includes/
│   │   ├── Config.php          ← DB singleton (PDO)
│   │   ├── MerchantAPI.php     ← Balance read/write
│   │   └── PluginManager.php   ← Auto-discovers plugins
│   ├── api/
│   │   ├── game-launch.php     ← Creates session token
│   │   ├── games.php           ← Public game list
│   │   └── ...
│   └── plugins/
│       └── YOUR_PLUGIN/        ← Your plugin lives here
│           ├── plugin.json
│           ├── assets/
│           │   └── image/
│           └── backend/
│               ├── api.php
│               └── your_plugin_settings.json
└── frontend/
    └── src/
        ├── App.jsx             ← Router
        ├── play/Lobby.jsx      ← Game launcher
        └── plugins/
            └── YOUR_PLUGIN/    ← Your frontend lives here
                ├── YourPlugin.jsx      ← Main component
                ├── hooks/
                │   └── useYourPlugin.js
                ├── components/
                │   └── ReelGrid.jsx etc.
                └── admin/
                    └── AdminPanel.jsx
```

---

## 2. SQL Database Schema

```sql
-- Players table
CREATE TABLE players (
  id               INT AUTO_INCREMENT PRIMARY KEY,
  username         VARCHAR(100),
  balance          DECIMAL(15,2) DEFAULT 0,
  currency         VARCHAR(10)   DEFAULT 'INR',
  client_id        INT,
  current_turnover DECIMAL(15,2) DEFAULT 0
);

-- Games table (auto-synced from plugin.json on first launch)
CREATE TABLE games (
  id            INT AUTO_INCREMENT PRIMARY KEY,
  name          VARCHAR(100),
  slug          VARCHAR(100) UNIQUE,
  plugin_folder VARCHAR(100),
  thumbnail     VARCHAR(255),
  status        ENUM('active','inactive') DEFAULT 'active',
  hot           TINYINT(1) DEFAULT 0,
  category      VARCHAR(50)
);

-- Game sessions (token-based auth)
CREATE TABLE game_sessions (
  id         INT AUTO_INCREMENT PRIMARY KEY,
  player_id  INT,
  game_id    INT,
  token      VARCHAR(255) UNIQUE,
  currency   VARCHAR(10),
  expires_at DATETIME,
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- Bets (stores spin result + persistent game state JSON)
CREATE TABLE bets (
  id         INT AUTO_INCREMENT PRIMARY KEY,
  player_id  INT,
  game_id    INT,
  bet_amount DECIMAL(10,2),
  win_amount DECIMAL(10,2),
  status     VARCHAR(20) DEFAULT 'completed',
  state      JSON,            -- ← Plugin state persisted here
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- Transactions (financial log)
CREATE TABLE transactions (
  id          INT AUTO_INCREMENT PRIMARY KEY,
  player_id   INT,
  amount      DECIMAL(15,2),
  type        ENUM('bet','win','deposit','withdrawal'),
  status      VARCHAR(20),
  description VARCHAR(255),
  created_at  TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- System settings (key-value store)
CREATE TABLE system_settings (
  `key`   VARCHAR(100) PRIMARY KEY,
  `value` TEXT
);
```

---

## 3. Step 1 — Create `plugin.json`

**Path:** `backend/plugins/YOUR_PLUGIN/plugin.json`

```json
{
  "name": "My Slot Game",
  "slug": "my_slot",
  "version": "1.0.0",
  "description": "A 5-reel slot game with cascading wins.",
  "provider": "MyStudio",
  "type": "slot",
  "thumbnail": "/backend/plugins/my_slot/assets/image/logo.webp",
  "entry": "MySlot.jsx"
}
```

| Field | Required | Notes |
|---|---|---|
| `slug` | ✅ | Must match folder name & frontend folder name |
| `entry` | ✅ | Main React component filename |
| `thumbnail` | ✅ | Shown in lobby game list |
| `type` | ✅ | `slot`, `crash`, `table`, etc. |

> **Auto-discovery**: PluginManager scans all folders in `backend/plugins/` and reads `plugin.json`. No registration needed — just create the folder.

---

## 4. Step 2 — Backend `api.php`

**Path:** `backend/plugins/YOUR_PLUGIN/backend/api.php`

### Boilerplate

```php
<?php
header('Content-Type: application/json');
require_once __DIR__ . '/../../../includes/Config.php';
require_once __DIR__ . '/../../../includes/MerchantAPI.php';

use App\Config;
use App\MerchantAPI;

$db          = Config::getInstance()->getDB();
$merchantAPI = new MerchantAPI($db);

$data    = json_decode(file_get_contents('php://input'), true);
$token   = $data['token']   ?? $_POST['token']  ?? '';
$action  = $data['action']  ?? 'get_state';

$settingsFile = __DIR__ . '/my_slot_settings.json';
```

### Required Actions

#### `get_state` — Load initial game state
```php
if ($action === 'get_state') {
    // 1. Verify session
    $stmt = $db->prepare("
        SELECT s.player_id, s.game_id, p.balance, p.currency, p.client_id
        FROM game_sessions s JOIN players p ON s.player_id = p.id
        WHERE s.token = ?
    ");
    $stmt->execute([$token]);
    $session = $stmt->fetch(PDO::FETCH_ASSOC);
    if (!$session) { echo json_encode(['error' => 'INVALID_SESSION']); exit; }

    $playerId = (int)$session['player_id'];
    $gameId   = (int)$session['game_id'];

    // 2. Load last state from bets table
    $stmt = $db->prepare("SELECT state FROM bets WHERE player_id=? AND game_id=? ORDER BY id DESC LIMIT 1");
    $stmt->execute([$playerId, $gameId]);
    $lastState = $stmt->fetchColumn();
    $gameState = $lastState ? json_decode($lastState, true) : null;

    // 3. Load settings JSON
    $settings = json_decode(file_exists($settingsFile) ? file_get_contents($settingsFile) : '{}', true);

    // 4. Get balance
    $balance = $merchantAPI->getBalance($playerId);

    // 5. Read currency from system_settings
    $currency = Config::getInstance()->getSetting('currency_symbol', '₹');

    echo json_encode([
        'status'     => 'success',
        'balance'    => $balance,
        'currency'   => $currency,
        'settings'   => $settings,
        'game_state' => $gameState
    ]);
    exit;
}
```

#### `spin` — Core game spin
```php
if ($action === 'spin') {
    $bet = (float)($data['bet'] ?? 0);

    // Session verification (same as above)
    // ...

    // Deduct bet
    $merchantAPI->updateBalance($playerId, $token, $bet, uniqid('BET_'), 'bet');

    // RNG (Provably Fair)
    $serverSeed = bin2hex(random_bytes(32));
    $clientSeed = $data['client_seed'] ?? bin2hex(random_bytes(16));
    $nonce      = mt_rand(1, 1000000);
    $hash       = hash_hmac('sha256', "$serverSeed-$nonce", $clientSeed);

    // ... generate grid from hash ...

    // Win calculation
    $totalWin = 0;
    // ... payline matching logic ...

    // Credit win
    if ($totalWin > 0) {
        $merchantAPI->updateBalance($playerId, $token, $totalWin, uniqid('WIN_'), 'win');
    }

    // Save state to bets table
    $newState = ['multiplier' => 1, 'free_spins' => 0];
    $stmt = $db->prepare("INSERT INTO bets (player_id, game_id, bet_amount, win_amount, status, state) VALUES (?,?,?,?,'completed',?)");
    $stmt->execute([$playerId, $gameId, $bet, $totalWin, json_encode($newState)]);

    echo json_encode([
        'status'      => 'success',
        'grid'        => $grid,
        'wins'        => $wins,
        'total_win'   => $totalWin,
        'new_balance' => $merchantAPI->getBalance($playerId),
        'state'       => $newState,
        'server_seed' => $serverSeed,
        'nonce'       => $nonce
    ]);
    exit;
}
```

#### Admin Actions (token = `admin_bypass`)
```php
if ($token === 'admin_bypass') {

    if ($action === 'get_settings') {
        $settings = json_decode(file_exists($settingsFile) ? file_get_contents($settingsFile) : '{}', true);
        echo json_encode(['settings' => $settings]);
        exit;
    }

    if ($action === 'save_settings') {
        $newSettings = $data['settings'] ?? [];
        file_put_contents($settingsFile, json_encode($newSettings, JSON_PRETTY_PRINT));
        echo json_encode(['status' => 'success']);
        exit;
    }

    if ($action === 'upload_symbol') {
        // Handle multipart file upload
        $symbolId  = $_POST['symbol_id'] ?? '';
        $file      = $_FILES['image'];
        $ext       = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
        $filename  = 'custom_' . preg_replace('/[^a-zA-Z0-9]/', '_', $symbolId) . '_' . time() . '.' . $ext;
        $targetDir = __DIR__ . '/../../../uploads/my_slot/';
        if (!file_exists($targetDir)) mkdir($targetDir, 0777, true);
        move_uploaded_file($file['tmp_name'], $targetDir . $filename);

        // Update settings JSON with new image path
        $settings = json_decode(file_exists($settingsFile) ? file_get_contents($settingsFile) : '{}', true);
        foreach ($settings['symbols'] as &$sym) {
            if ($sym['id'] === $symbolId) { $sym['customImage'] = $filename; break; }
        }
        file_put_contents($settingsFile, json_encode($settings, JSON_PRETTY_PRINT));
        echo json_encode(['status' => 'success', 'filename' => $filename]);
        exit;
    }
}
```

### Settings JSON Structure (`my_slot_settings.json`)
```json
{
  "rtp": 96,
  "min_bet": 1,
  "max_bet": 1000,
  "small_win_rate": 0,
  "free_spins_3_scatters": 10,
  "free_spins_4_scatters": 15,
  "free_spins_5_scatters": 20,
  "ace_meter_target": 50,
  "paytable": {
    "A":  {"3": 5,   "4": 20,  "5": 100},
    "K":  {"3": 4,   "4": 15,  "5": 80},
    "Wild": {"3": 5, "4": 20,  "5": 100}
  },
  "symbols": [
    {"id": "A",    "label": "Ace",     "customImage": null},
    {"id": "K",    "label": "King",    "customImage": null},
    {"id": "Wild", "label": "Wild",    "customImage": null}
  ],
  "buy_bonus_options": [
    {"spins": 10, "multiplier": 75,  "label": "10 Free Spins"},
    {"spins": 20, "multiplier": 150, "label": "20 Free Spins"}
  ]
}
```

---

## 5. Step 3 — Frontend React Component

**Path:** `frontend/src/plugins/my_slot/MySlot.jsx`

### How the game is launched
1. Player clicks game in lobby
2. Lobby calls `POST /backend/api/game-launch.php` with `{player_id, game_slug}`
3. Server creates a session token (128 hex chars, expires in 4 hours)
4. Server returns `launch_url` = `http://host/#/play/my_slot?token=TOKEN`
5. React Router matches `/play/:slug` → Lobby dynamically imports the plugin component
6. Component receives `token` from URL `?token=` query param

### Custom Hook (`hooks/useMySlot.js`)
```js
import { useState, useEffect, useCallback } from 'react';
import api from '../../../api/client';

export const useMySlot = (token) => {
    const [balance, setBalance]   = useState(0);
    const [currency, setCurrency] = useState('₹');
    const [settings, setSettings] = useState(null);
    const [grid, setGrid]         = useState([]);
    const [wins, setWins]         = useState([]);
    const [isSpinning, setIsSpinning] = useState(false);
    const [loading, setLoading]   = useState(true);

    // Load initial state
    const fetchState = useCallback(async () => {
        const res = await api.post('/plugins/my_slot/backend/api.php', {
            token, action: 'get_state'
        });
        if (res.data.status === 'success') {
            setBalance(parseFloat(res.data.balance));
            setCurrency(res.data.currency || '₹');
            setSettings(res.data.settings);
        }
        setLoading(false);
    }, [token]);

    // Spin
    const performSpin = async (bet) => {
        if (isSpinning) return;
        setIsSpinning(true);

        const res = await api.post('/plugins/my_slot/backend/api.php', {
            token, action: 'spin', bet
        });

        if (res.data.status === 'success') {
            setGrid(res.data.grid);
            setWins(res.data.wins);
            setBalance(parseFloat(res.data.new_balance));
        }
        setIsSpinning(false);
    };

    useEffect(() => { fetchState(); }, [fetchState]);

    return { balance, currency, settings, grid, wins, isSpinning, loading, performSpin };
};
```

### Main Component Template
```jsx
import React, { useState } from 'react';
import { useSearchParams } from 'react-router-dom';
import { useMySlot } from './hooks/useMySlot';

const MySlot = ({ onExit }) => {
    const [searchParams] = useSearchParams();
    const token = searchParams.get('token');
    const { balance, currency, grid, wins, isSpinning, loading, performSpin } = useMySlot(token);
    const [bet, setBet] = useState(1);

    if (loading) return <div className="text-white">Loading...</div>;

    return (
        <div className="fixed inset-0 bg-black flex justify-center items-center">
            <div className="relative w-full max-w-[420px] h-[100dvh] flex flex-col"
                 style={{ backgroundImage: "url('/backend/plugins/my_slot/assets/image/bg.png')", backgroundSize: '100% 100%' }}>

                {/* Header */}
                <header className="text-center pt-4 text-white text-xl font-bold">MY SLOT</header>

                {/* Reel Grid */}
                <main className="flex-1 flex items-center justify-center">
                    {/* Render your grid here using 'grid' state */}
                </main>

                {/* Footer Controls */}
                <footer className="pb-6 flex items-center justify-center gap-4">
                    <span className="text-white">{currency}{balance.toFixed(2)}</span>
                    <button
                        onClick={() => performSpin(bet)}
                        disabled={isSpinning}
                        className="w-16 h-16 rounded-full bg-blue-600 text-white font-bold">
                        SPIN
                    </button>
                </footer>
            </div>
        </div>
    );
};

export default MySlot;
```

---

## 6. Step 4 — Register in Frontend Router

The lobby auto-loads plugins via dynamic import. In `frontend/src/play/Lobby.jsx` the plugin is loaded when the URL slug matches. You just need to ensure the component file exists at the right path:

```
frontend/src/plugins/YOUR_SLUG/YourComponent.jsx
```

The slug in `plugin.json` must match the folder name AND the URL param (`/play/my_slot`).

---

## 7. Step 5 — Admin Panel (React)

**Path:** `frontend/src/plugins/my_slot/admin/AdminPanel.jsx`

```jsx
import React, { useState, useEffect } from 'react';
import api from '../../../api/client';

const PLUGIN_API = '/plugins/my_slot/backend/api.php';

const MySlotAdmin = () => {
    const [settings, setSettings] = useState(null);

    // Load settings
    useEffect(() => {
        api.post(PLUGIN_API, { token: 'admin_bypass', action: 'get_settings' })
           .then(res => setSettings(res.data.settings));
    }, []);

    // Save settings
    const saveSettings = async () => {
        await api.post(PLUGIN_API, { token: 'admin_bypass', action: 'save_settings', settings });
        alert('Saved!');
    };

    if (!settings) return <div>Loading...</div>;

    return (
        <div className="p-6">
            <h2 className="text-2xl font-bold mb-4">My Slot Settings</h2>

            {/* RTP Control */}
            <label>RTP (%)</label>
            <input type="number" value={settings.rtp}
                onChange={e => setSettings({...settings, rtp: parseFloat(e.target.value)})} />

            {/* Min/Max Bet */}
            <label>Min Bet</label>
            <input type="number" value={settings.min_bet}
                onChange={e => setSettings({...settings, min_bet: parseFloat(e.target.value)})} />

            {/* Symbol Image Upload */}
            {settings.symbols?.map(sym => (
                <div key={sym.id}>
                    <span>{sym.label}</span>
                    <input type="file" onChange={async (e) => {
                        const formData = new FormData();
                        formData.append('token', 'admin_bypass');
                        formData.append('action', 'upload_symbol');
                        formData.append('symbol_id', sym.id);
                        formData.append('image', e.target.files[0]);
                        await api.post(PLUGIN_API, formData, {
                            headers: { 'Content-Type': 'multipart/form-data' }
                        });
                    }} />
                </div>
            ))}

            <button onClick={saveSettings} className="bg-blue-600 text-white px-6 py-2 rounded">
                Save Settings
            </button>
        </div>
    );
};

export default MySlotAdmin;
```

### Register Admin Panel in Dashboard

In `frontend/src/admin/Dashboard.jsx`, add your plugin's admin component to the plugin management section:

```jsx
import MySlotAdmin from '../plugins/my_slot/admin/AdminPanel';

// Inside dashboard routes/tabs:
{ slug: 'my_slot', label: 'My Slot', component: <MySlotAdmin /> }
```

---

## 8. Step 6 — HTML Alternative Frontend

If you want a plain HTML frontend (not React), create:

**`frontend/public/games/my_slot/index.html`**

```html
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>My Slot Game</title>
</head>
<body style="background:#000; margin:0;">
<script>
    const urlParams = new URLSearchParams(window.location.search);
    const token = urlParams.get('token');
    const API   = '/casino/backend/plugins/my_slot/backend/api.php';

    async function apiCall(data) {
        const res = await fetch(API, {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ token, ...data })
        });
        return res.json();
    }

    // On load
    window.onload = async () => {
        const state = await apiCall({ action: 'get_state' });
        document.getElementById('balance').innerText = state.balance;
    };

    // Spin
    document.getElementById('spin-btn').onclick = async () => {
        const result = await apiCall({ action: 'spin', bet: 1 });
        console.log('Result:', result);
    };
</script>
<div id="balance">Loading...</div>
<button id="spin-btn">SPIN</button>
</body>
</html>
```

> For HTML games, bypass the React router by serving the file directly from the PHP backend.

---

## 9. API Reference — All Actions

| Action | Token | Method | Description |
|---|---|---|---|
| `get_state` | player token | POST | Load balance, settings, last game state |
| `spin` | player token | POST | Deduct bet, run RNG, calculate wins |
| `free_spin` | player token | POST | Same as spin but no balance deduction |
| `buy_bonus` | player token | POST | Purchase free spins with real balance |
| `get_settings` | `admin_bypass` | POST | Read settings JSON |
| `save_settings` | `admin_bypass` | POST | Write settings JSON |
| `upload_symbol` | `admin_bypass` | Multipart | Upload custom symbol image |

### Request format
```json
POST /backend/plugins/my_slot/backend/api.php
Content-Type: application/json

{
  "token": "SESSION_TOKEN_HERE",
  "action": "spin",
  "bet": 5.00,
  "client_seed": "optional_client_seed"
}
```

### Response format (spin)
```json
{
  "status": "success",
  "grid": [ {"name": "A", "golden": false}, ... ],
  "wins": [ {"line": 1, "symbols": 3, "win": 15.0, "symbol": "A"} ],
  "steps": [ { "grid": [...], "wins": [...], "state": {...}, "step_win": 15.0 } ],
  "total_win": 15.0,
  "new_balance": 285.0,
  "state": { "free_spins": 0, "multiplier": 1 },
  "server_seed": "abc123...",
  "nonce": 42345
}
```

---

## 10. MerchantAPI — Balance Management

```php
// Read balance
$balance = $merchantAPI->getBalance($playerId, $token);

// Deduct bet
$merchantAPI->updateBalance($playerId, $token, $betAmount, uniqid('BET_'), 'bet');

// Credit win
$merchantAPI->updateBalance($playerId, $token, $winAmount, uniqid('WIN_'), 'win');
```

Both calls auto-write to the `transactions` table. Bet also updates `current_turnover`.

---

## 11. Vite Dev Server Proxy

`frontend/vite.config.js`:
```js
server: {
  proxy: {
    '/backend': {
      target: 'http://localhost/casino/backend',
      changeOrigin: true,
      rewrite: (path) => path.replace(/^\/backend/, '')
    }
  }
}
```

So in React code you always call `/backend/plugins/...` and it proxies to the PHP backend automatically in dev. In production (built files served via XAMPP), the actual path resolves directly.

---

## 12. Game Session Flow (Full)

```
1. Player logs in → gets auth_token stored in localStorage
2. Player clicks game in lobby
3. Lobby POST → /backend/api/game-launch.php
   Body: { player_id, game_slug: 'my_slot' }
4. Server:
   a. Finds plugin via PluginManager
   b. Syncs game to `games` table if not exists
   c. Creates token in `game_sessions` (expires 4 hours)
   d. Returns launch_url = "http://host/#/play/my_slot?token=TOKEN"
5. Lobby opens launch_url (in iframe or navigate)
6. React loads MySlot.jsx component
7. Component reads token from useSearchParams()
8. useMySlot hook calls GET_STATE with token
9. Backend verifies token in game_sessions → returns balance + settings
10. Player spins → backend validates session → processes RNG → saves to bets table
```

---

## 13. RNG System (Provably Fair)

```php
// Server generates random seed
$serverSeed = bin2hex(random_bytes(32));

// Client may provide seed (or server generates one)
$clientSeed = $data['client_seed'] ?? bin2hex(random_bytes(16));

$nonce = mt_rand(1, 1000000);

// Hash combines both seeds
$hash = hash_hmac('sha256', "$serverSeed-$nonce", $clientSeed);

// Use hash sections to determine reel positions
for ($i = 0; $i < 5; $i++) {
    $slice = substr($hash, $i * 4, 8);
    $reelIndex = hexdec($slice) % count($reelStrips[$i]);
    // Pick symbols from strip starting at $reelIndex
}
```

Return `server_seed` and `nonce` to client so they can verify fairness.

---

## 14. Plugin Checklist (New Plugin Creation)

- [ ] **Create folder**: `backend/plugins/my_slot/`
- [ ] **Create** `plugin.json` with correct `slug` and `entry`
- [ ] **Create** `backend/plugins/my_slot/backend/api.php`
- [ ] **Create** `backend/plugins/my_slot/backend/my_slot_settings.json`
- [ ] **Add thumbnail**: `backend/plugins/my_slot/assets/image/logo.webp`
- [ ] **Add background**: `backend/plugins/my_slot/assets/image/bg.png`
- [ ] **Create frontend**: `frontend/src/plugins/my_slot/MySlot.jsx`
- [ ] **Create hook**: `frontend/src/plugins/my_slot/hooks/useMySlot.js`
- [ ] **Create admin**: `frontend/src/plugins/my_slot/admin/AdminPanel.jsx`
- [ ] **Register in DB** (auto via game-launch.php first call, or manually `INSERT INTO games`)
- [ ] **Test**: Run `npm run dev` in frontend, open `/#/play/my_slot?token=TEST`
- [ ] **Build**: `npm run build` to deploy

---

## 15. Key Constants & Defaults

| Setting | Default | Notes |
|---|---|---|
| DB name | `casino` | `Config.php` |
| DB user | `root` | `Config.php` |
| DB pass | `""` | `Config.php` |
| Session expiry | 4 hours | `game-launch.php` |
| Admin token | `admin_bypass` | For admin API calls |
| Currency key | `currency_symbol` | From `system_settings` table |
| Frontend port | `5173` (Vite) | `npm run dev` |
| Backend base | `http://localhost/casino/backend` | Vite proxy target |
