41 pages Β· 8 sections
Ctrl K
GitHub Portfolio

Project: PokerLab

PokerLab is a web application for poker training and analysis, featuring hand history review, equity calculations, and opponent profiling.

Project Overview

PokerLab bridges the gap between recreational and professional poker play by providing data-driven analysis tools. The application processes raw hand histories from major online poker platforms, calculates real-time equity via Monte Carlo simulation, tracks bankroll performance, and builds opponent behavioral profiles β€” all wrapped in a modern React interface backed by a Node.js API and PostgreSQL database.

Project Scope: PokerLab was built to demonstrate production-grade application architecture with Terraform-managed AWS infrastructure, containerized microservices, and a complete CI/CD pipeline. The poker domain provides rich data processing, mathematical simulation, and real-time analytics requirements.

Key Features

FeatureDescriptionTechnology
Hand History ImporterParse and store hand histories from PokerStars, partypoker, and 888pokerNode.js streams, custom parsers
Equity CalculatorMonte Carlo simulation for hand vs hand / range vs range equityWeb Workers, statistical sampling
Bankroll TrackerSession tracking, profit/loss analytics, variance visualizationChart.js, PostgreSQL aggregates
Session AnalyticsPerformance dashboards with filtering by date, stakes, game typeReact, D3.js, REST API
Opponent ProfilingAggregated stats: VPIP, PFR, aggression factor, positional tendenciesMaterialized views, Redis cache

Architecture

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                              AWS CloudFront CDN                              β”‚
β”‚                     (Static assets, SSL termination)                         β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                              β”‚
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                         Application Load Balancer                            β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
           β”‚                             β”‚                          β”‚
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Frontend Service   β”‚     β”‚   API Service          β”‚  β”‚  Worker Service    β”‚
β”‚  (ECS Fargate)      β”‚     β”‚   (ECS Fargate)        β”‚  β”‚  (ECS Fargate)     β”‚
β”‚                     β”‚     β”‚                        β”‚  β”‚                    β”‚
β”‚  React SPA          │────▢│  Node.js / Express     β”‚  β”‚  Hand processors   β”‚
β”‚  Nginx static       β”‚     β”‚  JWT auth              β”‚  β”‚  Equity sim        β”‚
β”‚  serve              β”‚     β”‚  Business logic        β”‚  β”‚  Report generation β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜     β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                        β”‚
                           β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                           β”‚                         β”‚
                  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                  β”‚  RDS PostgreSQL   β”‚   β”‚  ElastiCache Redis β”‚
                  β”‚  (Multi-AZ)       β”‚   β”‚  (Session/cache)   β”‚
                  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Technology Stack

LayerTechnologyVersion
FrontendReact 18, TypeScript, ViteReact ^18.2
UI ComponentsMaterial-UI (MUI)^5.14
State ManagementZustand^4.4
Backend APINode.js, Express, TypeScriptNode 20 LTS
AuthenticationJWT (access + refresh tokens)jsonwebtoken ^9
DatabasePostgreSQL 1515.x
CacheRedis7.x
InfrastructureTerraform, AWSTerraform ~> 1.6
ContainersDocker, ECS FargateDocker 24.x
CI/CDGitHub Actionsβ€”

Terraform Infrastructure

The entire AWS infrastructure is defined as code in the tf-pokerlab repository. This enables reproducible environments, peer-reviewed infrastructure changes, and disaster recovery.

VPC with Public/Private Subnets

# modules/vpc/main.tf
# Production-grade VPC with public and private subnets across 3 AZs

resource "aws_vpc" "main" {
  cidr_block           = var.vpc_cidr
  enable_dns_hostnames = true
  enable_dns_support   = true

  tags = {
    Name        = "${var.project_name}-${var.environment}"
    Environment = var.environment
    ManagedBy   = "terraform"
  }
}

# Internet Gateway for public subnet access
resource "aws_internet_gateway" "main" {
  vpc_id = aws_vpc.main.id
  tags = {
    Name = "${var.project_name}-igw-${var.environment}"
  }
}

# Public subnets β€” one per AZ for ALB and NAT Gateways
resource "aws_subnet" "public" {
  count                   = length(var.availability_zones)
  vpc_id                  = aws_vpc.main.id
  cidr_block              = cidrsubnet(var.vpc_cidr, 4, count.index)
  availability_zone       = var.availability_zones[count.index]
  map_public_ip_on_launch = true

  tags = {
    Name = "${var.project_name}-public-${count.index + 1}"
    Type = "public"
  }
}

# Private subnets β€” one per AZ for ECS tasks and RDS
resource "aws_subnet" "private" {
  count             = length(var.availability_zones)
  vpc_id            = aws_vpc.main.id
  cidr_block        = cidrsubnet(var.vpc_cidr, 4, count.index + length(var.availability_zones))
  availability_zone = var.availability_zones[count.index]

  tags = {
    Name = "${var.project_name}-private-${count.index + 1}"
    Type = "private"
  }
}

# NAT Gateway per AZ for outbound internet from private subnets
resource "aws_nat_gateway" "main" {
  count         = length(var.availability_zones)
  allocation_id = aws_eip.nat[count.index].id
  subnet_id     = aws_subnet.public[count.index].id
  tags = {
    Name = "${var.project_name}-nat-${count.index + 1}"
  }
  depends_on = [aws_internet_gateway.main]
}

resource "aws_eip" "nat" {
  count  = length(var.availability_zones)
  domain = "vpc"
  tags = {
    Name = "${var.project_name}-eip-${count.index + 1}"
  }
}

ECS Fargate for Containerized Services

# modules/ecs/main.tf
# ECS Cluster with Fargate launch type for serverless containers

resource "aws_ecs_cluster" "main" {
  name = "${var.project_name}-${var.environment}"

  setting {
    name  = "containerInsights"
    value = "enabled"
  }

  tags = {
    Environment = var.environment
  }
}

# Frontend Task Definition
resource "aws_ecs_task_definition" "frontend" {
  family                   = "${var.project_name}-frontend-${var.environment}"
  network_mode             = "awsvpc"
  requires_compatibilities = ["FARGATE"]
  cpu                      = "256"
  memory                   = "512"
  execution_role_arn       = var.ecs_execution_role_arn

  container_definitions = jsonencode([{
    name  = "frontend"
    image = "${var.ecr_repository_url}/pokerlab-frontend:${var.image_tag}"
    portMappings = [{
      containerPort = 80
      protocol      = "tcp"
    }]
    logConfiguration = {
      logDriver = "awslogs"
      options = {
        awslogs-group         = aws_cloudwatch_log_group.frontend.name
        awslogs-region        = var.aws_region
        awslogs-stream-prefix = "ecs"
      }
    }
  }])
}

# API Task Definition (more resources for backend processing)
resource "aws_ecs_task_definition" "api" {
  family                   = "${var.project_name}-api-${var.environment}"
  network_mode             = "awsvpc"
  requires_compatibilities = ["FARGATE"]
  cpu                      = "512"
  memory                   = "1024"
  execution_role_arn       = var.ecs_execution_role_arn
  task_role_arn            = var.ecs_task_role_arn

  container_definitions = jsonencode([{
    name  = "api"
    image = "${var.ecr_repository_url}/pokerlab-api:${var.image_tag}"
    portMappings = [{
      containerPort = 3000
      protocol      = "tcp"
    }]
    environment = [
      { name = "NODE_ENV", value = var.environment },
      { name = "DB_HOST", value = var.rds_endpoint },
      { name = "REDIS_HOST", value = var.redis_endpoint },
      { name = "PORT", value = "3000" }
    ]
    secrets = [
      { name = "DB_PASSWORD", valueFrom = var.db_secret_arn },
      { name = "JWT_SECRET", valueFrom = var.jwt_secret_arn }
    ]
    logConfiguration = {
      logDriver = "awslogs"
      options = {
        awslogs-group         = aws_cloudwatch_log_group.api.name
        awslogs-region        = var.aws_region
        awslogs-stream-prefix = "ecs"
      }
    }
  }])
}

# ECS Service with auto-scaling
resource "aws_ecs_service" "api" {
  name            = "api"
  cluster         = aws_ecs_cluster.main.id
  task_definition = aws_ecs_task_definition.api.arn
  desired_count   = var.api_desired_count
  launch_type     = "FARGATE"

  network_configuration {
    subnets          = var.private_subnet_ids
    security_groups  = [var.api_security_group_id]
    assign_public_ip = false
  }

  load_balancer {
    target_group_arn = var.api_target_group_arn
    container_name   = "api"
    container_port   = 3000
  }

  depends_on = [var.alb_listener]
}

# Auto-scaling policies
resource "aws_appautoscaling_target" "api" {
  max_capacity       = 10
  min_capacity       = 2
  resource_id        = "service/${aws_ecs_cluster.main.name}/${aws_ecs_service.api.name}"
  scalable_dimension = "ecs:service:DesiredCount"
  service_namespace  = "ecs"
}

resource "aws_appautoscaling_policy" "api_cpu" {
  name               = "api-cpu-auto-scaling"
  policy_type        = "TargetTrackingScaling"
  resource_id        = aws_appautoscaling_target.api.resource_id
  scalable_dimension = aws_appautoscaling_target.api.scalable_dimension
  service_namespace  = aws_appautoscaling_target.api.service_namespace

  target_tracking_scaling_policy_configuration {
    predefined_metric_specification {
      predefined_metric_type = "ECSServiceAverageCPUUtilization"
    }
    target_value       = 70.0
    scale_in_cooldown  = 300
    scale_out_cooldown = 60
  }
}

RDS PostgreSQL

# modules/rds/main.tf
# Multi-AZ PostgreSQL with automated backups and encryption

resource "aws_db_subnet_group" "main" {
  name       = "${var.project_name}-${var.environment}"
  subnet_ids = var.private_subnet_ids

  tags = {
    Name = "${var.project_name}-db-subnet-group"
  }
}

resource "aws_db_instance" "main" {
  identifier = "${var.project_name}-${var.environment}"

  engine               = "postgres"
  engine_version       = "15.4"
  instance_class       = var.db_instance_class  # db.t3.medium for prod
  allocated_storage    = 100
  max_allocated_storage = 1000  # Autoscaling limit
  storage_type         = "gp3"
  storage_encrypted    = true

  db_name  = var.db_name
  username = var.db_username
  password = var.db_password
  port     = 5432

  multi_az               = var.environment == "production"
  publicly_accessible    = false
  vpc_security_group_ids = [var.db_security_group_id]
  db_subnet_group_name   = aws_db_subnet_group.main.name

  backup_retention_period = 30
  backup_window          = "03:00-04:00"
  maintenance_window     = "Mon:04:00-Mon:05:00"
  delete_automated_backups = false

  deletion_protection = var.environment == "production"
  skip_final_snapshot = var.environment != "production"
  final_snapshot_identifier = var.environment == "production" ? "${var.project_name}-final" : null

  enabled_cloudwatch_logs_exports = ["postgresql", "upgrade"]

  tags = {
    Name        = "${var.project_name}-postgres"
    Environment = var.environment
  }
}

CI/CD Pipeline with GitHub Actions

The pipeline implements a GitOps workflow with automated testing, security scanning, infrastructure validation, and blue/green deployment.

# .github/workflows/deploy.yml
name: Build, Test, and Deploy

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]

env:
  AWS_REGION: us-east-1
  ECR_REGISTRY: ${{ secrets.AWS_ACCOUNT_ID }}.dkr.ecr.us-east-1.amazonaws.com

jobs:
  # -------------------------------------------------------------------------
  # 1. Lint and Unit Tests
  # -------------------------------------------------------------------------
  test:
    runs-on: ubuntu-latest
    services:
      postgres:
        image: postgres:15
        env:
          POSTGRES_PASSWORD: testpass
          POSTGRES_DB: pokerlab_test
        options: >-
          --health-cmd pg_isready
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5
        ports:
          - 5432:5432
      redis:
        image: redis:7
        ports:
          - 6379:6379

    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'

      - name: Install dependencies
        run: npm ci

      - name: Lint
        run: npm run lint

      - name: Unit tests
        run: npm run test:ci
        env:
          NODE_ENV: test
          DB_HOST: localhost
          DB_NAME: pokerlab_test
          DB_PASSWORD: testpass
          REDIS_HOST: localhost

      - name: Upload coverage
        uses: codecov/codecov-action@v3
        with:
          files: ./coverage/lcov.info

  # -------------------------------------------------------------------------
  # 2. Security Scanning
  # -------------------------------------------------------------------------
  security:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Run Trivy vulnerability scanner
        uses: aquasecurity/trivy-action@master
        with:
          scan-type: 'fs'
          format: 'sarif'
          output: 'trivy-results.sarif'
      - name: Upload Trivy scan results
        uses: github/codeql-action/upload-sarif@v2
        with:
          sarif_file: 'trivy-results.sarif'

  # -------------------------------------------------------------------------
  # 3. Terraform Validation
  # -------------------------------------------------------------------------
  terraform:
    runs-on: ubuntu-latest
    defaults:
      run:
        working-directory: ./terraform
    steps:
      - uses: actions/checkout@v4
      - uses: hashicorp/setup-terraform@v3
        with:
          terraform_version: "1.6.0"
      - name: Terraform fmt
        run: terraform fmt -check -recursive
      - name: Terraform init
        run: terraform init -backend=false
      - name: Terraform validate
        run: terraform validate

  # -------------------------------------------------------------------------
  # 4. Build and Push Container Images
  # -------------------------------------------------------------------------
  build:
    runs-on: ubuntu-latest
    needs: [test, security, terraform]
    if: github.ref == 'refs/heads/main'
    steps:
      - uses: actions/checkout@v4
      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          aws-region: ${{ env.AWS_REGION }}
      - name: Login to Amazon ECR
        uses: aws-actions/amazon-ecr-login@v2

      - name: Build and push API image
        run: |
          docker build -t $ECR_REGISTRY/pokerlab-api:${{ github.sha }} ./api
          docker push $ECR_REGISTRY/pokerlab-api:${{ github.sha }}

      - name: Build and push Frontend image
        run: |
          docker build -t $ECR_REGISTRY/pokerlab-frontend:${{ github.sha }} ./frontend
          docker push $ECR_REGISTRY/pokerlab-frontend:${{ github.sha }}

  # -------------------------------------------------------------------------
  # 5. Deploy to ECS
  # -------------------------------------------------------------------------
  deploy:
    runs-on: ubuntu-latest
    needs: build
    environment:
      name: production
      url: https://pokerlab.kuyaops.com
    steps:
      - uses: actions/checkout@v4
      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          aws-region: ${{ env.AWS_REGION }}
      - name: Deploy to ECS
        run: |
          aws ecs update-service \
            --cluster pokerlab-production \
            --service api \
            --force-new-deployment

Local Development Environment

Complete docker-compose.yml

# docker-compose.yml
# Full local development stack with hot reload and persistent data volumes
version: "3.8"

services:
  # ---------------------------------------------------------------------------
  # PostgreSQL Database
  # ---------------------------------------------------------------------------
  postgres:
    image: postgres:15-alpine
    container_name: pokerlab-postgres
    restart: unless-stopped
    environment:
      POSTGRES_USER: pokerlab
      POSTGRES_PASSWORD: devpassword
      POSTGRES_DB: pokerlab_dev
    ports:
      - "5432:5432"
    volumes:
      - postgres_data:/var/lib/postgresql/data
      - ./database/init:/docker-entrypoint-initdb.d:ro
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U pokerlab -d pokerlab_dev"]
      interval: 10s
      timeout: 5s
      retries: 5

  # ---------------------------------------------------------------------------
  # Redis Cache
  # ---------------------------------------------------------------------------
  redis:
    image: redis:7-alpine
    container_name: pokerlab-redis
    restart: unless-stopped
    ports:
      - "6379:6379"
    volumes:
      - redis_data:/data
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 10s
      timeout: 3s
      retries: 5

  # ---------------------------------------------------------------------------
  # Backend API
  # ---------------------------------------------------------------------------
  api:
    build:
      context: ./api
      dockerfile: Dockerfile.dev
    container_name: pokerlab-api
    restart: unless-stopped
    environment:
      NODE_ENV: development
      PORT: 3000
      DB_HOST: postgres
      DB_PORT: 5432
      DB_NAME: pokerlab_dev
      DB_USER: pokerlab
      DB_PASSWORD: devpassword
      REDIS_HOST: redis
      REDIS_PORT: 6379
      JWT_SECRET: dev-jwt-secret-change-in-production
      JWT_EXPIRES_IN: 15m
      JWT_REFRESH_EXPIRES_IN: 7d
    ports:
      - "3000:3000"
    volumes:
      - ./api/src:/app/src:ro
      - ./api/package.json:/app/package.json:ro
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_healthy
    healthcheck:
      test: ["CMD", "wget", "--spider", "-q", "http://localhost:3000/health"]
      interval: 15s
      timeout: 5s
      retries: 3

  # ---------------------------------------------------------------------------
  # Frontend (React with Vite dev server)
  # ---------------------------------------------------------------------------
  frontend:
    build:
      context: ./frontend
      dockerfile: Dockerfile.dev
    container_name: pokerlab-frontend
    restart: unless-stopped
    environment:
      VITE_API_URL: http://localhost:3000
      VITE_WS_URL: ws://localhost:3000
    ports:
      - "5173:5173"
    volumes:
      - ./frontend/src:/app/src
      - ./frontend/public:/app/public
      - ./frontend/package.json:/app/package.json:ro
    depends_on:
      - api

  # ---------------------------------------------------------------------------
  # pgAdmin for database management (optional)
  # ---------------------------------------------------------------------------
  pgadmin:
    image: dpage/pgadmin4:latest
    container_name: pokerlab-pgadmin
    restart: unless-stopped
    environment:
      PGADMIN_DEFAULT_EMAIL: admin@pokerlab.local
      PGADMIN_DEFAULT_PASSWORD: admin
    ports:
      - "5050:80"
    volumes:
      - pgadmin_data:/var/lib/pgadmin
    depends_on:
      - postgres

volumes:
  postgres_data:
  redis_data:
  pgadmin_data:

Environment Setup Guide

  1. Clone both repositories
    # Application code
    git clone https://github.com/j1-medilo06/pokerlab-app.git
    cd pokerlab-app
    
    # Infrastructure (optional for local dev)
    git clone https://github.com/j1-medilo06/tf-pokerlab.git
  2. Start the development stack
    docker-compose up -d
    
    # Wait for services to be healthy
    docker-compose ps
    
    # View logs
    docker-compose logs -f api
  3. Run database migrations
    # API container runs migrations automatically on startup
    # Or manually:
    docker-compose exec api npx knex migrate:latest
  4. Seed sample data (optional)
    docker-compose exec api npx knex seed:run
  5. Access the application
    ServiceURLCredentials
    Frontendhttp://localhost:5173β€”
    APIhttp://localhost:3000β€”
    API Healthhttp://localhost:3000/healthβ€”
    pgAdminhttp://localhost:5050admin@pokerlab.local / admin
    PostgreSQLlocalhost:5432pokerlab / devpassword
    Redislocalhost:6379β€”

Key Components Detail

Hand History Importer

The importer supports multiple poker site formats through a pluggable parser architecture. Each parser implements a common interface, normalizing hands into a unified schema.

// api/src/parsers/BaseHandParser.ts
export interface ParsedHand {
  handId: string;
  site: 'pokerstars' | 'partypoker' | '888poker';
  gameType: 'cash' | 'tournament' | 'sng';
  format: 'holdem' | 'omaha' | 'omaha5';
  stakes: { smallBlind: number; bigBlind: number; currency: string };
  tableName: string;
  maxSeats: number;
  datetime: Date;
  players: ParsedPlayer[];
  actions: ParsedAction[];
  boardCards?: string[];
  summary: HandSummary;
}

export interface ParsedPlayer {
  seat: number;
  name: string;
  stack: number;
  cards?: string[];
  isHero: boolean;
}

export interface ParsedAction {
  playerName: string;
  street: 'preflop' | 'flop' | 'turn' | 'river' | 'showdown';
  action: 'fold' | 'check' | 'call' | 'bet' | 'raise' | 'allin';
  amount?: number;
  position: 'SB' | 'BB' | 'UTG' | 'MP' | 'CO' | 'BTN';
}

Equity Calculator

The equity calculator uses Monte Carlo simulation to estimate win probability. For two known hands with a random board, it runs 10,000–100,000 iterations, dealing random remaining cards and evaluating the winner using the pokersolver library.

// api/src/services/EquityCalculator.ts
import * as pokersolver from 'pokersolver';

export class EquityCalculator {
  private readonly HAND = pokersolver.Hand;

  /**
   * Calculate equity for given hand(s) against range(s) via Monte Carlo.
   * @param hands Array of specific hands (e.g., ['As', 'Kd'])
   * @param ranges Array of weighted range arrays
   * @param board Optional known board cards
   * @param iterations Number of Monte Carlo trials (default 50000)
   */
  async calculateEquity(
    hands: string[][],
    ranges: string[][][],
    board: string[] = [],
    iterations: number = 50000
  ): Promise<EquityResult> {
    const deck = this.createDeck().filter(
      c => !board.includes(c) && !hands.flat().includes(c)
    );
    
    let wins = new Array(hands.length).fill(0);
    let ties = new Array(hands.length).fill(0);

    for (let i = 0; i < iterations; i++) {
      const shuffled = this.shuffle([...deck]);
      let dealtHands = hands.map(h => [...h]);
      let dealtRanges = ranges.map(r => this.sampleFromRange(r, shuffled));
      
      const allHands = [...dealtHands, ...dealtRanges];
      const cardsNeeded = 5 - board.length;
      const runoutBoard = [...board, ...shuffled.splice(0, cardsNeeded)];
      
      // Evaluate all hands against the board
      const evaluated = allHands.map(h => this.HAND.solve([...h, ...runoutBoard]));
      const winners = this.HAND.winners(evaluated);
      
      if (winners.length > 1) {
        const tieShare = 1 / winners.length;
        winners.forEach(w => {
          const idx = evaluated.indexOf(w);
          if (idx < hands.length) ties[idx] += tieShare;
        });
      } else {
        const winnerIdx = evaluated.indexOf(winners[0]);
        if (winnerIdx < hands.length) wins[winnerIdx]++;
      }
    }

    return {
      equities: hands.map((_, i) => ({
        hand: hands[i],
        winPct: (wins[i] / iterations) * 100,
        tiePct: (ties[i] / iterations) * 100,
        equity: ((wins[i] + ties[i]) / iterations) * 100
      })),
      iterations,
      duration: Date.now() - startTime
    };
  }
}

GitHub Repositories

RepositoryDescriptionLink
pokerlab-appReact frontend + Node.js API + Docker Composegithub.com/j1-medilo06/pokerlab-app
tf-pokerlabTerraform modules for AWS infrastructuregithub.com/j1-medilo06/tf-pokerlab

Deployment Architecture

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                                  DEVELOPER                                   β”‚
β”‚                              (git push origin main)                          β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                               β”‚
                    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                    β”‚   GitHub Actions    β”‚
                    β”‚   CI/CD Pipeline    β”‚
                    β”‚                     β”‚
                    β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚
                    β”‚  β”‚ Lint & Test   β”‚  β”‚
                    β”‚  β”‚ Security Scan β”‚  β”‚
                    β”‚  β”‚ TF Validate   β”‚  β”‚
                    β”‚  β”‚ Build Images  β”‚  β”‚
                    β”‚  β”‚ Push to ECR   β”‚  β”‚
                    β”‚  β”‚ Deploy to ECS β”‚  β”‚
                    β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚
                    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                               β”‚
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                              AWS PRODUCTION                                  β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚
β”‚  β”‚ CloudFront  β”‚  β”‚ ALB          β”‚  β”‚ ECS Fargate β”‚  β”‚ RDS PostgreSQL   β”‚  β”‚
β”‚  β”‚ (CDN/SSL)   β”‚  β”‚ (Routing)    β”‚  β”‚ (API + Web) β”‚  β”‚ (Multi-AZ)       β”‚  β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚
β”‚                                                                            β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚
β”‚  β”‚ ElastiCache β”‚  β”‚ CloudWatch   β”‚  β”‚ S3 (Logs)   β”‚  β”‚ Secrets Manager β”‚  β”‚
β”‚  β”‚ (Redis)     β”‚  β”‚ (Metrics)    β”‚  β”‚ (Backups)   β”‚  β”‚ (Credentials)   β”‚  β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
Infrastructure Best Practices Implemented:
  • Multi-AZ deployment for high availability
  • Private subnets for compute and database β€” no public IPs on containers
  • Least-privilege IAM roles for each ECS task
  • Encrypted storage at rest (RDS, EBS) and in transit (TLS 1.3)
  • Container image scanning with Trivy in CI pipeline
  • Database deletion protection and automated snapshots in production
  • Auto-scaling based on CPU utilization (70% target)

References & Links

ResourceLink
Application Repositorygithub.com/j1-medilo06/pokerlab-app
Terraform Repositorygithub.com/j1-medilo06/tf-pokerlab
Author GitHubgithub.com/j1-medilo06
Portfoliokuyaops.com