Game Development Guide
Build an HTML5 casino game for this RGS with one PHP class (the outcome engine) and one PixiJS frontend (the renderer). The platform handles sessions, wallet debits/credits, round persistence, player history and replay — your code only decides outcomes and draws them. The whole workflow is also exposed as an MCP server so AI agents can build games end-to-end.
Overview & architecture
The RGS (Remote Gaming Server) sits between the game and the casino's wallet. A game has two halves:
- a frontend (HTML/JS) — what the player sees and interacts with
- a backend outcome engine — a PHP class the RGS loads, which decides whether the player won and how much
One round of play
-
The casino launches your game URL with a
token(wallet session) andgame_id. -
The client SDK calls
load_game— the RGS validates the token against the wallet, opens a game session and returns bet configuration + balance. -
The player acts; the client SDK calls
playwithbet_amount+ your game-specific parameters. - The RGS runs your game class to decide the outcome, settles bet/win with the wallet (atomically where supported), persists the round for history and replay, and returns your payload plus the new balance and a fresh session.
- The client animates the result and calls
ack_game_session.
Money is handled in integer cents internally; the wire
format uses decimal strings. Game code never touches either — you return a float
winAmount and the SDK does the rest.
Components
Server-side Game SDK —
app/Sdk/
The abstraction every new game is built on. A game is one stateless PHP class; the SDK handles everything else.
| Component | Path | Role |
|---|---|---|
GameInterface |
app/Sdk/Contracts/GameInterface.php |
The contract: play(GameContext): GameResult |
GameContext |
app/Sdk/GameContext.php |
Input: player, bet, client params, prior state, RNG |
GameResult |
app/Sdk/GameResult.php |
Output: win amount, client payload, replay data, round state |
GameException |
app/Sdk/GameException.php |
Clean rejection — aborts before the player is charged |
Rng |
app/Sdk/Rng.php |
CSPRNG helpers — the only allowed randomness source |
GameRegistry |
app/Sdk/GameRegistry.php |
Maps game codes → classes (from config/games.php) |
GameKernel |
app/Sdk/GameKernel.php |
Runs SDK games, persists rounds, adapts to the wire format |
GameDispatcher |
app/Sdk/GameDispatcher.php |
Routes to the SDK kernel or the frozen legacy engine |
Client SDK —
resources/rgs-games/_shared/
| File | Role |
|---|---|
rgs-sdk.js |
The game client API: boot(), play(), ack(),
balance tracking, session rotation, replay mode, error normalization
|
rgs_bootstrap.js |
Launch-parameter handling (token, rgs_url, replay payloads) |
query_string.js |
Query-string helper |
Your rendering layer (PixiJS) sits on top of this — the SDK talks to the server, PixiJS draws the results. See step 3.
RGS API — app/Actions/Rgs/, RgsController
One endpoint, POST /api/rgs, dispatched by the rquest parameter — the full
reference is below.
Wallet abstraction — app/Services/Wallet/
Games never touch money. The play pipeline settles every round through a wallet adapter selected by
the WALLET_ADAPTER environment variable:
| Adapter | Use |
|---|---|
platform |
The in-house platform wallet (combined bet+win call) |
qtech |
QTech Game Provider API v1.11 (see docs/QTECH-INTEGRATION.md) |
local |
Dev-only in-process wallet for browser-testing games (refuses production) |
Data model
| Table | Meaning |
|---|---|
games |
Game catalogue: code (game_id), name, min/max bet, RTP, active flag |
game_sessions |
One per round-in-progress per player; rotates when a round completes |
game_rounds |
One per round: bet/win in cents, win type, input/result/steps/state JSON — powers history and replay |
rgs_players, rgs_wallet_transactions |
External-player mapping, wallet audit + rollback data |
Tooling
| Tool | Purpose |
|---|---|
php artisan make:game <code> "<Name>" |
Scaffolds a complete runnable game (backend, frontend, blade view, registry entry, DB row) |
MCP server (php artisan mcp:start game-sdk) |
Lets AI agents build games: docs, reference code, scaffolding, validation, real play-testing, test runs |
Reference game Dice Duel (dd) |
app/Games/DiceDuelGame.php + resources/rgs-games/dd/ — the
template to imitate
|
| Test suite | Pest; game-test pattern in tests/Feature/Sdk/GameSdkTest.php |
Random Number Generator (RNG)
The RGS includes a certified-random module that all games must
use for outcome determination. The App\Sdk\Rng class wraps PHP's cryptographically
secure random number generator (random_int) and provides game-specific helpers.
Important: Games must never use
mt_rand(), rand(), or PHP's shuffle() directly. Always
use $context->rng so the RNG source stays swappable and auditable.
Accessing the RNG
The RNG is available in your game's play() method via the context object:
public function play(GameContext $context): array
{
$rng = $context->rng;
// Roll a die (1-6)
$roll = $rng->int(1, 6);
// Draw 20 unique numbers from 1-80 (keno)
$drawn = $rng->uniqueInts(1, 80, 20);
// Shuffle a deck of cards
$deck = $rng->deck();
}
RNG Methods
| Method | Description | Example |
|---|---|---|
int(min, max) |
Random integer between min and max (inclusive) | $rng->int(1, 6) → 1–6 |
chance(probability) |
Returns true with given probability (0.0–1.0) | $rng->chance(0.25) → 25% true |
pick(array) |
One random element from a non-empty array | $rng->pick(['a','b','c']) |
uniqueInts(min, max, count) |
Draw count distinct integers (lottery/keno style) | $rng->uniqueInts(1, 80, 20) |
shuffle(array) |
Cryptographically shuffled copy (Fisher-Yates) | $rng->shuffle($items) |
weightedPick(weights) |
Pick a key based on relative weights | $rng->weightedPick(['cherry'=>50, 'seven'=>5]) |
deck() |
Shuffled 52-card deck as 0–51 indexes | $rng->deck() |
Card deck encoding
The deck() method returns card indexes 0–51. To decode:
$cardIndex = 27; // Example card
$suit = intdiv($cardIndex, 13); // 0=spades, 1=hearts, 2=diamonds, 3=clubs
$rank = $cardIndex % 13; // 0=2, 1=3, ... 8=10, 9=J, 10=Q, 11=K, 12=A
Weighted picks for slots/symbols
Use weightedPick() for slot reels or any outcome where symbols have different
probabilities:
$symbol = $rng->weightedPick([
'cherry' => 50, // 50/56 ≈ 89.3%
'seven' => 5, // 5/56 ≈ 8.9%
'wild' => 1, // 1/56 ≈ 1.8%
]);
Security & compliance
-
CSPRNG backed — All methods use PHP's
random_int(), which sources entropy from the operating system's cryptographic random number generator. - Auditable — Centralizing RNG in one class makes it easy to audit, test, and potentially swap for a hardware RNG or third-party certified source.
- Deterministic testing — For unit tests, you can mock the Rng class to produce predictable sequences.
Build a game, step by step
Step 0 — Design the math first
- Game mechanics: which inputs the player gives, how the outcome is decided.
- Math model: paytable and RTP. Compute the theoretical RTP (Σ probability × payout) — you must state it at registration and it should typically be 90–99%.
- Round shape: single-step (slots, dice, keno) or multi-step (blackjack-style decisions mid-round)?
Step 1 — Scaffold
php artisan make:game hilo "Hi-Lo Cards" --rtp=96.5 --category=table
Creates a working 50/50 placeholder game, registered everywhere:
| Artifact | Path |
|---|---|
| Backend game class | app/Games/HiloGame.php |
| Frontend | resources/rgs-games/hilo/index.html + js/game.js |
| Blade view | resources/views/rgs-games/hilo.blade.php |
| Registry entry | config/games.php |
| Database row | games table (skip with --no-db) |
Never create these files by hand — always scaffold.
Step 2 — Backend: the PHP outcome module
The game backend is one stateless class in app/Games/ implementing
App\Sdk\Contracts\GameInterface. The RGS loads it via the registry in
config/games.php. The whole game is play():
public function play(GameContext $context): GameResult
{
// 1. Validate input (untrusted!) — GameException aborts before any charge
$guess = strtolower((string) $context->requireInput('guess'));
if (! in_array($guess, ['low', 'high'], true)) {
throw new GameException('guess must be "low" or "high"');
}
// 2. Decide the outcome — ONLY via $context->rng
$card = $context->rng->int(1, 13);
$won = ($guess === 'high') === ($card > 7);
$win = $won ? round($context->betAmount * 1.9, 2) : 0.0;
// 3. Return payload (for the client) + replay data (for history)
return GameResult::completed($win)
->payload(['card' => $card, 'won' => $won])
->input(['guess' => $guess])
->outcome(['card' => $card, 'won' => $won])
->step('draw', ['card' => $card])
->summary("Guessed {$guess}, drew {$card}.");
}
Full GameContext/GameResult/Rng reference:
Server SDK. Multi-step
games return GameResult::awaitingAction($state) instead — see
Multi-step games.
Step 3 — Frontend: PixiJS rendering on the client SDK
The frontend lives in resources/rgs-games/{code}/ and is served at
/games/{code}/index.html. It has exactly one job:
render what the server returns. It must never compute or
predict outcomes. All RGS communication goes through _shared/rgs-sdk.js — PixiJS is
the rendering layer on top.
Load order in index.html (shared scripts via relative paths, no hardcoded hosts):
<script src="https://cdn.jsdelivr.net/npm/pixi.js@8.x/dist/pixi.min.js"></script>
<script src="../_shared/query_string.js" onerror="this.remove()"></script>
<script src="../_shared/rgs_bootstrap.js"></script>
<script src="../_shared/rgs-sdk.js"></script>
<script src="js/game.js"></script>
The pattern — boot the SDK, build the PixiJS stage when the game config arrives, and animate server results:
var app, stage;
RgsSdk.boot({
onReady: function (game) {
// game.minBet / maxBet / betSteps / defaultBet / balance / currency
app = new PIXI.Application();
app.init({ resizeTo: window, background: '#0f0f23' }).then(function () {
document.body.appendChild(app.canvas);
buildTable(game); // draw the idle scene + bet UI
});
},
onReplay: function (replay) {
// Replay mode: render replay.input / replay.result / replay.steps.
// replay.winamount and replay.betamount are decimals (same as play API).
renderReplay(replay);
},
onError: function (message) { showErrorOverlay(message); }
});
RgsSdk.onBalanceChange(function (balance) { hud.balance.text = balance.toFixed(2); });
function onSpinPressed(bet, guess) {
RgsSdk.play({ bet_amount: bet, guess: guess })
.then(function (res) {
// res = your GameResult payload + winamount, accountsum, new_game_session
animateOutcome(res, function () { RgsSdk.ack(); });
})
.catch(function (err) { showErrorOverlay(err.message); });
}
Frontend requirements:
-
Build the bet UI from
bet_steps/min_bet/max_bet/default_bet— never hardcode bets. If the wallet caps bets, the ladder arrives pre-clamped. -
Handle the error callback and rejected
play()promises: show a message, halt. -
Support replay mode (
onReplay) rendering purely from the persisted round data. -
The displayed balance always comes from RGS responses (
onBalanceChange), never from local arithmetic. -
Keep everything self-contained under the game folder (assets, sounds); load shared scripts
relatively (
../_shared/…). Vendor the PixiJS bundle into the game folder for production builds if offline serving is required.
Step 4 — Verify & test
-
Wiring — registry entry, DB row and files consistent?
(The MCP
validate-gametool checks this automatically.) -
Browser — set
WALLET_ADAPTER=localin.env, then open/games/hilo/index.html?token=dev&game_id=hilo. Every token is a separate player starting with 1000.00 EUR. Play wins, losses, insufficient balance, and try to break it. -
Automated — write a Pest feature test per the pattern in
tests/Feature/Sdk/GameSdkTest.php: full round viaPOST /api/rgs, payout amount on a win, invalid input → HTTP 400 with no wallet transaction. Run./vendor/bin/pest. -
Replay — open
/player/history?token=…, replay a round, confirm it renders from stored data.
it('plays a round', function () {
$load = $this->postJson('/api/rgs', [
'rquest' => 'load_game', 'token' => 't', 'game_id' => 'hilo',
])->json();
$res = $this->postJson('/api/rgs', [
'rquest' => 'play',
'token' => 't',
'game_session' => $load['game_session'],
'bet_amount' => 1.0,
'guess' => 'high',
]);
$res->assertOk()->assertJsonPath('status', 'OK');
});
Step 5 — Release
-
Confirm the
gamesrow (name, min/max bet, RTP, category,is_active). ./vendor/bin/pinton changed files; full test suite green.-
Deploy — games are served via the
public/gamessymlink (php artisan games:linkin the deploy pipeline).
Sandbox mode (preview development)
The sandbox environment lets you develop and test games without wallet/platform registration. Games run with an isolated sandbox wallet (1000.00 EUR balance).
Creating a sandbox game
php artisan make:game hilo "Hi-Lo Cards" --sandbox --rtp=96.5 --category=table
This creates:
app/Games/Sandbox/HiloGame.php— backend classresources/rgs-games/_sandbox/hilo/— frontend filesresources/views/rgs-games/_sandbox/hilo.blade.php— blade view- Row in
sandbox_gamestable (status:pending)
Test immediately at
/sandbox/games/hilo/play.html — no token needed.
Distributed development with MCP
The RGS MCP server supports a distributed workflow where different AI agents or developers work on backend and frontend separately:
┌─────────────────────┐ ┌─────────────────────┐ ┌─────────────┐
│ RGS MCP Server │ │ Local AI / Dev │ │ Developer │
│ (Backend) │ │ (Frontend) │ │ (Human) │
└──────────┬──────────┘ └──────────┬──────────┘ └──────┬──────┘
│ │ │
1. scaffold-game ────────────────────┼───────────────────────┤
--sandbox │ │
│ │ │
│ 2. Build frontend │
│ (HTML/JS/CSS) │
│ │ │
│◄──────────────────────────┤ │
│ 3. upload-game-frontend │ │
│ (base64 ZIP) │ │
│ │ │
4. Preview URL ──────────────────────┼──────────────────────►│
/sandbox/games/{code}/play.html │ │
│ │ │
│ │ 5. Test & Approve
│ │ │
6. export-sandbox-game ◄─────────────┼───────────────────────┤
│ │ │
HTTP MCP Endpoint: POST /mcp/game-sdk
# Upload frontend (base64-encoded ZIP)
curl -X POST "https://rgs.cloudvlt.com/mcp/game-sdk" \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "upload-game-frontend",
"arguments": {
"code": "mygame",
"zip_base64": "UEsDBBQ..."
}
}
}'
Frontend ZIP requirements:
- Must contain
index.htmlat root level - Should contain
js/game.jsfor game logic -
Use relative paths:
../_shared/sandbox_bootstrap.jsand../_shared/rgs-sdk.js
Filament admin — sandbox game management
The RGS Filament admin panel (/admin/sandbox-games) provides a UI for managing
sandbox games with an approval workflow:
- Preview — Opens the game in a new tab for testing
- Approve — Marks game as approved
- Reject — Marks game as rejected with a reason
- Export ZIP — Downloads production-ready bundle
- Delete — Removes game files and database record
| Permission | Description |
|---|---|
view_sandbox_games |
View the sandbox games list |
approve_sandbox_games |
Approve, reject, or reset game status |
delete_sandbox_games |
Delete sandbox games |
Exporting to production
When ready for production, export the game:
php artisan sandbox:export hilo
# Or via MCP: export-sandbox-game code=hilo
This creates a ZIP bundle with:
- Production-namespaced PHP class
- Frontend files with production paths
config-entry.txt— line to add toconfig/games.phpmigration.sql— INSERT forgamestable
Multi-step games
Games where the player makes decisions mid-round (blackjack: hit / stand / double) keep the round
open by returning awaitingAction:
public function play(GameContext $context): GameResult
{
if (! $context->isContinuation) {
// First call: the bet is taken now.
$deck = $context->rng->deck();
$hand = [array_shift($deck), array_shift($deck)];
return GameResult::awaitingAction([
'deck' => $deck,
'hand' => $hand,
'bet' => $context->betAmount, // remember the stake yourself
])
->payload(['hand' => $hand, 'actions' => ['hit', 'stand']])
->step('deal', ['hand' => $hand]);
}
// Continuation: betAmount is 0, your saved state comes back.
$state = $context->state;
return match ($context->action()) {
'hit' => $this->hit($context, $state),
'stand' => $this->stand($context, $state), // -> GameResult::completed(...)
default => throw new GameException('Illegal action'),
};
}
What the SDK does for you:
-
The session is marked resumable; the next
playcall routes back withisContinuation = trueand your$state. -
The stake is charged once, on the opening call; the win is
paid when you return
completed. -
One round row is kept and updated each step — carry steps forward with
->steps([...$context->steps, ...$newSteps]). -
The client keeps the same
game_sessionuntil it receivesnew_game_session(automatic with the client SDK).
Game replay
Every completed round can be re-watched: the game frontend is
re-opened in a special replay mode and re-renders the round purely from the data persisted when it
was played — no game logic runs again, no wallet is touched, nothing is random. Replay powers the
player history page, admin round inspection, and the QTech game-result verification
endpoint, so recording good replay data is a hard requirement, not a nicety.
1. Recording — what the backend persists
During play(), your game records the round on the GameResult:
| Call | Persisted as | What to put in it |
|---|---|---|
->input(array) |
game_rounds.input |
What the player chose (their guess, picked numbers, actions) |
->outcome(array) |
game_rounds.result |
What happened (dice, cards, totals, won/lost) |
->step(event, data) |
game_rounds.steps |
One entry per visual moment (deal, draw, spin, hit) — the animation script |
Design test: could someone re-render the round with only this data, without running
your game class? If not, record more. Multi-step games must carry steps forward with
->steps([...$context->steps, ...$new]) so the whole round replays, not just
the last action.
2. Delivery — how a replay reaches the game
-
GET /player/history?token=…lists the player's rounds; each row carries a temporary signed replay URL (2 hours; QTechgame-resultlinks are valid 24 hours). Replay URLs cannot be constructed by hand. -
The signed URL opens
GET /player/replay/{roundId}, which loads the stored round and embeds the game frontend, launching it withreplay=<roundId>plus a#replay_data=<base64url-JSON>fragment containing the full round payload. -
The same payload is also available raw (with a valid signature) at
GET /api/game-round/{roundId}/replay.
3. Rendering — the onReplay callback
The client SDK detects the replay fragment during RgsSdk.boot(), decodes it, skips the
load_game handshake and fires onReplay(replay) instead of
onReady. The replay object:
| Field | Meaning |
|---|---|
replay.input |
What you stored via GameResult::input() |
replay.result |
What you stored via GameResult::outcome() |
replay.steps |
The recorded step list, in order |
replay.winamount |
Win amount as decimal (same format as play API) |
replay.betamount |
Bet amount as decimal |
How Dice Duel renders it (resources/rgs-games/dd/js/game.js) — show a replay badge,
disable the controls, hide the live balance (there is none), and run the normal outcome animation
from the stored result:
function runReplay(replay) {
el.replayBadge.style.display = 'block';
setBusy(true); // no betting in replay
var result = replay.result || {};
var winAmount = replay.winamount || 0;
el.balance.parentNode.style.visibility = 'hidden';
if (result.dice) {
animateRoll(result.dice, function () {
showOutcome(!!result.won, winAmount, result.total);
});
}
}
Behaviour in replay mode:
-
RgsSdk.isReplay()returnstrue;RgsSdk.play()rejects with"Cannot play in replay mode"— the mode is read-only by construction. -
No RGS calls are made: there is no session, no live balance, no
ack(). Hide or freeze the bet UI and balance HUD. -
Reuse your normal rendering/animation path fed from
replay.result/replay.steps— a PixiJS frontend replays by running the same timeline it uses for live results, just sourced from the stored steps instead of aplay()response.
4. Verifying replay (definition of done)
-
Play a few rounds against the local wallet, open
/player/history?token=dev, and replay one — it must render correctly from stored data alone. - For multi-step games, replay a round with several actions and confirm every step re-renders in order.
- Endpoint details: History & replay API; launch parameters: Game launch URL.
Using Dice Duel as a template
Dice Duel (code dd) is the reference implementation
every new game should imitate — a complete, shippable game in two small files:
| Half | File | What to copy from it |
|---|---|---|
| Backend | app/Games/DiceDuelGame.php |
Input validation with GameException, RNG usage, payout math,
payload/input/outcome/step/summary recording
|
| Frontend | resources/rgs-games/dd/index.html + js/game.js |
The complete client game loop in ~150 lines: boot → render config → play → animate → ack → repeat, plus replay mode and error handling |
The game itself: the player bets low or high, two dice are rolled; totals 2–6 are low, 8–12 are high, 7 pushes (stake returned). A correct guess pays 2.3×.
Workflow: template → your game
- Read both Dice Duel files (or use the MCP
read-reference-gametool). -
Scaffold your game:
php artisan make:game <code> "<Name>"— the scaffold is itself a minimal Dice-Duel-shaped game. -
Replace the outcome logic in
play()keeping the same structure: validate → RNG → payout → record. -
Replace the DOM rendering in
js/game.jswith your PixiJS stage, keeping the SDK calls (boot/play/ack/onBalanceChange/onReplay) exactly as the template uses them. -
Try it live:
/games/dd/index.html?token=dev&game_id=dd(requiresWALLET_ADAPTER=local).
Building with AI agents (MCP)
The repo ships an MCP (Model Context Protocol) server exposing the whole game-building workflow as tools, so an AI agent can scaffold, implement, validate and play-test a game end-to-end without human plumbing.
Connect via HTTP (Remote / Cloud)
The MCP server is available via HTTP at:
https://rgs.cloudvlt.com/mcp/game-sdk
Claude Desktop
Add to ~/Library/Application Support/Claude/claude_desktop_config.json:
{
"mcpServers": {
"rgs-game-sdk": {
"url": "https://rgs.cloudvlt.com/mcp/game-sdk"
}
}
}
Restart Claude Desktop after editing.
Cursor / Windsurf
Add to .cursor/mcp.json or .windsurf/mcp.json in your project:
{
"mcpServers": {
"rgs-game-sdk": {
"url": "https://rgs.cloudvlt.com/mcp/game-sdk"
}
}
}
Test with curl
curl -X POST https://rgs.cloudvlt.com/mcp/game-sdk \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}'
Connect via stdio (Local Development)
For local development, the MCP server is auto-wired for Claude Code via .mcp.json:
{
"mcpServers": {
"rgs-game-sdk": {
"command": "php",
"args": ["artisan", "mcp:start", "game-sdk-local"]
}
}
}
| Tool | Purpose |
|---|---|
read-docs |
This guide, the SDK reference, QTech docs, READMEs |
read-reference-game |
Dice Duel source, scaffold stubs, the client SDK |
scaffold-game |
Runs make:game |
list-games |
All games with engine, limits, frontend status |
validate-game |
Checks registry/class/DB/frontend wiring |
play-test-round |
Plays real rounds through the full pipeline (dev wallet) — verifies outcomes, payouts, input validation |
run-tests |
Runs the Pest suite |
The agent workflow
-
read-docs(topicgame-development, thengame-sdk) — learn components, process, requirements. -
read-reference-game— study Dice Duel (backend + frontend) before writing code. -
scaffold-game— generate the runnable skeleton. Never create the files by hand. -
Edit
app/Games/{Class}Game.php(PHP outcome module) andresources/rgs-games/{code}/js/game.js+index.html(PixiJS UI). validate-game— verify all wiring is consistent.-
play-test-round— play real rounds, including invalid input (it must fail cleanly with no charge). -
run-tests— the whole suite must pass; add Pest tests pertests/Feature/Sdk/GameSdkTest.php.
Server implementation: app/Mcp/GameSdkServer.php. Start manually with
php artisan mcp:start game-sdk for any MCP-compatible client.
API reference — POST /api/rgs
One endpoint; the rquest parameter selects the method. (POST /api/api.php
is a legacy alias.) The client SDK wraps all of this — game code normally never calls these
directly.
📦 Postman Collection:
Download RGS API Collection
— Import into Postman to test all endpoints against https://rgs.cloudvlt.com.
Includes load_game, play (Dice Duel + Blackjack), get_accountsum, and ack_game_session with
auto-saved session variables.
Conventions
-
Requests:
POSTwithapplication/x-www-form-urlencodedor JSON bodies. -
Responses: JSON. Every success carries
"status": "OK"; every failure is HTTP 400 with{"status": "Failed", "msg": "reason"}. -
Money: decimal strings with 2 decimals on the wire (
"accountsum": "998.70"); bets sent as decimal numbers. Internally integer cents. -
Auth: the wallet session
tokenfrom the launch URL authenticates every call. -
CORS:
/api/rgsanswers withAccess-Control-Allow-Origin: *.
Common parameters for every method:
| Param | Type | Required | Description |
|---|---|---|---|
rquest |
string | yes |
Method name: load_game, play, get_accountsum,
ack_game_session
|
token |
string | yes | Wallet session token from the launch URL |
auth Get Wallet Session Token
Before calling any RGS API method, you need a wallet session token. This token authenticates the player and is obtained by logging in via the platform API.
POST {platform_url}/api/portal/players/login
Content-Type: application/json
{
"email": "player@example.com",
"password": "secret",
"player_ip": "1.2.3.4",
"portal_id": 1
}
{
"status": "success",
"wallet_session_token": "1|abc123xyz...",
"player": {
"id": 42,
"email": "player@example.com",
"name": "John Doe",
"account": {
"accountsum": 1000.00,
"currency": { "iso_code": "EUR", "symbol": "€" }
}
}
}
The wallet_session_token is what you pass as token to all RGS API
calls. It's a Laravel Sanctum token that identifies the player session.
💡 For development: When using
WALLET_ADAPTER=local, any token value works (e.g. token=dev). Each
unique token gets a fresh player with 1000.00 EUR balance.
rquest load_game
Validates the token against the wallet, opens a game session, and returns the game configuration. Must be called once before playing.
| Param | Type | Required | Description |
|---|---|---|---|
game_id |
string | yes | Game code (dd, …) — must match games.game_id |
POST /api/rgs
rquest=load_game&token=<wallet-token>&game_id=dd
{
"game_id": "dd",
"sname": "dd",
"name": "Dice Duel",
"game_session": "01JZWX3E5H8Q2R9T0V1W2X3Y4Z",
"player_name": "mrCat",
"currency": "EUR",
"language": "en",
"accountsum": "1000.00",
"min_bet": 0.10,
"max_bet": 100.00,
"bet_steps": [0.10, 0.20, 0.50, 1.00, 2.00, 5.00],
"default_bet": 0.10,
"lines": 1,
"status": "OK"
}
| Field | Meaning |
|---|---|
game_session |
Session id to pass to every play call. One session = one round. |
accountsum |
Player balance (decimal string) |
min_bet / max_bet / bet_steps /
default_bet
|
Bet configuration — clients must build their bet UI from these; the ladder arrives pre-clamped if the wallet caps bets |
lines |
Line count for slot-style games (1 otherwise) |
Failures: missing/invalid token or unknown game →
400 "Invalid token or game not available".
rquest play
Plays one round — or one step of a multi-step round. Runs the game's outcome logic, settles bet/win with the wallet, persists the round.
| Param | Type | Required | Description |
|---|---|---|---|
game_session |
string | yes |
The current session from load_game (or the last
new_game_session)
|
bet_amount |
decimal | yes* | Stake for this round. *Ignored (treated as 0) when continuing an open multi-step round. |
game_id |
string | no | Game code (informational; the session already knows it) |
| game-specific… | any | per game |
Every other parameter is handed to the game class as
$context->input. Dice Duel: guess=low|high. Multi-step:
action=bet|hit|stand|…
|
POST /api/rgs
rquest=play&token=<t>&game_session=01JZ…&bet_amount=1.00&guess=high
{
"dice": [6, 4],
"total": 10,
"guess": "high",
"won": true,
"winamount": 2.3,
"status_id": 0,
"accountsum": "1001.30",
"multi_state": 0,
"new_game_session": "01JZWX4A7B8C9D0E1F2G3H4J5K",
"status": "OK"
}
| Field | Meaning |
|---|---|
| game payload |
Whatever the game returned via GameResult::payload() (here:
dice, total, guess, won)
|
winamount |
Win for this call (decimal) |
accountsum |
Balance after settlement |
multi_state |
0 = round finished. 1 = round still open — the player must
act again
|
status_id |
0 finished, 3 awaiting player action |
new_game_session |
Present only when the round completed: use it
for the next round. While multi_state=1, keep sending the same
game_session.
|
Failures (all 400):
- invalid/expired session →
"Game session not found" -
game input rejected (
GameException) → the game's message — no wallet transaction occurs - insufficient funds → wallet error message; rollback semantics guarantee the player is refunded on partial failure
rquest get_accountsum
Balance refresh (e.g. on idle). Takes only the common parameters.
{ "accountsum": "998.70", "currency": "EUR", "status": "OK" }
rquest ack_game_session
The client confirms the round result was displayed to the player. Fire-and-forget bookkeeping; failures can be ignored by clients.
| Param | Type | Required |
|---|---|---|
game_session |
string | yes |
{ "msg": "game session acknowledged", "status": "OK" }
load_resume_game is deprecated — resuming works through
play itself: a resumable session routes the next play call back
into the open round.
GET /api/round/{id}/outcome
Returns a signed URL to view the visual replay of a specific game round. Useful for support, auditing, or sharing round outcomes with players.
| Param | Type | Description |
|---|---|---|
id |
integer | The game round ID (from game_rounds table) |
GET /api/round/739/outcome
{
"round_id": 739,
"replay_url": "https://rgs.cloudvlt.com/player/replay/739?signature=abc123..."
}
Opening replay_url displays the game with the exact outcome of that round,
including all bets, cards dealt, and final result.
Game launch URL
Games are static frontends served at /games/{code}/index.html. The casino/aggregator
launches them with query parameters:
| Param | Required | Description |
|---|---|---|
token |
yes | Wallet session token for the player |
game_id |
yes | Game code, passed to load_game |
rgs_url (alias api_url) |
no | Full RGS endpoint URL; defaults to <origin>/api/rgs |
device_id |
no | web | mobile |
resume_session |
no | Session id to resume |
replay + #replay_data= |
no | Replay mode — the game renders the encoded round instead of playing |
https://rgs.cloudvlt.com/games/dd/index.html?token=abc123&game_id=dd&device_id=web
Player history & replay
Everything a game records via GameResult::input() / outcome() / step() powers these
automatically. Replay links are temporary signed URLs.
| Endpoint | Data required | Purpose |
|---|---|---|
GET /player/history |
token (query), optional game, page |
HTML page listing the player's rounds with replay links (2h signed) |
GET /player/replay/{roundId} |
valid signature | HTML page embedding the game in replay mode |
GET /api/game-round/{roundId}/replay |
valid signature |
Raw replay JSON: id, game_code, betamount,
winamount (decimals), input, result,
steps, created_at
|
The #replay_data= fragment handed to the game is base64url-encoded JSON of that
payload; the client SDK decodes it and calls the game's onReplay(replay) callback. All
money values (winamount, betamount) are decimals, consistent with the play
API. The full flow — recording, delivery, rendering — is described in
Game replay.
Health check
GET /api/health → 200 { "status": "ok" }
GET /up → 200 (framework liveness)
QTech platform inbound endpoints (/api/qtech/game-launch,
/api/qtech/game-result) and the outbound platform-wallet contract are documented in
docs/API.md and docs/QTECH-INTEGRATION.md.
Server SDK reference (PHP)
Namespace App\Sdk. Your game implements one method:
interface GameInterface
{
public function play(GameContext $context): GameResult;
}
Game classes are resolved through the Laravel container (constructor injection works) and
must be stateless — all round state travels through
GameContext/GameResult.
GameContext (input)
| Property / method | Type | Meaning |
|---|---|---|
$context->playerId |
int |
Internal player id |
$context->gameCode |
string |
Your game code ('dd') |
$context->sessionId |
string |
Current game session ULID |
$context->currency |
string |
Player currency ('EUR') |
$context->betAmount |
float |
Stake for this call — 0.0 on continuation steps |
$context->input |
array |
All client parameters of the play call |
$context->input($key, $default) |
mixed |
Read one parameter |
$context->requireInput($key) |
mixed |
Read one parameter or throw GameException |
$context->action() |
string |
Multi-step action ('bet', 'hit', …), defaults
'bet'
|
$context->isContinuation |
bool |
true when resuming an open round |
$context->state |
?array |
State returned from the previous step (null on fresh rounds) |
$context->steps |
array |
Steps recorded so far this round |
$context->rng |
Rng |
The only allowed randomness source |
GameResult (output)
| Factory / setter | Meaning |
|---|---|
GameResult::completed(float $winAmount = 0) |
Round finished. Wallet settles; the client gets a fresh session. |
GameResult::awaitingAction(array $state, float $winAmount = 0) |
Round still open (multi-step games) |
->payload(array) |
Client-visible response data (winamount and status_id added
automatically)
|
->input(array) |
What the player chose — persisted for history/replay |
->outcome(array) |
What happened — persisted for history/replay |
->step(string $event, array $data = []) |
Append one replayable step |
->steps(array) |
Replace all steps (use to carry $context->steps forward) |
->summary(string) |
One human-readable line describing the round |
->winType(string) |
Override the recorded win type (default derived
win/push/no-win)
|
Rng
Backed by PHP's CSPRNG. Available as $context->rng:
| Method | Example |
|---|---|
int($min, $max) |
die roll: int(1, 6) |
chance($probability) |
chance(0.475) → bool |
pick($items) |
pick(['heads','tails']) |
uniqueInts($min, $max, $count) |
keno draw: uniqueInts(1, 80, 20) |
shuffle($items) |
shuffled copy, Fisher-Yates |
weightedPick($weights) |
slot reels: weightedPick(['cherry'=>50,'seven'=>5]) |
deck() |
shuffled 52-card deck as 0–51 indexes |
GameException
Throw it (or use requireInput()) to reject a request cleanly. The request aborts
before any wallet transaction — the player is never charged —
and the client receives HTTP 400, which the client SDK surfaces as a rejected promise.
Client SDK reference (JS)
resources/rgs-games/_shared/rgs-sdk.js
(ES5, no dependencies). Exactly one of the three boot callbacks fires.
| Function | Purpose |
|---|---|
RgsSdk.boot(options) |
Bootstrap + load_game handshake; onReady(game) /
onReplay(replay) / onError(message)
|
RgsSdk.play(params) |
Play a round/step; auto token, session, game_id, session rotation, balance
update. Returns a promise.
|
RgsSdk.ack() |
Acknowledge the shown result (fire-and-forget) |
RgsSdk.refreshBalance() |
Re-fetch balance (get_accountsum) |
RgsSdk.onBalanceChange(cb) |
Called with the new balance whenever it changes — bind your HUD once |
RgsSdk.getBalance() / getSession() / getGame() |
Current state accessors |
RgsSdk.isReplay() |
true in replay mode |
RgsSdk.request(params) |
Low-level RGS call (adds token, normalizes errors) |
The game object passed to onReady: name,
balance, currency, minBet, maxBet,
betSteps, defaultBet, session. The replay object
passed to onReplay: input, result, steps,
winamount (cents).
Hard rules
These keep the RGS certifiable — treat them as non-negotiable requirements:
-
All randomness through
$context->rng. Neverrand(),mt_rand(),shuffle(), orarray_rand()in game code. -
Games never touch money. No wallet calls, no balance math
beyond computing
winAmountfrombetAmount. -
Stateless game classes. Round state only via
awaitingAction($state)→$context->state. -
Validate every client input — the client is untrusted.
Reject with
GameException; it aborts before the player is charged. -
Record replayable data
(
input/outcome/step) for every round. - The frontend never decides outcomes. It renders what the server returns; the displayed balance always comes from the RGS response.
-
Game code is a string stored in the
games.game_idcolumn (single source of truth). The game client must exist atresources/rgs-games/{code}/. - The stated RTP must match the implemented math model. New games must never use the legacy eval engine.
Quick reference
php artisan make:game <code> "<Name>" # scaffold a new game
./vendor/bin/pest # run all tests
WALLET_ADAPTER=local # .env — then open:
# /games/<code>/index.html?token=dev&game_id=<code>
php artisan mcp:start game-sdk # MCP server for AI agents
Blackjack Frontend Development
Complete guide for building a new blackjack frontend that integrates with the RGS.
🎮 Try the demo: Blackjack Pro — Full PixiJS implementation with login, game menu, and complete gameplay. 📥 Download game-pro.js
Game Variant: American Blackjack
The RGS implements American Blackjack with the following rules:
| Rule | Implementation |
|---|---|
| Decks | 1 deck (configurable) |
| Dealer hole card | Yes — dealer gets 2 cards upfront (one hidden) |
| Insurance | Available when dealer shows Ace |
| Peek for blackjack | Yes — insurance resolves immediately if dealer has BJ |
| Double down | Allowed on 9, 10, 11 only |
| Split | Allowed on pairs (same card type) |
| Dealer stands on | 17 (hard and soft) |
| Blackjack payout | 3:2 (1.5× bet) |
| Push | Bet returned |
What makes it American (vs European)
- Hole card dealt immediately — Dealer receives both cards at deal time
- Early insurance resolution — If dealer has blackjack after insurance, it pays immediately
- No "lose all" on dealer BJ — In European, splits/doubles lose everything if dealer has BJ; here insurance protects
Not implemented
Surrender, re-splitting, double after split, side bets (21+3, Perfect Pairs, etc.)
Blackjack API Reference
1. Initialize session — load_game
POST /api/rgs
rquest=load_game&token={token}&game_id=bj
Response includes game_session, accountsum, min_bet,
max_bet, bet_steps.
2. Deal (new round) — play with action=bet
POST /api/rgs
rquest=play&token={token}&game_session={session}&game_id=bj&bet_amount=10&continue=0&action=bet
3. Player actions — play with continue=1
POST /api/rgs
rquest=play&token={token}&game_session={session}&game_id=bj&continue=1&action={action}
Available actions
| Action | Description | When available |
|---|---|---|
bet |
Start new round with bet | Beginning of round (continue=0) |
hit |
Draw another card | Normal play |
stand |
Keep current hand | Normal play |
double |
Double bet, take one card, stand | Player score 9-11, first decision |
split |
Split pair into two hands | Two cards of same value |
insurance |
Side bet (half initial bet) | Dealer shows Ace |
hit_hand1 / stand_hand1 |
Actions for first split hand | After split |
hit_hand2 / stand_hand2 |
Actions for second split hand | After split |
Response fields
| Field | Type | Description |
|---|---|---|
status_id |
int | 0 = round complete, 3 = waiting for action |
actions |
string | Comma-separated available actions |
dealercards |
string | Comma-separated card indices (0-51) |
playercards |
string | Player cards (normal play) |
player_hand1 / player_hand2 |
string | Split hand cards |
dealer_value |
int | Dealer hand value |
player_value |
int | Player hand value |
result |
string | Outcome text (e.g., player wins, dealer busted) |
winamount |
float | Win amount (decimal) |
accountsum |
float | Updated balance |
new_game_session |
string | New session for next round (when complete) |
Card mapping (0-51)
Cards are integers 0-51 representing a standard deck. The API returns card IDs in
dealercards and playercards fields.
| ♥ Hearts | ♦ Diamonds | ♣ Clubs | ♠ Spades | |||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| ID | Card | Val | ID | Card | Val | ID | Card | Val | ID | Card | Val | |||
| 0 | 2♥ | 2 | 13 | 2♦ | 2 | 26 | 2♣ | 2 | 39 | 2♠ | 2 | |||
| 1 | 3♥ | 3 | 14 | 3♦ | 3 | 27 | 3♣ | 3 | 40 | 3♠ | 3 | |||
| 2 | 4♥ | 4 | 15 | 4♦ | 4 | 28 | 4♣ | 4 | 41 | 4♠ | 4 | |||
| 3 | 5♥ | 5 | 16 | 5♦ | 5 | 29 | 5♣ | 5 | 42 | 5♠ | 5 | |||
| 4 | 6♥ | 6 | 17 | 6♦ | 6 | 30 | 6♣ | 6 | 43 | 6♠ | 6 | |||
| 5 | 7♥ | 7 | 18 | 7♦ | 7 | 31 | 7♣ | 7 | 44 | 7♠ | 7 | |||
| 6 | 8♥ | 8 | 19 | 8♦ | 8 | 32 | 8♣ | 8 | 45 | 8♠ | 8 | |||
| 7 | 9♥ | 9 | 20 | 9♦ | 9 | 33 | 9♣ | 9 | 46 | 9♠ | 9 | |||
| 8 | 10♥ | 10 | 21 | 10♦ | 10 | 34 | 10♣ | 10 | 47 | 10♠ | 10 | |||
| 9 | J♥ | 10 | 22 | J♦ | 10 | 35 | J♣ | 10 | 48 | J♠ | 10 | |||
| 10 | Q♥ | 10 | 23 | Q♦ | 10 | 36 | Q♣ | 10 | 49 | Q♠ | 10 | |||
| 11 | K♥ | 10 | 24 | K♦ | 10 | 37 | K♣ | 10 | 50 | K♠ | 10 | |||
| 12 | A♥ | 1/11 | 25 | A♦ | 1/11 | 38 | A♣ | 1/11 | 51 | A♠ | 1/11 | |||
// JavaScript card decoding
function decodeCard(id) {
var suits = ['♥', '♦', '♣', '♠'];
var ranks = ['2','3','4','5','6','7','8','9','10','J','Q','K','A'];
return ranks[id % 13] + suits[Math.floor(id / 13)];
}
// decodeCard(10) → "Q♥" (Queen of Hearts)
// decodeCard(51) → "A♠" (Ace of Spades)
Result strings
| Result | Meaning |
|---|---|
ongoing |
Round in progress |
player wins |
Player wins |
player wins with blackjack |
Natural 21 (1.5× payout) |
dealer wins |
Dealer wins |
dealer busted |
Dealer over 21 |
player busted |
Player over 21 |
push |
Tie |
Integration Guide
Architecture
Step 1: Player login (Platform API)
POST /api/portal/players/login
{
"email": "player@example.com",
"password": "password123",
"portal_id": 1,
"player_ip": "127.0.0.1"
}
// Response includes wallet_session_token
Step 2: Build game launch URL
https://rgs.cloudvlt.com/games/bj/play.html?token={wallet_session_token}&game_id=bj&rgs_url=https%3A%2F%2Frgs.cloudvlt.com%2Fapi%2Frgs&device_id=web
Step 3: Frontend implementation
// API helper
function api(params) {
var body = Object.assign({}, params, { token: window.RGS_TOKEN });
return fetch(window.RGS_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: Object.keys(body).map(function(k) {
return encodeURIComponent(k) + '=' + encodeURIComponent(body[k]);
}).join('&')
}).then(function(r) { return r.json(); });
}
// Load game
api({ rquest: 'load_game', game_id: 'bj' }).then(function(res) {
state.gameSession = res.game_session;
state.balance = parseFloat(res.accountsum);
});
// Deal
api({
rquest: 'play',
game_session: state.gameSession,
game_id: 'bj',
bet_amount: 10,
continue: 0,
action: 'bet'
}).then(handleResponse);
// Player action
api({
rquest: 'play',
game_session: state.gameSession,
game_id: 'bj',
continue: 1,
action: 'hit' // or 'stand', 'double', 'split', 'insurance'
}).then(handleResponse);
Step 4: Handle response
function handleResponse(res) {
// Update balance
state.balance = parseFloat(res.accountsum);
// Parse cards
state.dealerCards = res.dealercards.split(',').map(Number);
state.playerCards = res.playercards ? res.playercards.split(',').map(Number) : [];
// Check for split
if (res.player_hand1) {
state.split = true;
state.hand1 = res.player_hand1.split(',').map(Number);
state.hand2 = res.player_hand2.split(',').map(Number);
}
// Available actions
state.actions = (res.actions || '').split(',').filter(Boolean);
// Round complete?
if (parseInt(res.status_id) === 0) {
showOutcome(res.result, parseFloat(res.winamount));
state.gameSession = res.new_game_session; // For next round
} else {
showActionButtons(state.actions);
}
renderTable();
}
Interactive API Testers
Test the Blackjack API directly from this page:
Testing with cURL
# Load game
curl -X POST https://rgs.cloudvlt.com/api/rgs \
-d "rquest=load_game&token=1|abc123...&game_id=bj"
# Deal ($5 bet)
curl -X POST https://rgs.cloudvlt.com/api/rgs \
-d "rquest=play&token=1|abc123...&game_session=SESSION_ID&game_id=bj&bet_amount=5&continue=0&action=bet"
# Hit
curl -X POST https://rgs.cloudvlt.com/api/rgs \
-d "rquest=play&token=1|abc123...&game_session=SESSION_ID&game_id=bj&continue=1&action=hit"
# Stand
curl -X POST https://rgs.cloudvlt.com/api/rgs \
-d "rquest=play&token=1|abc123...&game_session=SESSION_ID&game_id=bj&continue=1&action=stand"
Full tutorial with platform setup, player creation, and deployment:
docs/BLACKJACK-FRONTEND-TUTORIAL.md
Multiplayer Blackjack
Real-time multiplayer blackjack with synchronized rounds. Up to 7 players can sit at a table and play simultaneously against the dealer. Uses WebSocket for instant updates.
Live Demo: Blackjack Pro — Login and select "MULTIPLAYER" from the game menu. Open in multiple browser tabs to simulate multiple players at the same table.
Game Flow
The following diagram shows the complete flow from joining a table to completing a round:
Tables API
List available tables
GET /api/multiplayer/blackjack/tables
Response:
{
"success": true,
"tables": [
{
"table_id": "table-1",
"name": "VIP Table 1",
"type": "vip",
"min_bet": 1.00,
"max_bet": 500.00,
"seat_count": 7,
"occupied_seats": 2,
"available_seats": 4,
"status": "waiting",
"current_phase": "betting"
}
]
}
Create a new table
POST /api/multiplayer/blackjack/tables
Content-Type: application/json
{
"name": "My Table", // optional, auto-generated if not provided
"type": "standard", // optional: standard, vip, turbo (default: standard)
"min_bet": 1.00, // optional, in currency units (default: 1.00)
"max_bet": 500.00, // optional, in currency units (default: 500.00)
"seat_count": 7 // optional: 1-7 (default: 7)
}
Response (201 Created):
{
"success": true,
"table": {
"table_id": "table-a1b2c3d4",
"name": "My Table",
"type": "standard",
"min_bet": 1.00,
"max_bet": 500.00,
"seat_count": 7,
"status": "waiting",
"current_phase": "betting"
}
}
Creates a new blackjack table with the specified configuration. The table is immediately available for players to join. All parameters are optional - sensible defaults are used.
Get table state
GET /api/multiplayer/blackjack/tables/{table_id}?player_id={player_id}
Response:
{
"success": true,
"table": {
"table_id": "table-1",
"name": "VIP Table 1",
"current_phase": "player_turn",
"current_seat": 2,
"dealer_cards": [8],
"dealer_value": null,
"seats": [
{
"seat_number": 1,
"player_id": "player_alice",
"player_name": "Alice",
"bet": 10.00,
"cards": [15, 22],
"hand_value": 17,
"status": "standing",
"is_current": false
},
{
"seat_number": 2,
"player_id": "player_bob",
"player_name": "Bob",
"bet": 25.00,
"cards": [0, 9],
"hand_value": 12,
"status": "playing",
"is_current": true,
"is_me": true,
"available_actions": ["hit", "stand", "double"]
}
]
}
}
Join a table
POST /api/multiplayer/blackjack/tables/{table_id}/join
Content-Type: application/json
{
"seat_number": 3,
"player_id": "player_charlie",
"player_name": "Charlie",
"token": "wallet-session-token"
}
Leave a table
POST /api/multiplayer/blackjack/tables/{table_id}/leave
Content-Type: application/json
{
"seat_number": 3,
"player_id": "player_charlie"
}
Place a bet
POST /api/multiplayer/blackjack/tables/{table_id}/bet
Content-Type: application/json
{
"seat_number": 3,
"bet_amount": 25.00,
"player_id": "player_charlie"
}
Player action
POST /api/multiplayer/blackjack/tables/{table_id}/action
Content-Type: application/json
{
"seat_number": 3,
"action": "hit",
"player_id": "player_charlie"
}
// Valid actions: hit, stand, double, split, insurance
Testing with cURL
# List tables
curl https://rgs.cloudvlt.com/api/multiplayer/blackjack/tables
# Create table
curl -X POST https://rgs.cloudvlt.com/api/multiplayer/blackjack/tables \
-H "Content-Type: application/json" \
-d '{"name":"Test Table","type":"standard","min_bet":5,"max_bet":500,"seat_count":6}'
# Join table
curl -X POST https://rgs.cloudvlt.com/api/multiplayer/blackjack/tables/{tableId}/join \
-H "Content-Type: application/json" \
-d '{"seat_number":1,"player_id":"test123","player_name":"Tester"}'
# Place bet
curl -X POST https://rgs.cloudvlt.com/api/multiplayer/blackjack/tables/{tableId}/bet \
-H "Content-Type: application/json" \
-d '{"seat_number":1,"bet_amount":25,"player_id":"test123"}'
# Perform action (hit/stand/double/split)
curl -X POST https://rgs.cloudvlt.com/api/multiplayer/blackjack/tables/{tableId}/action \
-H "Content-Type: application/json" \
-d '{"seat_number":1,"action":"hit","player_id":"test123"}'
Launch Game URL
https://rgs.cloudvlt.com/games/mbj/play.html?token={wallet_session_token}&game_id=mbj
Game Flow
| Phase | Description | Actions |
|---|---|---|
betting |
Players join table and place bets | join, leave, bet |
player_turn |
Each player acts in seat order (1→6) | hit, stand, double, split, insurance |
settling |
Dealer draws, hands are compared | — |
completed |
Results displayed, ready for new round | new-round |
Synchronized Round Flow
- Players join during
bettingphase and place bets - Round starts: dealer deals 2 cards to each player with a bet, plus 2 to dealer (1 face down)
- Players act in seat order (1→6). Only the current seat can take actions
- After all players finish, dealer reveals hole card and draws to 17+
- Hands are settled, winnings credited
- Table resets to
bettingfor next round
Real-time Updates (WebSocket)
The multiplayer blackjack uses Laravel Echo with Pusher/Reverb for real-time updates. All players at a table receive instant updates when any action occurs.
Channel
blackjack-table.{table_id}
Event: table.updated
// Broadcast payload
{
"table": { /* full table state */ },
"timestamp": "2026-09-18T14:30:00+00:00"
}
JavaScript Client Example
import Echo from 'laravel-echo';
import Pusher from 'pusher-js';
window.Pusher = Pusher;
window.Echo = new Echo({
broadcaster: 'pusher',
key: 'your-pusher-key',
cluster: 'mt1',
forceTLS: true
});
// Subscribe to table updates
Echo.channel(`blackjack-table.${tableId}`)
.listen('.table.updated', (e) => {
console.log('Table updated:', e.table);
updateUI(e.table);
});
6-Deck Shoe
Multiplayer tables use a 6-deck shoe (312 cards) that is automatically reshuffled when fewer than 20 cards remain. Card IDs use the same 0-51 mapping as single-player blackjack, repeated 6 times in the shoe.