Extension Guide

How to extend the poker engine with new game variants, payment providers, admin panel pages, API endpoints, and WebSocket events.

Adding a New Game Variant

The engine uses the HandEvaluatorFactory pattern to dispatch hand evaluation to variant-specific implementations. Adding a new variant requires implementing the IHandEvaluator interface and registering it in the factory.

Step 1: Implement IHandEvaluator

Create your evaluator in src/game/hand-evaluator/evaluator-factory.ts, alongside the existing ones, or in a new file imported there:

// src/game/hand-evaluator/my-variant-evaluator.ts
import { Card } from '../engine/card.js';
import type { HandResult } from './hand-rank.js';
import type { IHandEvaluator } from './evaluator-factory.js';
import { evaluate7CardHand } from './evaluator.js';

export class MyVariantEvaluator implements IHandEvaluator {
  evaluate(holeCards: Card[], communityCards?: Card[]): HandResult {
    // Validate card counts for your variant
    if (holeCards.length !== 2) {
      throw new Error(`MyVariant requires exactly 2 hole cards, got ${holeCards.length}`);
    }

    // Implement your hand evaluation logic here
    // Use evaluate5CardHand() or evaluate7CardHand() from './evaluator.js' as building blocks
    const allCards = [...holeCards, ...(communityCards ?? [])];
    return evaluate7CardHand(allCards);
  }
}

Existing implementations to reference:

  • HoldEmEvaluator — standard 7-card evaluation
  • OmahaEvaluator — 2+3 rule with combination enumeration (4 or 5 hole cards)
  • ShortDeckEvaluator — modified hand rankings (flush beats full house)
  • StudEvaluator — 7 hole cards, no community cards

Step 2: Add to the Prisma GameType Enum

Edit prisma/schema.prisma:

enum GameType {
  TEXAS_HOLDEM
  OMAHA
  OMAHA_5
  OMAHA_HI_LO
  SHORT_DECK
  STUD_7_CARD
  STUD_HI_LO
  MIXED_GAME
  MY_VARIANT  // Add your variant here
}

Then run:

npx prisma migrate dev --name add_my_variant

Step 3: Register in the Factory

Open src/game/hand-evaluator/evaluator-factory.ts and add your case:

// src/game/hand-evaluator/evaluator-factory.ts
import { MyVariantEvaluator } from './my-variant-evaluator.js';

export class HandEvaluatorFactory {
  static create(gameType: GameType): IHandEvaluator {
    switch (gameType) {
      case GameType.TEXAS_HOLDEM:
        return new HoldEmEvaluator();

      // ... existing cases ...

      case GameType.MY_VARIANT:
        return new MyVariantEvaluator();

      default:
        throw new Error(`Unknown game type: ${gameType}. No evaluator registered for this variant.`);
    }
  }
}

Step 4: Handle Hi-Lo Variants

If your variant has a hi-lo showdown (like Omaha Hi-Lo or Stud Hi-Lo), the table routes to runHiLoShowdown() instead of runShowdown(). Add your variant to the isHiLo condition in src/game/table/table-actions.ts, and handle it in runHiLoShowdown() in src/game/flow/showdown.ts.

Step 5: Configure the Game Engine

Add variant-specific configuration where the engine switches on game type:

ConcernFile
Dealing patterncreateDealingPattern() in src/game/engine/dealing-engine.ts
Betting rounds and state progressiongetVariantStateConfig() in src/game/flow/variant-state-config.ts
Default betting limitgetVariantBettingLimit() in src/game/betting/betting-limit-utils.ts
Bring-in gamesusesBringIn() in src/game/betting/bring-in.ts
Mixed-game rotation and labelssrc/game/flow/mixed-game-engine.ts
Hand start flowsrc/game/table/table.ts
Bot strategysrc/game/bot/decision-engine.ts

Step 6: Write Tests

Add tests in src/game/hand-evaluator/evaluator-factory.test.ts:

it('creates MyVariantEvaluator for MY_VARIANT', () => {
  const evaluator = HandEvaluatorFactory.create(GameType.MY_VARIANT);
  expect(evaluator).toBeInstanceOf(MyVariantEvaluator);
});

it('evaluates MY_VARIANT hands correctly', () => {
  const evaluator = new MyVariantEvaluator();
  const result = evaluator.evaluate([/* hole cards */], [/* community cards */]);
  expect(result.rank).toBe(HandRank.TWO_PAIR);
});

Adding a New Payment Provider

Payment providers are classes that implement IPaymentProvider from src/services/payment-gateway.service.ts. There are two ways to add one, and both are covered in full in the Payment Providers guide.

As a plugin (no core changes). Register the class from your plugin’s register() function:

export function register(registry) {
  registry.registerProvider('my_provider', MyProvider);
  registry.registerProviderMeta('my_provider', {
    displayName: 'My Provider',
    currencies: ['USD'],
    defaultActiveDeposit: false,
    defaultActiveWithdraw: false,
  });
}

The engine creates the payment system row on activation. Credentials can come from the plugin’s configFields or the payment system’s config. See the Plugin Development Guide.

In core. Create the provider in src/services/providers/my-provider.provider.ts, end the file with registerProvider('my_provider', MyProvider), and add a side-effect import alongside the others in src/app.ts:

// src/app.ts
import './services/providers/my-provider.provider.js';

Inbound webhooks arrive at POST /api/v1/webhooks/payment/:paymentSystemId. Add your provider’s signature handling to src/services/payment-webhook.service.ts.

Provider credentials are stored AES-256-GCM encrypted in the payment system’s config by the gateway. Administrators manage them through the admin panel, so a provider needs no settings service of its own.

Adding a New Admin Panel Page

Step 1: Create the Page Component

// client/src/pages/admin/my-feature/MyFeaturePage.tsx
import { useState, useEffect } from 'react';
import { apiClient } from '../../../api/client';

export function MyFeaturePage() {
  const [data, setData] = useState([]);

  useEffect(() => {
    apiClient.get('/admin/my-feature').then(setData);
  }, []);

  return (
    <div>
      <h1>My Feature</h1>
      {/* Page content */}
    </div>
  );
}

Step 2: Add the Route

// client/src/routes/index.tsx
const MyFeaturePage = lazy(() => import('@/pages/admin/my-feature/MyFeaturePage').then(m => ({ default: m.MyFeaturePage })));

// Inside the children of the '/admin' route:
{ path: 'my-feature', element: <Lazy><MyFeaturePage /></Lazy> },

Add an item to a group in the navGroups array in client/src/layouts/AdminLayout.tsx. The layout hides it from users without the required feature:

{ to: '/admin/my-feature', labelKey: 'nav.admin.myFeature', defaultLabel: 'My Feature', requiredFeature: 'my-feature:read' },

Step 4: Define the Feature Permission

Add your feature to ADMIN_FEATURES in src/api/middleware/auth.ts:

export const ADMIN_FEATURES = {
  // ... existing features ...
  'my-feature:read': 'b',   // Moderator and above
  'my-feature:edit': 'a1',  // Admin and above
} as const;

Adding a New API Endpoint

Step 1: Define the Schema

// src/models/schemas.ts
import { z } from 'zod';

export const myResourceSchema = z.object({
  name: z.string().min(1).max(100),
  value: z.number().positive(),
});

export type MyResourceInput = z.infer<typeof myResourceSchema>;

Step 2: Create the Service

// src/services/my-resource.service.ts
import { prisma } from '../config/database.js';
import { NotFoundError } from '../models/errors.js';

export async function createMyResource(input: MyResourceInput, userId: string) {
  return prisma.myResource.create({
    data: { ...input, userId },
  });
}

export async function getMyResource(id: string) {
  const resource = await prisma.myResource.findUnique({ where: { id } });
  if (!resource) throw new NotFoundError('MyResource');
  return resource;
}

Step 3: Create the Route Handler

// src/api/routes/my-resource.routes.ts
import { Router } from 'express';
import { authenticate } from '../middleware/auth.js';
import { validateBody, validateParams } from '../middleware/validation.js';
import { myResourceSchema } from '../../models/schemas.js';
import { z } from 'zod';
import * as myResourceService from '../../services/my-resource.service.js';

const router = Router();

router.post('/',
  authenticate,
  validateBody(myResourceSchema),
  async (req, res, next) => {
    try {
      const resource = await myResourceService.createMyResource(req.body, req.user!.sub);
      res.status(201).json({ success: true, data: resource });
    } catch (error) {
      next(error);
    }
  }
);

router.get('/:id',
  authenticate,
  validateParams(z.object({ id: z.string().uuid() })),
  async (req, res, next) => {
    try {
      const resource = await myResourceService.getMyResource(req.params.id);
      res.json({ success: true, data: resource });
    } catch (error) {
      next(error);
    }
  }
);

export default router;

Step 4: Register the Routes

Mount the router in src/app.ts with the other API routes, and add the export to src/api/routes/index.ts if you import it from there:

// src/app.ts
import myResourceRoutes from './api/routes/my-resource.routes.js';

app.use('/api/v1/my-resources', myResourceRoutes);

Step 5: Error Handling

All route errors should be passed to next(error). The global error handler in src/api/middleware/error-handler.ts converts known error types to appropriate HTTP status codes:

Error ClassHTTP Status
ValidationError400
AuthenticationError401
AuthorizationError403
FeatureTierError403
NotFoundError404
ConflictError409
RateLimitError429

Adding a New WebSocket Event

WebSocket events use Socket.io with typed event maps. Server types live in src/websocket/types/events.ts, which is the single source of truth for the server. The client-facing copy is packages/shared/src/types/events.ts, so add new events to both.

Step 1: Add Event Types

// src/websocket/types/events.ts and packages/shared/src/types/events.ts

// Server → Client events
export interface ServerToClientEvents {
  // ... existing events ...
  my_event: (data: MyEventData) => void;
}

// Client → Server events
export interface ClientToServerEvents {
  // ... existing events ...
  my_action: (data: MyActionData, callback?: (response: MyActionResponse) => void) => void;
}

// Define the data types
export interface MyEventData {
  tableId: string;
  payload: string;
}

export interface MyActionData {
  tableId: string;
  value: number;
}

export interface MyActionResponse {
  success: boolean;
  error?: string;
}

Step 2: Emit the Event from Server

// In a game engine handler or service:
import { broadcastToTable, sendToUser } from '../websocket/broadcast.js';

// Emit to everyone at a table (players and spectators)
await broadcastToTable(tableId, 'my_event', { tableId, payload: 'data' });

// Emit to a specific user
await sendToUser(userId, 'my_event', { tableId, payload: 'data' });

Step 3: Handle the Event on Server

Client events are registered in src/websocket/connection-handler.ts, in the block marked EVENT HANDLERS (authenticated users only). Add a Zod schema for the payload in src/websocket/schemas/client-events.ts, and treat the callback as optional:

// src/websocket/connection-handler.ts
socket.on('my_action', async (data, callback) => {
  try {
    await processMyAction(socket.data.userId, data);
    safeAck(callback, { success: true });
  } catch (error) {
    safeAck(callback, { success: false, error: 'Action failed' });
  }
});

Step 4: Listen on the Client

// client/src/lib/socket.ts or a store
socket.on('my_event', (data) => {
  console.log('Received my_event:', data);
  // Update store state
});

// Emit an action
socket.emit('my_action', { tableId: 'xyz', value: 100 }, (response) => {
  if (!response.success) console.error(response.error);
});

Extending the Database Schema

Adding a New Table

// prisma/schema.prisma
model MyResource {
  id        String   @id @default(cuid())
  name      String
  value     Int
  userId    String
  user      User     @relation(fields: [userId], references: [id])
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt

  @@map("my_resources")
}

Then:

# Create a migration
npx prisma migrate dev --name add_my_resource_table

# Generate updated Prisma client
npx prisma generate

Adding a Column to an Existing Table

model User {
  // ... existing fields ...
  myNewField String? @default("default_value")
}
npx prisma migrate dev --name add_my_new_field_to_user

Important: Never use prisma migrate reset or prisma db push --force-reset against the development database — it wipes all data. Use prisma migrate dev for additive changes.

Try the live demo Pricing

Have questions about Poker Engine? We reply fast.