From 9284914ae23fa9a2a4a210eee5d8232e51bccadd Mon Sep 17 00:00:00 2001
From: Martin Raymond <codephobia@hotmail.com>
Date: Sat, 9 Mar 2024 08:58:09 -0800
Subject: [PATCH] dynamic tables

---
 apps/api/.env.example                         |   1 +
 apps/api/core.go                              |  13 +-
 apps/dashboard/src/app/app.module.ts          |   8 +
 apps/dashboard/src/app/core/tables/index.ts   |   4 +
 .../src/app/core/tables/tables.actions.ts     |  11 +
 .../src/app/core/tables/tables.effects.ts     |  27 ++
 .../src/app/core/tables/tables.reducer.ts     |  24 ++
 .../src/app/core/tables/tables.selectors.ts   |   9 +
 .../containers/home/home-page.component.html  |  52 ++--
 .../containers/home/home-page.component.ts    |   9 +-
 .../src/app/services/table.service.ts         |  26 ++
 .../tournament-loaded.component.html          |   6 +-
 .../tournament-loaded.component.ts            |   1 -
 .../tournament-loaded.store.ts                |   7 +-
 .../tournament-setup.component.html           |  14 +-
 .../tournament-setup.store.ts                 |   9 +-
 libs/go/api/errors.go                         |   2 +
 libs/go/api/game.go                           | 294 ++++++++++++++----
 libs/go/api/overlay.go                        | 110 +++++--
 libs/go/api/routes.go                         |  61 ++--
 libs/go/api/tables.go                         | 120 ++++++-
 libs/go/apidocs/overlay-toggle_swagger.go     |  14 +-
 libs/go/apidocs/table-count_swagger.go        |  17 +
 libs/go/apidocs/table_swagger.go              |  30 ++
 package-lock.json                             |  22 ++
 package.json                                  |   1 +
 26 files changed, 715 insertions(+), 177 deletions(-)
 create mode 100644 apps/dashboard/src/app/core/tables/index.ts
 create mode 100644 apps/dashboard/src/app/core/tables/tables.actions.ts
 create mode 100644 apps/dashboard/src/app/core/tables/tables.effects.ts
 create mode 100644 apps/dashboard/src/app/core/tables/tables.reducer.ts
 create mode 100644 apps/dashboard/src/app/core/tables/tables.selectors.ts
 create mode 100644 apps/dashboard/src/app/services/table.service.ts
 create mode 100644 libs/go/apidocs/table-count_swagger.go
 create mode 100644 libs/go/apidocs/table_swagger.go

diff --git a/apps/api/.env.example b/apps/api/.env.example
index 5ac51c6..e38560d 100644
--- a/apps/api/.env.example
+++ b/apps/api/.env.example
@@ -5,3 +5,4 @@ POSTGRES_DB=pool
 POSTGRES_PORT=5432
 CHALLONGE_API_KEY=secret
 CHALLONGE_USERNAME=username
+MAX_TABLE_COUNT=11
diff --git a/apps/api/core.go b/apps/api/core.go
index 1dcb120..90a239e 100644
--- a/apps/api/core.go
+++ b/apps/api/core.go
@@ -3,6 +3,7 @@ package main
 import (
 	"fmt"
 	"os"
+	"strconv"
 
 	"github.com/codephobia/pool-overlay/libs/go/api"
 	"github.com/codephobia/pool-overlay/libs/go/challonge"
@@ -47,9 +48,15 @@ func NewCore() (*Core, error) {
 
 	// Initialize game state.
 	tables := map[int]*state.State{}
-	tables[1] = state.NewState(db, 1)
-	tables[2] = state.NewState(db, 2)
-	tables[3] = state.NewState(db, 3)
+
+	// Add tables to game state based on default provided in env file.
+	maxTableCount, err := strconv.Atoi(os.Getenv("MAX_TABLE_COUNT"))
+	if err != nil {
+		maxTableCount = 3
+	}
+	for i := 1; i <= maxTableCount; i++ {
+		tables[i] = state.NewState(db, i)
+	}
 
 	// Initialize Challonge.
 	challonge := challonge.NewChallonge(os.Getenv("CHALLONGE_API_KEY"), os.Getenv("CHALLONGE_USERNAME"), db, overlay, tables)
diff --git a/apps/dashboard/src/app/app.module.ts b/apps/dashboard/src/app/app.module.ts
index 4d5a20e..abf3365 100644
--- a/apps/dashboard/src/app/app.module.ts
+++ b/apps/dashboard/src/app/app.module.ts
@@ -1,8 +1,10 @@
 import { NgModule, isDevMode } from '@angular/core';
+import { HttpClientModule } from '@angular/common/http';
 import { BrowserModule } from '@angular/platform-browser';
 import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
 import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
 import { StoreModule } from '@ngrx/store';
+import { EffectsModule } from '@ngrx/effects';
 import { StoreRouterConnectingModule, routerReducer } from '@ngrx/router-store';
 import { StoreDevtoolsModule } from '@ngrx/store-devtools';
 
@@ -11,6 +13,7 @@ import { AppComponent } from './components/app/app.component';
 import { SideNavComponent } from './components/side-nav/side-nav.component';
 import { ENV_CONFIG } from './models/environment-config.model';
 import { environment } from '../environments/environment';
+import * as fromTables from './core/tables';
 
 const COMPONENTS = [
     AppComponent,
@@ -24,11 +27,16 @@ const COMPONENTS = [
     imports: [
         BrowserModule,
         BrowserAnimationsModule,
+        HttpClientModule,
         FontAwesomeModule,
         AppRoutingModule,
         StoreModule.forRoot({
+            [fromTables.stateKey]: fromTables.reducer,
             router: routerReducer,
         }),
+        EffectsModule.forRoot([
+            fromTables.TablesEffects,
+        ]),
         StoreRouterConnectingModule.forRoot(),
         StoreDevtoolsModule.instrument({
             maxAge: 25,
diff --git a/apps/dashboard/src/app/core/tables/index.ts b/apps/dashboard/src/app/core/tables/index.ts
new file mode 100644
index 0000000..129148e
--- /dev/null
+++ b/apps/dashboard/src/app/core/tables/index.ts
@@ -0,0 +1,4 @@
+export * from './tables.actions';
+export * from './tables.effects';
+export * from './tables.reducer';
+export * from './tables.selectors';
diff --git a/apps/dashboard/src/app/core/tables/tables.actions.ts b/apps/dashboard/src/app/core/tables/tables.actions.ts
new file mode 100644
index 0000000..2fb255a
--- /dev/null
+++ b/apps/dashboard/src/app/core/tables/tables.actions.ts
@@ -0,0 +1,11 @@
+import { createActionGroup, emptyProps, props } from '@ngrx/store';
+
+export const TablesActions = createActionGroup({
+    source: 'Tables',
+    events: {
+        'Get Count': emptyProps(),
+        'Get Count Success': props<{ count: number }>(),
+        'Get Count Error': props<{ error: string }>(),
+        'Set Count': props<{ count: number }>(),
+    },
+});
diff --git a/apps/dashboard/src/app/core/tables/tables.effects.ts b/apps/dashboard/src/app/core/tables/tables.effects.ts
new file mode 100644
index 0000000..241fbd4
--- /dev/null
+++ b/apps/dashboard/src/app/core/tables/tables.effects.ts
@@ -0,0 +1,27 @@
+import { Injectable } from '@angular/core';
+import { Actions, ROOT_EFFECTS_INIT, createEffect, ofType } from '@ngrx/effects';
+
+import { TablesActions } from './tables.actions';
+import { TableService } from '../../services/table.service';
+import { catchError, map, of, switchMap } from 'rxjs';
+
+@Injectable()
+export class TablesEffects {
+    constructor(
+        private actions$: Actions,
+        private tableService: TableService,
+    ) { }
+
+    init$ = createEffect(() => this.actions$.pipe(
+        ofType(ROOT_EFFECTS_INIT),
+        map(() => TablesActions.getCount()),
+    ));
+
+    getCount$ = createEffect(() => this.actions$.pipe(
+        ofType(TablesActions.getCount),
+        switchMap(() => this.tableService.count().pipe(
+            map(({ count }) => TablesActions.getCountSuccess({ count })),
+            catchError((error) => of(TablesActions.getCountError({ error }))),
+        )),
+    ));
+}
diff --git a/apps/dashboard/src/app/core/tables/tables.reducer.ts b/apps/dashboard/src/app/core/tables/tables.reducer.ts
new file mode 100644
index 0000000..0ee180e
--- /dev/null
+++ b/apps/dashboard/src/app/core/tables/tables.reducer.ts
@@ -0,0 +1,24 @@
+import { createReducer, on } from '@ngrx/store';
+import { TablesActions } from './tables.actions';
+
+export const stateKey = 'table-count';
+
+export interface State {
+    count: number;
+}
+
+export const initialState: State = {
+    count: 1,
+}
+
+export const reducer = createReducer(
+    initialState,
+    on(
+        TablesActions.getCountSuccess,
+        TablesActions.setCount,
+        (state, { count }) => ({
+            ...state,
+            count,
+        })
+    )
+);
diff --git a/apps/dashboard/src/app/core/tables/tables.selectors.ts b/apps/dashboard/src/app/core/tables/tables.selectors.ts
new file mode 100644
index 0000000..639647a
--- /dev/null
+++ b/apps/dashboard/src/app/core/tables/tables.selectors.ts
@@ -0,0 +1,9 @@
+import { createFeatureSelector, createSelector } from '@ngrx/store';
+import { State, stateKey } from './tables.reducer';
+
+export const selectTablesState = createFeatureSelector<State>(stateKey);
+
+export const selectTablesCount = createSelector(
+    selectTablesState,
+    (state: State) => state.count
+);
diff --git a/apps/dashboard/src/app/overlay/containers/home/home-page.component.html b/apps/dashboard/src/app/overlay/containers/home/home-page.component.html
index e8a7023..55cf499 100644
--- a/apps/dashboard/src/app/overlay/containers/home/home-page.component.html
+++ b/apps/dashboard/src/app/overlay/containers/home/home-page.component.html
@@ -1,25 +1,27 @@
-<ul class="flex flex-wrap text-sm font-medium text-center text-gray-500 dark:text-gray-400 p-4">
-    <li class="mr-2" *ngFor="let table of tables">
-        <button
-            type="button"
-            (click)="setTable(table)"
-            class="inline-block py-3 px-4 rounded-lg"
-            [ngClass]="{ 'text-white bg-blue-600': table === currentTable, 'hover:bg-gray-800 hover:text-white': table != currentTable }"
-        >
-            Table {{ table }}
-        </button>
-    </li>
-    <li>
-        <a class="inline-block py-3 px-4 text-gray-400 cursor-not-allowed dark:text-gray-500"> <fa-icon [icon]="faPlus"></fa-icon> Add Table </a>
-    </li>
-</ul>
-<div class="flex flex-row flex-none mt-5 h-52 justify-center items-center">
-    <ng-container *ngFor="let table of tables">
-        <pool-overlay-scoreboard [hidden]="table !== currentTable" [table]="table"></pool-overlay-scoreboard>
-    </ng-container>
-</div>
-<div>
-    <ng-container *ngFor="let table of tables">
-        <pool-overlay-controller [hidden]="table !== currentTable" [table]="table"></pool-overlay-controller>
-    </ng-container>
-</div>
+<ng-container *ngIf="tables$ | async as tables">
+    <ul class="flex flex-wrap text-sm font-medium text-center text-gray-500 dark:text-gray-400 p-4">
+        <li class="mr-2" *ngFor="let table of tables">
+            <button
+                type="button"
+                (click)="setTable(table)"
+                class="inline-block py-3 px-4 rounded-lg"
+                [ngClass]="{ 'text-white bg-blue-600': table === currentTable, 'hover:bg-gray-800 hover:text-white': table != currentTable }"
+            >
+                Table {{ table }}
+            </button>
+        </li>
+        <li>
+            <a class="inline-block py-3 px-4 text-gray-400 cursor-not-allowed dark:text-gray-500"> <fa-icon [icon]="faPlus"></fa-icon> Add Table </a>
+        </li>
+    </ul>
+    <div class="flex flex-row flex-none mt-5 h-52 justify-center items-center">
+        <ng-container *ngFor="let table of tables">
+            <pool-overlay-scoreboard [hidden]="table !== currentTable" [table]="table"></pool-overlay-scoreboard>
+        </ng-container>
+    </div>
+    <div>
+        <ng-container *ngFor="let table of tables">
+            <pool-overlay-controller [hidden]="table !== currentTable" [table]="table"></pool-overlay-controller>
+        </ng-container>
+    </div>
+</ng-container>
diff --git a/apps/dashboard/src/app/overlay/containers/home/home-page.component.ts b/apps/dashboard/src/app/overlay/containers/home/home-page.component.ts
index 935e371..409c6fc 100644
--- a/apps/dashboard/src/app/overlay/containers/home/home-page.component.ts
+++ b/apps/dashboard/src/app/overlay/containers/home/home-page.component.ts
@@ -1,5 +1,8 @@
 import { Component } from '@angular/core';
 import { faPlus } from '@fortawesome/pro-regular-svg-icons';
+import { Store } from '@ngrx/store';
+import * as fromTables from '../../../core/tables';
+import { map } from 'rxjs';
 
 @Component({
     selector: 'pool-overlay-home-page',
@@ -7,8 +10,12 @@ import { faPlus } from '@fortawesome/pro-regular-svg-icons';
 })
 export class HomePageComponent {
     public faPlus = faPlus;
-    public tables: number[] = [1, 2, 3];
     public currentTable = 1;
+    public tables$ = this.store.select(fromTables.selectTablesCount).pipe(
+        map((count) => Array.from(new Array(count), (x, i) => i + 1)),
+    );
+
+    constructor(private store: Store) { }
 
     public setTable(table: number): void {
         this.currentTable = table;
diff --git a/apps/dashboard/src/app/services/table.service.ts b/apps/dashboard/src/app/services/table.service.ts
new file mode 100644
index 0000000..8483696
--- /dev/null
+++ b/apps/dashboard/src/app/services/table.service.ts
@@ -0,0 +1,26 @@
+import { Inject, Injectable } from '@angular/core';
+import { HttpClient } from '@angular/common/http';
+import { Observable } from 'rxjs';
+
+import { EnvironmentConfig, ENV_CONFIG } from '../models/environment-config.model';
+import { ICount } from '../models/count.model';
+
+@Injectable({ providedIn: 'root' })
+export class TableService {
+    private apiURL: string;
+    private apiVersion: string;
+    private endpoint = 'table';
+
+    constructor(
+        @Inject(ENV_CONFIG) config: EnvironmentConfig,
+        private http: HttpClient,
+    ) {
+        this.apiURL = config.environment.apiURL;
+        this.apiVersion = config.environment.apiVersion;
+    }
+
+    public count(): Observable<ICount> {
+        let url = `${this.apiURL}/${this.apiVersion}/${this.endpoint}/count`;
+        return this.http.get<ICount>(url);
+    }
+}
diff --git a/apps/dashboard/src/app/tournament/components/tournament-loaded/tournament-loaded.component.html b/apps/dashboard/src/app/tournament/components/tournament-loaded/tournament-loaded.component.html
index 85fd763..b9305a3 100644
--- a/apps/dashboard/src/app/tournament/components/tournament-loaded/tournament-loaded.component.html
+++ b/apps/dashboard/src/app/tournament/components/tournament-loaded/tournament-loaded.component.html
@@ -10,8 +10,8 @@ <h2 class="text-white uppercase">{{ vm.tournament?.name }}</h2>
         </div>
         <div class="flex flex-row flex-grow bg-sad-table-odd">
             <div class="flex flex-col flex-grow p-5 gap-5">
-                <div class="flex flex-row gap-5">
-                    <div class="flex flex-col flex-grow text-white w-1/3" *ngFor="let table of tables">
+                <div class="grid grid-cols-3 gap-5">
+                    <div class="flex flex-col text-white" *ngFor="let table of vm.tablesArr">
                         <div class="flex flex-row h-66px justify-between items-center py-2.5 px-5 border-l border-r border-t border-sad-active rounded-t bg-sad-table-even uppercase">
                             <div>Table {{ table }}</div>
                             <div>
@@ -20,7 +20,7 @@ <h2 class="text-white uppercase">{{ vm.tournament?.name }}</h2>
                                 </button>
                                 <ng-template #menu>
                                     <div class="border bg-sad-input border-sad-active rounded mt-2.5 py-2.5 text-white" cdkMenu>
-                                        <ng-container *ngFor="let menuTable of tables">
+                                        <ng-container *ngFor="let menuTable of vm.tablesArr">
                                             <button type="button" *ngIf="table !== menuTable" class="block hover:bg-sad-active py-2.5 px-5" cdkMenuItem (click)="swapTables(table, menuTable)">
                                                 Swap with Table {{ menuTable }}
                                             </button>
diff --git a/apps/dashboard/src/app/tournament/components/tournament-loaded/tournament-loaded.component.ts b/apps/dashboard/src/app/tournament/components/tournament-loaded/tournament-loaded.component.ts
index 077c7d7..e200339 100644
--- a/apps/dashboard/src/app/tournament/components/tournament-loaded/tournament-loaded.component.ts
+++ b/apps/dashboard/src/app/tournament/components/tournament-loaded/tournament-loaded.component.ts
@@ -15,7 +15,6 @@ export class TournamentLoadedComponent {
     readonly faLock = faLock;
     readonly faEllipsisVertical = faEllipsisVertical
     readonly gameType = GameType;
-    readonly tables = [1, 2, 3];
     readonly vm$ = this.store.vm$;
 
     constructor(
diff --git a/apps/dashboard/src/app/tournament/components/tournament-loaded/tournament-loaded.store.ts b/apps/dashboard/src/app/tournament/components/tournament-loaded/tournament-loaded.store.ts
index 96d6784..54440dc 100644
--- a/apps/dashboard/src/app/tournament/components/tournament-loaded/tournament-loaded.store.ts
+++ b/apps/dashboard/src/app/tournament/components/tournament-loaded/tournament-loaded.store.ts
@@ -5,6 +5,8 @@ import { IGame, OverlayState, Tournament } from '@pool-overlay/models';
 import { switchMap, tap } from 'rxjs';
 import { TablesService } from '../../services/tables.service';
 import { TournamentsService } from '../../services/tournament.service';
+import { Store } from '@ngrx/store';
+import * as fromTables from '../../../core/tables';
 
 export enum LoadingState {
     INIT,
@@ -33,6 +35,7 @@ export const initialState: TournamentLoadedState = {
 export class TournamentLoadedStore extends ComponentStore<TournamentLoadedState> {
     constructor(
         private router: Router,
+        private store: Store,
         private tournamentsService: TournamentsService,
         private tablesService: TablesService,
     ) {
@@ -81,10 +84,12 @@ export class TournamentLoadedStore extends ComponentStore<TournamentLoadedState>
         this.isLoaded$,
         this.tournament$,
         this.tables$,
-        (isLoaded, tournament, tables) => ({
+        this.store.select(fromTables.selectTablesCount),
+        (isLoaded, tournament, tables, tablesCount) => ({
             isLoaded,
             tournament,
             tables,
+            tablesArr: Array.from(new Array(tablesCount), (x, i) => i + 1),
         })
     );
 
diff --git a/apps/dashboard/src/app/tournament/components/tournament-setup/tournament-setup.component.html b/apps/dashboard/src/app/tournament/components/tournament-setup/tournament-setup.component.html
index cc7e40e..0c2e458 100644
--- a/apps/dashboard/src/app/tournament/components/tournament-setup/tournament-setup.component.html
+++ b/apps/dashboard/src/app/tournament/components/tournament-setup/tournament-setup.component.html
@@ -61,10 +61,16 @@ <h2 class="text-white uppercase">{{ vm.tournament?.name }}</h2>
                         <div class="flex flex-row flex-grow bg-sad-table-odd p-5 border border-sad-active rounded-b">
                             <div class="flex flex-col gap-2.5">
                                 <div class="flex flex-row gap-2.5">
-                                    <button type="button" class="px-5 py-2.5 rounded text-white" [ngClass]="vm.maxTables === 1 ? 'bg-blue-700' : 'bg-gray-700'" (click)="updateMaxTables(1)">1</button>
-                                    <button type="button" class="px-5 py-2.5 rounded text-white" [ngClass]="vm.maxTables === 2 ? 'bg-blue-700' : 'bg-gray-700'" (click)="updateMaxTables(2)">2</button>
-                                    <button type="button" class="px-5 py-2.5 rounded text-white" [ngClass]="vm.maxTables === 3 ? 'bg-blue-700' : 'bg-gray-700'" (click)="updateMaxTables(3)">3</button>
-                                    <button type="button" class="px-5 py-2.5 rounded text-white" [ngClass]="vm.maxTables === 4 ? 'bg-blue-700' : 'bg-gray-700'" (click)="updateMaxTables(4)">4</button>
+                                    <ng-container *ngFor="let table of vm.tables">
+                                        <button
+                                            type="button"
+                                            class="px-5 py-2.5 rounded text-white"
+                                            [ngClass]="vm.maxTables === table ? 'bg-blue-700' : 'bg-gray-700'"
+                                            (click)="updateMaxTables(table)"
+                                        >
+                                            {{ table }}
+                                        </button>
+                                    </ng-container>
                                 </div>
                             </div>
                         </div>
diff --git a/apps/dashboard/src/app/tournament/components/tournament-setup/tournament-setup.store.ts b/apps/dashboard/src/app/tournament/components/tournament-setup/tournament-setup.store.ts
index 33be6c8..0a4fdec 100644
--- a/apps/dashboard/src/app/tournament/components/tournament-setup/tournament-setup.store.ts
+++ b/apps/dashboard/src/app/tournament/components/tournament-setup/tournament-setup.store.ts
@@ -1,9 +1,11 @@
 import { Injectable } from '@angular/core';
 import { Router } from '@angular/router';
+import { Store } from '@ngrx/store';
 import { ComponentStore, tapResponse } from '@ngrx/component-store';
-import { GameType, Tournament } from '@pool-overlay/models';
 import { switchMap, tap, withLatestFrom } from 'rxjs';
+import { GameType, Tournament } from '@pool-overlay/models';
 import { TournamentsService } from '../../services/tournament.service';
+import * as fromTables from '../../../core/tables';
 
 export enum LoadingState {
     INIT,
@@ -43,6 +45,7 @@ export const initialState: TournamentSetupState = {
 export class TournamentSetupStore extends ComponentStore<TournamentSetupState> {
     constructor(
         private router: Router,
+        private store: Store,
         private tournamentsService: TournamentsService,
     ) {
         super(initialState);
@@ -120,6 +123,7 @@ export class TournamentSetupStore extends ComponentStore<TournamentSetupState> {
     readonly vm$ = this.select(
         this.isLoaded$,
         this.tournament$,
+        this.store.select(fromTables.selectTablesCount),
         this.maxTables$,
         this.isHandicapped$,
         this.showOverlay$,
@@ -129,9 +133,10 @@ export class TournamentSetupStore extends ComponentStore<TournamentSetupState> {
         this.gameType$,
         this.aSideRaceTo$,
         this.bSideRaceTo$,
-        (isLoaded, tournament, maxTables, isHandicapped, showOverlay, showFlags, showFargo, showScore, gameType, aSideRaceTo, bSideRaceTo) => ({
+        (isLoaded, tournament, tablesCount, maxTables, isHandicapped, showOverlay, showFlags, showFargo, showScore, gameType, aSideRaceTo, bSideRaceTo) => ({
             isLoaded,
             tournament,
+            tables: Array.from(new Array(tablesCount), (x, i) => i + 1),
             maxTables,
             isHandicapped,
             showOverlay,
diff --git a/libs/go/api/errors.go b/libs/go/api/errors.go
index 32c44bc..82da1d7 100644
--- a/libs/go/api/errors.go
+++ b/libs/go/api/errors.go
@@ -37,4 +37,6 @@ var (
 	ErrInvalidTournamentDetails = errors.New("invalid tournament details")
 	// ErrInvalidTableNumber - Invalid table number.
 	ErrInvalidTableNumber = errors.New("invalid table number")
+	// ErrRemoveOnlyTable - Cannot remove only table.
+	ErrRemoveOnlyTable = errors.New("cannot remove only table")
 )
diff --git a/libs/go/api/game.go b/libs/go/api/game.go
index 1044e72..f4faa55 100644
--- a/libs/go/api/game.go
+++ b/libs/go/api/game.go
@@ -5,10 +5,12 @@ import (
 	"errors"
 	"log"
 	"net/http"
+	"strconv"
 
 	"github.com/codephobia/pool-overlay/libs/go/events"
 	"github.com/codephobia/pool-overlay/libs/go/models"
 	"github.com/codephobia/pool-overlay/libs/go/overlay"
+	"github.com/gorilla/mux"
 )
 
 const (
@@ -110,15 +112,15 @@ type GameFargoHotHandicapPatchResp struct {
 }
 
 // Handler for /game.
-func (server *Server) handleGame(table int) http.Handler {
+func (server *Server) handleGame() http.Handler {
 	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 		switch r.Method {
 		case "OPTIONS":
 			server.HandleOptions(w, r)
 		case "GET":
-			server.handleGameGet(w, r, table)
+			server.handleGameGet(w, r)
 		case "POST":
-			server.handleGamePost(w, r, table)
+			server.handleGamePost(w, r)
 		default:
 			server.handleError(w, r, http.StatusMethodNotAllowed, ErrEndpointMethodNotAllowed)
 		}
@@ -126,38 +128,70 @@ func (server *Server) handleGame(table int) http.Handler {
 }
 
 // Game handler for GET method.
-func (server *Server) handleGameGet(w http.ResponseWriter, r *http.Request, table int) {
+func (server *Server) handleGameGet(w http.ResponseWriter, r *http.Request) {
+	// get params for table numbers from url
+	params := mux.Vars(r)
+
+	// get table number
+	tableNumValue, ok := params["tableNum"]
+	if !ok || len(tableNumValue) < 1 {
+		server.handleError(w, r, http.StatusUnprocessableEntity, ErrInvalidTableNumber)
+		return
+	}
+
+	// convert table number to int
+	tableNum, err := strconv.Atoi(tableNumValue)
+	if err != nil || tableNum > len(server.tables) {
+		server.handleError(w, r, http.StatusUnprocessableEntity, ErrInvalidTableNumber)
+		return
+	}
+
 	// send response
-	server.handleSuccess(w, r, server.tables[table].Game)
+	server.handleSuccess(w, r, server.tables[tableNum].Game)
 }
 
 // Game handler for POST method.
-func (server *Server) handleGamePost(w http.ResponseWriter, r *http.Request, table int) {
+func (server *Server) handleGamePost(w http.ResponseWriter, r *http.Request) {
+	// get params for table numbers from url
+	params := mux.Vars(r)
+
+	// get table number
+	tableNumValue, ok := params["tableNum"]
+	if !ok || len(tableNumValue) < 1 {
+		server.handleError(w, r, http.StatusUnprocessableEntity, ErrInvalidTableNumber)
+		return
+	}
+
+	// convert table number to int
+	tableNum, err := strconv.Atoi(tableNumValue)
+	if err != nil || tableNum > len(server.tables) {
+		server.handleError(w, r, http.StatusUnprocessableEntity, ErrInvalidTableNumber)
+		return
+	}
+
 	// Save existing game.
-	if err := server.tables[table].Game.Save(true); err != nil {
+	if err := server.tables[tableNum].Game.Save(true); err != nil {
 		server.handleError(w, r, http.StatusInternalServerError, err)
 		return
 	}
 
 	// Check if we're in tournament mode right now.
 	if server.challonge.InTournamentMode() {
-		if err := server.challonge.Continue(table); err != nil {
+		if err := server.challonge.Continue(tableNum); err != nil {
 			log.Printf("unable to continue tournament: %s", err)
 		}
 	} else {
 		// Reset game to create a new one with same players / settings.
-		if err := server.tables[table].Game.Reset(); err != nil {
+		if err := server.tables[tableNum].Game.Reset(); err != nil {
 			server.handleError(w, r, http.StatusInternalServerError, err)
 			return
 		}
 	}
 
-	// TODO: BROADCAST NEXT GAME FOR TABLE OR LOCK DOWN TABLET ON THAT TABLE
-
 	// Generate message to broadcast to overlay.
 	message, err := overlay.NewEvent(
 		events.GameEventType,
-		events.NewGameEventPayload(server.tables[table].Game),
+		events.NewGameEventPayload(server.tables[tableNum].Game),
 	).ToBytes()
 	if err != nil {
 		server.handleError(w, r, http.StatusUnprocessableEntity, ErrUnableToBroadcastUpdate)
@@ -172,13 +206,13 @@ func (server *Server) handleGamePost(w http.ResponseWriter, r *http.Request, tab
 }
 
 // Handler for /game/type.
-func (server *Server) handleGameType(table int) http.Handler {
+func (server *Server) handleGameType() http.Handler {
 	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 		switch r.Method {
 		case "OPTIONS":
 			server.HandleOptions(w, r)
 		case "PATCH":
-			server.handleGameTypePatch(w, r, table)
+			server.handleGameTypePatch(w, r)
 		default:
 			server.handleError(w, r, http.StatusMethodNotAllowed, ErrEndpointMethodNotAllowed)
 		}
@@ -186,7 +220,24 @@ func (server *Server) handleGameType(table int) http.Handler {
 }
 
 // Game type handler for PATCH method.
-func (server *Server) handleGameTypePatch(w http.ResponseWriter, r *http.Request, table int) {
+func (server *Server) handleGameTypePatch(w http.ResponseWriter, r *http.Request) {
+	// get params for table numbers from url
+	params := mux.Vars(r)
+
+	// get table number
+	tableNumValue, ok := params["tableNum"]
+	if !ok || len(tableNumValue) < 1 {
+		server.handleError(w, r, http.StatusUnprocessableEntity, ErrInvalidTableNumber)
+		return
+	}
+
+	// convert table number to int
+	tableNum, err := strconv.Atoi(tableNumValue)
+	if err != nil || tableNum > len(server.tables) {
+		server.handleError(w, r, http.StatusUnprocessableEntity, ErrInvalidTableNumber)
+		return
+	}
+
 	// decode the body
 	var body GameTypePatchBody
 	decoder := json.NewDecoder(r.Body)
@@ -202,7 +253,7 @@ func (server *Server) handleGameTypePatch(w http.ResponseWriter, r *http.Request
 	}
 
 	// update game type
-	if err := server.tables[table].Game.SetType(body.Type); err != nil {
+	if err := server.tables[tableNum].Game.SetType(body.Type); err != nil {
 		// TODO: LOG THIS ERROR
 		server.handleError(w, r, http.StatusInternalServerError, ErrInternalServerError)
 	}
@@ -210,7 +261,7 @@ func (server *Server) handleGameTypePatch(w http.ResponseWriter, r *http.Request
 	// Generate message to broadcast to overlay.
 	message, err := overlay.NewEvent(
 		events.GameEventType,
-		events.NewGameEventPayload(server.tables[table].Game),
+		events.NewGameEventPayload(server.tables[tableNum].Game),
 	).ToBytes()
 	if err != nil {
 		server.handleError(w, r, http.StatusUnprocessableEntity, ErrUnableToBroadcastUpdate)
@@ -225,13 +276,13 @@ func (server *Server) handleGameTypePatch(w http.ResponseWriter, r *http.Request
 }
 
 // Handler for /game/vs-mode.
-func (server *Server) handleGameVsMode(table int) http.Handler {
+func (server *Server) handleGameVsMode() http.Handler {
 	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 		switch r.Method {
 		case "OPTIONS":
 			server.HandleOptions(w, r)
 		case "PATCH":
-			server.handleGameVsModePatch(w, r, table)
+			server.handleGameVsModePatch(w, r)
 		default:
 			server.handleError(w, r, http.StatusMethodNotAllowed, ErrEndpointMethodNotAllowed)
 		}
@@ -239,7 +290,24 @@ func (server *Server) handleGameVsMode(table int) http.Handler {
 }
 
 // Game vs-mode handler for PATCH method.
-func (server *Server) handleGameVsModePatch(w http.ResponseWriter, r *http.Request, table int) {
+func (server *Server) handleGameVsModePatch(w http.ResponseWriter, r *http.Request) {
+	// get params for table numbers from url
+	params := mux.Vars(r)
+
+	// get table number
+	tableNumValue, ok := params["tableNum"]
+	if !ok || len(tableNumValue) < 1 {
+		server.handleError(w, r, http.StatusUnprocessableEntity, ErrInvalidTableNumber)
+		return
+	}
+
+	// convert table number to int
+	tableNum, err := strconv.Atoi(tableNumValue)
+	if err != nil || tableNum > len(server.tables) {
+		server.handleError(w, r, http.StatusUnprocessableEntity, ErrInvalidTableNumber)
+		return
+	}
+
 	// decode the body
 	var body GameVsModePatchBody
 	decoder := json.NewDecoder(r.Body)
@@ -255,7 +323,7 @@ func (server *Server) handleGameVsModePatch(w http.ResponseWriter, r *http.Reque
 	}
 
 	// update game vs mode
-	if err := server.tables[table].Game.SetVsMode(body.VsMode); err != nil {
+	if err := server.tables[tableNum].Game.SetVsMode(body.VsMode); err != nil {
 		// TODO: LOG THIS ERROR
 		server.handleError(w, r, http.StatusInternalServerError, ErrInternalServerError)
 	}
@@ -263,7 +331,7 @@ func (server *Server) handleGameVsModePatch(w http.ResponseWriter, r *http.Reque
 	// Generate message to broadcast to overlay.
 	message, err := overlay.NewEvent(
 		events.GameEventType,
-		events.NewGameEventPayload(server.tables[table].Game),
+		events.NewGameEventPayload(server.tables[tableNum].Game),
 	).ToBytes()
 	if err != nil {
 		server.handleError(w, r, http.StatusUnprocessableEntity, ErrUnableToBroadcastUpdate)
@@ -278,13 +346,13 @@ func (server *Server) handleGameVsModePatch(w http.ResponseWriter, r *http.Reque
 }
 
 // Handler for /game/race-to.
-func (server *Server) handleGameRaceTo(table int) http.Handler {
+func (server *Server) handleGameRaceTo() http.Handler {
 	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 		switch r.Method {
 		case "OPTIONS":
 			server.HandleOptions(w, r)
 		case "PATCH":
-			server.handleGameRaceToPatch(w, r, table)
+			server.handleGameRaceToPatch(w, r)
 		default:
 			server.handleError(w, r, http.StatusMethodNotAllowed, ErrEndpointMethodNotAllowed)
 		}
@@ -292,7 +360,24 @@ func (server *Server) handleGameRaceTo(table int) http.Handler {
 }
 
 // Game race-to handler for PATCH method.
-func (server *Server) handleGameRaceToPatch(w http.ResponseWriter, r *http.Request, table int) {
+func (server *Server) handleGameRaceToPatch(w http.ResponseWriter, r *http.Request) {
+	// get params for table numbers from url
+	params := mux.Vars(r)
+
+	// get table number
+	tableNumValue, ok := params["tableNum"]
+	if !ok || len(tableNumValue) < 1 {
+		server.handleError(w, r, http.StatusUnprocessableEntity, ErrInvalidTableNumber)
+		return
+	}
+
+	// convert table number to int
+	tableNum, err := strconv.Atoi(tableNumValue)
+	if err != nil || tableNum > len(server.tables) {
+		server.handleError(w, r, http.StatusUnprocessableEntity, ErrInvalidTableNumber)
+		return
+	}
+
 	// decode the body
 	var body GameRaceToPatchBody
 	decoder := json.NewDecoder(r.Body)
@@ -309,12 +394,12 @@ func (server *Server) handleGameRaceToPatch(w http.ResponseWriter, r *http.Reque
 
 	// update race to number
 	if body.Direction == gameDirectionIncrement {
-		if err := server.tables[table].Game.IncrementRaceTo(); err != nil {
+		if err := server.tables[tableNum].Game.IncrementRaceTo(); err != nil {
 			// TODO: LOG THIS ERROR
 			server.handleError(w, r, http.StatusInternalServerError, ErrInternalServerError)
 		}
 	} else {
-		if err := server.tables[table].Game.DecrementRaceTo(); err != nil {
+		if err := server.tables[tableNum].Game.DecrementRaceTo(); err != nil {
 			// TODO: LOG THIS ERROR
 			server.handleError(w, r, http.StatusInternalServerError, ErrInternalServerError)
 		}
@@ -323,7 +408,7 @@ func (server *Server) handleGameRaceToPatch(w http.ResponseWriter, r *http.Reque
 	// Generate message to broadcast to overlay.
 	message, err := overlay.NewEvent(
 		events.GameEventType,
-		events.NewGameEventPayload(server.tables[table].Game),
+		events.NewGameEventPayload(server.tables[tableNum].Game),
 	).ToBytes()
 	if err != nil {
 		server.handleError(w, r, http.StatusUnprocessableEntity, ErrUnableToBroadcastUpdate)
@@ -335,21 +420,21 @@ func (server *Server) handleGameRaceToPatch(w http.ResponseWriter, r *http.Reque
 
 	// send response
 	server.handleSuccess(w, r, GameRaceToResp{
-		RaceTo:              server.tables[table].Game.RaceTo,
-		UseFargoHotHandicap: server.tables[table].Game.UseFargoHotHandicap,
+		RaceTo:              server.tables[tableNum].Game.RaceTo,
+		UseFargoHotHandicap: server.tables[tableNum].Game.UseFargoHotHandicap,
 	})
 }
 
 // Handler for /game/score
-func (server *Server) handleGameScore(table int) http.Handler {
+func (server *Server) handleGameScore() http.Handler {
 	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 		switch r.Method {
 		case "OPTIONS":
 			server.HandleOptions(w, r)
 		case "PATCH":
-			server.handleGameScorePatch(w, r, table)
+			server.handleGameScorePatch(w, r)
 		case "DELETE":
-			server.handleGameScoreDelete(w, r, table)
+			server.handleGameScoreDelete(w, r)
 		default:
 			server.handleError(w, r, http.StatusMethodNotAllowed, ErrEndpointMethodNotAllowed)
 		}
@@ -357,7 +442,24 @@ func (server *Server) handleGameScore(table int) http.Handler {
 }
 
 // Game score handler for PATCH method.
-func (server *Server) handleGameScorePatch(w http.ResponseWriter, r *http.Request, table int) {
+func (server *Server) handleGameScorePatch(w http.ResponseWriter, r *http.Request) {
+	// get params for table numbers from url
+	params := mux.Vars(r)
+
+	// get table number
+	tableNumValue, ok := params["tableNum"]
+	if !ok || len(tableNumValue) < 1 {
+		server.handleError(w, r, http.StatusUnprocessableEntity, ErrInvalidTableNumber)
+		return
+	}
+
+	// convert table number to int
+	tableNum, err := strconv.Atoi(tableNumValue)
+	if err != nil || tableNum > len(server.tables) {
+		server.handleError(w, r, http.StatusUnprocessableEntity, ErrInvalidTableNumber)
+		return
+	}
+
 	// decode the body
 	var body GameScorePatchBody
 	decoder := json.NewDecoder(r.Body)
@@ -374,7 +476,7 @@ func (server *Server) handleGameScorePatch(w http.ResponseWriter, r *http.Reques
 
 	// update score
 	if body.Direction == gameDirectionIncrement {
-		if err := server.tables[table].Game.IncrementScore(body.PlayerNum); err != nil {
+		if err := server.tables[tableNum].Game.IncrementScore(body.PlayerNum); err != nil {
 			if errors.Is(err, models.ErrInvalidPlayerNumber) {
 				server.handleError(w, r, http.StatusUnprocessableEntity, err)
 			} else {
@@ -384,7 +486,7 @@ func (server *Server) handleGameScorePatch(w http.ResponseWriter, r *http.Reques
 			return
 		}
 	} else {
-		if err := server.tables[table].Game.DecrementScore(body.PlayerNum); err != nil {
+		if err := server.tables[tableNum].Game.DecrementScore(body.PlayerNum); err != nil {
 			if errors.Is(err, models.ErrInvalidPlayerNumber) {
 				server.handleError(w, r, http.StatusUnprocessableEntity, err)
 			} else {
@@ -398,7 +500,7 @@ func (server *Server) handleGameScorePatch(w http.ResponseWriter, r *http.Reques
 	// Generate message to broadcast to overlay.
 	message, err := overlay.NewEvent(
 		events.GameEventType,
-		events.NewGameEventPayload(server.tables[table].Game),
+		events.NewGameEventPayload(server.tables[tableNum].Game),
 	).ToBytes()
 	if err != nil {
 		server.handleError(w, r, http.StatusUnprocessableEntity, ErrUnableToBroadcastUpdate)
@@ -410,7 +512,7 @@ func (server *Server) handleGameScorePatch(w http.ResponseWriter, r *http.Reques
 
 	// Check if we're in tournament mode right now.
 	if server.challonge.InTournamentMode() {
-		if err := server.challonge.UpdateMatchScore(table); err != nil {
+		if err := server.challonge.UpdateMatchScore(tableNum); err != nil {
 			// fail gracefully since live score keeping isn't that important
 			log.Printf("error updating match score on challonge: %s", err)
 		}
@@ -418,15 +520,32 @@ func (server *Server) handleGameScorePatch(w http.ResponseWriter, r *http.Reques
 
 	// send response
 	server.handleSuccess(w, r, GameScoreResp{
-		ScoreOne: server.tables[table].Game.ScoreOne,
-		ScoreTwo: server.tables[table].Game.ScoreTwo,
+		ScoreOne: server.tables[tableNum].Game.ScoreOne,
+		ScoreTwo: server.tables[tableNum].Game.ScoreTwo,
 	})
 }
 
 // Game score reset handler for DELETE method.
-func (server *Server) handleGameScoreDelete(w http.ResponseWriter, r *http.Request, table int) {
+func (server *Server) handleGameScoreDelete(w http.ResponseWriter, r *http.Request) {
+	// get params for table numbers from url
+	params := mux.Vars(r)
+
+	// get table number
+	tableNumValue, ok := params["tableNum"]
+	if !ok || len(tableNumValue) < 1 {
+		server.handleError(w, r, http.StatusUnprocessableEntity, ErrInvalidTableNumber)
+		return
+	}
+
+	// convert table number to int
+	tableNum, err := strconv.Atoi(tableNumValue)
+	if err != nil || tableNum > len(server.tables) {
+		server.handleError(w, r, http.StatusUnprocessableEntity, ErrInvalidTableNumber)
+		return
+	}
+
 	// reset game score
-	if err := server.tables[table].Game.ResetScore(); err != nil {
+	if err := server.tables[tableNum].Game.ResetScore(); err != nil {
 		// TODO: LOG THIS ERROR
 		server.handleError(w, r, http.StatusInternalServerError, ErrInternalServerError)
 	}
@@ -434,7 +553,7 @@ func (server *Server) handleGameScoreDelete(w http.ResponseWriter, r *http.Reque
 	// Generate message to broadcast to overlay.
 	message, err := overlay.NewEvent(
 		events.GameEventType,
-		events.NewGameEventPayload(server.tables[table].Game),
+		events.NewGameEventPayload(server.tables[tableNum].Game),
 	).ToBytes()
 	if err != nil {
 		server.handleError(w, r, http.StatusUnprocessableEntity, ErrUnableToBroadcastUpdate)
@@ -446,21 +565,21 @@ func (server *Server) handleGameScoreDelete(w http.ResponseWriter, r *http.Reque
 
 	// send response
 	server.handleSuccess(w, r, GameScoreResp{
-		ScoreOne: server.tables[table].Game.ScoreOne,
-		ScoreTwo: server.tables[table].Game.ScoreTwo,
+		ScoreOne: server.tables[tableNum].Game.ScoreOne,
+		ScoreTwo: server.tables[tableNum].Game.ScoreTwo,
 	})
 }
 
 // Handler for /game/players
-func (server *Server) handleGamePlayers(table int) http.Handler {
+func (server *Server) handleGamePlayers() http.Handler {
 	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 		switch r.Method {
 		case "OPTIONS":
 			server.HandleOptions(w, r)
 		case "PATCH":
-			server.handleGamePlayersPatch(w, r, table)
+			server.handleGamePlayersPatch(w, r)
 		case "DELETE":
-			server.handleGamePlayersDelete(w, r, table)
+			server.handleGamePlayersDelete(w, r)
 		default:
 			server.handleError(w, r, http.StatusMethodNotAllowed, ErrEndpointMethodNotAllowed)
 		}
@@ -468,7 +587,24 @@ func (server *Server) handleGamePlayers(table int) http.Handler {
 }
 
 // Game players handler for PATCH method.
-func (server *Server) handleGamePlayersPatch(w http.ResponseWriter, r *http.Request, table int) {
+func (server *Server) handleGamePlayersPatch(w http.ResponseWriter, r *http.Request) {
+	// get params for table numbers from url
+	params := mux.Vars(r)
+
+	// get table number
+	tableNumValue, ok := params["tableNum"]
+	if !ok || len(tableNumValue) < 1 {
+		server.handleError(w, r, http.StatusUnprocessableEntity, ErrInvalidTableNumber)
+		return
+	}
+
+	// convert table number to int
+	tableNum, err := strconv.Atoi(tableNumValue)
+	if err != nil || tableNum > len(server.tables) {
+		server.handleError(w, r, http.StatusUnprocessableEntity, ErrInvalidTableNumber)
+		return
+	}
+
 	// decode the body
 	var body GamePlayersPatchBody
 	decoder := json.NewDecoder(r.Body)
@@ -490,7 +626,7 @@ func (server *Server) handleGamePlayersPatch(w http.ResponseWriter, r *http.Requ
 		}
 	}
 
-	if err := server.tables[table].Game.SetPlayer(body.PlayerNum, &player); err != nil {
+	if err := server.tables[tableNum].Game.SetPlayer(body.PlayerNum, &player); err != nil {
 		if errors.Is(err, models.ErrInvalidPlayerNumber) {
 			server.handleError(w, r, http.StatusUnprocessableEntity, models.ErrInvalidPlayerNumber)
 		} else {
@@ -503,7 +639,7 @@ func (server *Server) handleGamePlayersPatch(w http.ResponseWriter, r *http.Requ
 	// Generate message to broadcast to overlay.
 	message, err := overlay.NewEvent(
 		events.GameEventType,
-		events.NewGameEventPayload(server.tables[table].Game),
+		events.NewGameEventPayload(server.tables[tableNum].Game),
 	).ToBytes()
 	if err != nil {
 		server.handleError(w, r, http.StatusUnprocessableEntity, ErrUnableToBroadcastUpdate)
@@ -514,11 +650,28 @@ func (server *Server) handleGamePlayersPatch(w http.ResponseWriter, r *http.Requ
 	server.overlay.Broadcast <- message
 
 	// send response
-	server.handleSuccess(w, r, server.tables[table].Game)
+	server.handleSuccess(w, r, server.tables[tableNum].Game)
 }
 
 // Game players handler for DELETE method.
-func (server *Server) handleGamePlayersDelete(w http.ResponseWriter, r *http.Request, table int) {
+func (server *Server) handleGamePlayersDelete(w http.ResponseWriter, r *http.Request) {
+	// get params for table numbers from url
+	params := mux.Vars(r)
+
+	// get table number
+	tableNumValue, ok := params["tableNum"]
+	if !ok || len(tableNumValue) < 1 {
+		server.handleError(w, r, http.StatusUnprocessableEntity, ErrInvalidTableNumber)
+		return
+	}
+
+	// convert table number to int
+	tableNum, err := strconv.Atoi(tableNumValue)
+	if err != nil || tableNum > len(server.tables) {
+		server.handleError(w, r, http.StatusUnprocessableEntity, ErrInvalidTableNumber)
+		return
+	}
+
 	// decode the body
 	var body GamePlayersDeleteBody
 	decoder := json.NewDecoder(r.Body)
@@ -528,7 +681,7 @@ func (server *Server) handleGamePlayersDelete(w http.ResponseWriter, r *http.Req
 	}
 
 	// unset the current player
-	if err := server.tables[table].Game.UnsetPlayer(body.PlayerNum); err != nil {
+	if err := server.tables[tableNum].Game.UnsetPlayer(body.PlayerNum); err != nil {
 		if errors.Is(err, models.ErrInvalidPlayerNumber) {
 			server.handleError(w, r, http.StatusUnprocessableEntity, models.ErrInvalidPlayerNumber)
 		} else {
@@ -541,7 +694,7 @@ func (server *Server) handleGamePlayersDelete(w http.ResponseWriter, r *http.Req
 	// Generate message to broadcast to overlay.
 	message, err := overlay.NewEvent(
 		events.GameEventType,
-		events.NewGameEventPayload(server.tables[table].Game),
+		events.NewGameEventPayload(server.tables[tableNum].Game),
 	).ToBytes()
 	if err != nil {
 		server.handleError(w, r, http.StatusUnprocessableEntity, ErrUnableToBroadcastUpdate)
@@ -552,7 +705,7 @@ func (server *Server) handleGamePlayersDelete(w http.ResponseWriter, r *http.Req
 	server.overlay.Broadcast <- message
 
 	// send response
-	server.handleSuccess(w, r, server.tables[table].Game)
+	server.handleSuccess(w, r, server.tables[tableNum].Game)
 }
 
 // Handler for /game/players/flag
@@ -728,13 +881,13 @@ func (server *Server) handleGameTeamsPatch(w http.ResponseWriter, r *http.Reques
 }
 
 // Handler for /game/fargo-hot-handicap.
-func (server *Server) handleGameFargoHotHandicap(table int) http.Handler {
+func (server *Server) handleGameFargoHotHandicap() http.Handler {
 	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 		switch r.Method {
 		case "OPTIONS":
 			server.HandleOptions(w, r)
 		case "PATCH":
-			server.handleGameFargoHotHandicapPatch(w, r, table)
+			server.handleGameFargoHotHandicapPatch(w, r)
 		default:
 			server.handleError(w, r, http.StatusMethodNotAllowed, ErrEndpointMethodNotAllowed)
 		}
@@ -742,7 +895,24 @@ func (server *Server) handleGameFargoHotHandicap(table int) http.Handler {
 }
 
 // Game fargo-hot-handicap handler for PATCH method.
-func (server *Server) handleGameFargoHotHandicapPatch(w http.ResponseWriter, r *http.Request, table int) {
+func (server *Server) handleGameFargoHotHandicapPatch(w http.ResponseWriter, r *http.Request) {
+	// get params for table numbers from url
+	params := mux.Vars(r)
+
+	// get table number
+	tableNumValue, ok := params["tableNum"]
+	if !ok || len(tableNumValue) < 1 {
+		server.handleError(w, r, http.StatusUnprocessableEntity, ErrInvalidTableNumber)
+		return
+	}
+
+	// convert table number to int
+	tableNum, err := strconv.Atoi(tableNumValue)
+	if err != nil || tableNum > len(server.tables) {
+		server.handleError(w, r, http.StatusUnprocessableEntity, ErrInvalidTableNumber)
+		return
+	}
+
 	// decode the body
 	var body GameFargoHotHandicapPatchBody
 	decoder := json.NewDecoder(r.Body)
@@ -752,7 +922,7 @@ func (server *Server) handleGameFargoHotHandicapPatch(w http.ResponseWriter, r *
 	}
 
 	// Update the game fargo hot handicap option.
-	if err := server.tables[table].Game.SetUseFargoHotHandicap(body.UseFargoHotHandicap); err != nil {
+	if err := server.tables[tableNum].Game.SetUseFargoHotHandicap(body.UseFargoHotHandicap); err != nil {
 		// TODO: Currently all errors return as 500 here, but might not always make sense. Could use errors.Is for this.
 		server.handleError(w, r, http.StatusInternalServerError, err)
 		return
@@ -761,7 +931,7 @@ func (server *Server) handleGameFargoHotHandicapPatch(w http.ResponseWriter, r *
 	// Generate message to broadcast to overlay.
 	message, err := overlay.NewEvent(
 		events.GameEventType,
-		events.NewGameEventPayload(server.tables[table].Game),
+		events.NewGameEventPayload(server.tables[tableNum].Game),
 	).ToBytes()
 	if err != nil {
 		server.handleError(w, r, http.StatusUnprocessableEntity, ErrUnableToBroadcastUpdate)
@@ -773,6 +943,6 @@ func (server *Server) handleGameFargoHotHandicapPatch(w http.ResponseWriter, r *
 
 	// send response
 	server.handleSuccess(w, r, GameFargoHotHandicapPatchResp{
-		UseFargoHotHandicap: server.tables[table].Game.UseFargoHotHandicap,
+		UseFargoHotHandicap: server.tables[tableNum].Game.UseFargoHotHandicap,
 	})
 }
diff --git a/libs/go/api/overlay.go b/libs/go/api/overlay.go
index 3411d64..7f62842 100644
--- a/libs/go/api/overlay.go
+++ b/libs/go/api/overlay.go
@@ -3,9 +3,11 @@ package api
 import (
 	"log"
 	"net/http"
+	"strconv"
 
 	"github.com/codephobia/pool-overlay/libs/go/events"
 	"github.com/codephobia/pool-overlay/libs/go/overlay"
+	"github.com/gorilla/mux"
 )
 
 // OverlayToggleResp is the response from toggling the overlay.
@@ -94,11 +96,11 @@ func (server *Server) handleOverlayGet(w http.ResponseWriter, r *http.Request) {
 }
 
 // Handler for overlay toggle.
-func (server *Server) handleOverlayToggle(table int) http.Handler {
+func (server *Server) handleOverlayToggle() http.Handler {
 	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 		switch r.Method {
 		case "GET":
-			server.handleOverlayToggleGet(w, r, table)
+			server.handleOverlayToggleGet(w, r)
 		default:
 			server.handleError(w, r, http.StatusMethodNotAllowed, ErrEndpointMethodNotAllowed)
 		}
@@ -106,13 +108,30 @@ func (server *Server) handleOverlayToggle(table int) http.Handler {
 }
 
 // Overlay toggle handler for GET method.
-func (server *Server) handleOverlayToggleGet(w http.ResponseWriter, r *http.Request, table int) {
-	hidden := server.tables[table].Overlay.ToggleHidden()
+func (server *Server) handleOverlayToggleGet(w http.ResponseWriter, r *http.Request) {
+	// get params for table numbers from url
+	params := mux.Vars(r)
+
+	// get table number
+	tableNumValue, ok := params["tableNum"]
+	if !ok || len(tableNumValue) < 1 {
+		server.handleError(w, r, http.StatusUnprocessableEntity, ErrInvalidTableNumber)
+		return
+	}
+
+	// convert table number to int
+	tableNum, err := strconv.Atoi(tableNumValue)
+	if err != nil || tableNum > len(server.tables) {
+		server.handleError(w, r, http.StatusUnprocessableEntity, ErrInvalidTableNumber)
+		return
+	}
+
+	hidden := server.tables[tableNum].Overlay.ToggleHidden()
 
 	// Generate message to broadcast to overlay.
 	message, err := overlay.NewEvent(
 		events.OverlayStateEventType,
-		server.tables[table].Overlay,
+		server.tables[tableNum].Overlay,
 	).ToBytes()
 	if err != nil {
 		server.handleError(w, r, http.StatusUnprocessableEntity, ErrUnableToBroadcastUpdate)
@@ -128,11 +147,11 @@ func (server *Server) handleOverlayToggleGet(w http.ResponseWriter, r *http.Requ
 }
 
 // Handler for overlay toggle flags.
-func (server *Server) handleOverlayToggleFlags(table int) http.Handler {
+func (server *Server) handleOverlayToggleFlags() http.Handler {
 	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 		switch r.Method {
 		case "GET":
-			server.handleOverlayToggleFlagsGet(w, r, table)
+			server.handleOverlayToggleFlagsGet(w, r)
 		default:
 			server.handleError(w, r, http.StatusMethodNotAllowed, ErrEndpointMethodNotAllowed)
 		}
@@ -140,13 +159,30 @@ func (server *Server) handleOverlayToggleFlags(table int) http.Handler {
 }
 
 // Overlay toggle flags handler for GET method.
-func (server *Server) handleOverlayToggleFlagsGet(w http.ResponseWriter, r *http.Request, table int) {
-	showFlags := server.tables[table].Overlay.ToggleFlags()
+func (server *Server) handleOverlayToggleFlagsGet(w http.ResponseWriter, r *http.Request) {
+	// get params for table numbers from url
+	params := mux.Vars(r)
+
+	// get table number
+	tableNumValue, ok := params["tableNum"]
+	if !ok || len(tableNumValue) < 1 {
+		server.handleError(w, r, http.StatusUnprocessableEntity, ErrInvalidTableNumber)
+		return
+	}
+
+	// convert table number to int
+	tableNum, err := strconv.Atoi(tableNumValue)
+	if err != nil || tableNum > len(server.tables) {
+		server.handleError(w, r, http.StatusUnprocessableEntity, ErrInvalidTableNumber)
+		return
+	}
+
+	showFlags := server.tables[tableNum].Overlay.ToggleFlags()
 
 	// Generate message to broadcast to overlay.
 	message, err := overlay.NewEvent(
 		events.OverlayStateEventType,
-		server.tables[table].Overlay,
+		server.tables[tableNum].Overlay,
 	).ToBytes()
 	if err != nil {
 		server.handleError(w, r, http.StatusUnprocessableEntity, ErrUnableToBroadcastUpdate)
@@ -162,11 +198,11 @@ func (server *Server) handleOverlayToggleFlagsGet(w http.ResponseWriter, r *http
 }
 
 // Handler for overlay toggle fargo.
-func (server *Server) handleOverlayToggleFargo(table int) http.Handler {
+func (server *Server) handleOverlayToggleFargo() http.Handler {
 	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 		switch r.Method {
 		case "GET":
-			server.handleOverlayToggleFargoGet(w, r, table)
+			server.handleOverlayToggleFargoGet(w, r)
 		default:
 			server.handleError(w, r, http.StatusMethodNotAllowed, ErrEndpointMethodNotAllowed)
 		}
@@ -174,13 +210,30 @@ func (server *Server) handleOverlayToggleFargo(table int) http.Handler {
 }
 
 // Overlay toggle fargo handler for GET method.
-func (server *Server) handleOverlayToggleFargoGet(w http.ResponseWriter, r *http.Request, table int) {
-	showFargo := server.tables[table].Overlay.ToggleFargo()
+func (server *Server) handleOverlayToggleFargoGet(w http.ResponseWriter, r *http.Request) {
+	// get params for table numbers from url
+	params := mux.Vars(r)
+
+	// get table number
+	tableNumValue, ok := params["tableNum"]
+	if !ok || len(tableNumValue) < 1 {
+		server.handleError(w, r, http.StatusUnprocessableEntity, ErrInvalidTableNumber)
+		return
+	}
+
+	// convert table number to int
+	tableNum, err := strconv.Atoi(tableNumValue)
+	if err != nil || tableNum > len(server.tables) {
+		server.handleError(w, r, http.StatusUnprocessableEntity, ErrInvalidTableNumber)
+		return
+	}
+
+	showFargo := server.tables[tableNum].Overlay.ToggleFargo()
 
 	// Generate message to broadcast to overlay.
 	message, err := overlay.NewEvent(
 		events.OverlayStateEventType,
-		server.tables[table].Overlay,
+		server.tables[tableNum].Overlay,
 	).ToBytes()
 	if err != nil {
 		server.handleError(w, r, http.StatusUnprocessableEntity, ErrUnableToBroadcastUpdate)
@@ -196,11 +249,11 @@ func (server *Server) handleOverlayToggleFargoGet(w http.ResponseWriter, r *http
 }
 
 // Handler for overlay toggle score.
-func (server *Server) handleOverlayToggleScore(table int) http.Handler {
+func (server *Server) handleOverlayToggleScore() http.Handler {
 	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 		switch r.Method {
 		case "GET":
-			server.handleOverlayToggleScoreGet(w, r, table)
+			server.handleOverlayToggleScoreGet(w, r)
 		default:
 			server.handleError(w, r, http.StatusMethodNotAllowed, ErrEndpointMethodNotAllowed)
 		}
@@ -208,13 +261,30 @@ func (server *Server) handleOverlayToggleScore(table int) http.Handler {
 }
 
 // Overlay toggle score handler for GET method.
-func (server *Server) handleOverlayToggleScoreGet(w http.ResponseWriter, r *http.Request, table int) {
-	showScore := server.tables[table].Overlay.ToggleScore()
+func (server *Server) handleOverlayToggleScoreGet(w http.ResponseWriter, r *http.Request) {
+	// get params for table numbers from url
+	params := mux.Vars(r)
+
+	// get table number
+	tableNumValue, ok := params["tableNum"]
+	if !ok || len(tableNumValue) < 1 {
+		server.handleError(w, r, http.StatusUnprocessableEntity, ErrInvalidTableNumber)
+		return
+	}
+
+	// convert table number to int
+	tableNum, err := strconv.Atoi(tableNumValue)
+	if err != nil || tableNum > len(server.tables) {
+		server.handleError(w, r, http.StatusUnprocessableEntity, ErrInvalidTableNumber)
+		return
+	}
+
+	showScore := server.tables[tableNum].Overlay.ToggleScore()
 
 	// Generate message to broadcast to overlay.
 	message, err := overlay.NewEvent(
 		events.OverlayStateEventType,
-		server.tables[table].Overlay,
+		server.tables[tableNum].Overlay,
 	).ToBytes()
 	if err != nil {
 		server.handleError(w, r, http.StatusUnprocessableEntity, ErrUnableToBroadcastUpdate)
diff --git a/libs/go/api/routes.go b/libs/go/api/routes.go
index 83e958d..327a5b8 100644
--- a/libs/go/api/routes.go
+++ b/libs/go/api/routes.go
@@ -30,28 +30,16 @@ func (server *Server) InitRoutes() {
 	server.AddRouteToAllVersions("/overlay", server.handleOverlay())
 
 	// overlay/toggle
-	// server.AddRouteToAllVersions("/overlay/toggle", server.handleOverlayToggle())
-	server.AddRouteToAllVersions("/table/1/overlay/toggle", server.handleOverlayToggle(1))
-	server.AddRouteToAllVersions("/table/2/overlay/toggle", server.handleOverlayToggle(2))
-	server.AddRouteToAllVersions("/table/3/overlay/toggle", server.handleOverlayToggle(3))
+	server.AddRouteToAllVersions("/table/{tableNum}/overlay/toggle", server.handleOverlayToggle())
 
 	// overlay/toggle/flags
-	// server.AddRouteToAllVersions("/overlay/toggle/flags", server.handleOverlayToggleFlags())
-	server.AddRouteToAllVersions("/table/1/overlay/toggle/flags", server.handleOverlayToggleFlags(1))
-	server.AddRouteToAllVersions("/table/2/overlay/toggle/flags", server.handleOverlayToggleFlags(2))
-	server.AddRouteToAllVersions("/table/3/overlay/toggle/flags", server.handleOverlayToggleFlags(3))
+	server.AddRouteToAllVersions("/table/{tableNum}/overlay/toggle/flags", server.handleOverlayToggleFlags())
 
 	// overlay/toggle/fargo
-	// server.AddRouteToAllVersions("/overlay/toggle/fargo", server.handleOverlayToggleFargo())
-	server.AddRouteToAllVersions("/table/1/overlay/toggle/fargo", server.handleOverlayToggleFargo(1))
-	server.AddRouteToAllVersions("/table/2/overlay/toggle/fargo", server.handleOverlayToggleFargo(2))
-	server.AddRouteToAllVersions("/table/3/overlay/toggle/fargo", server.handleOverlayToggleFargo(3))
+	server.AddRouteToAllVersions("/table/{tableNum}/overlay/toggle/fargo", server.handleOverlayToggleFargo())
 
 	// overlay/toggle/score
-	// server.AddRouteToAllVersions("/overlay/toggle/score", server.handleOverlayToggleScore())
-	server.AddRouteToAllVersions("/table/1/overlay/toggle/score", server.handleOverlayToggleScore(1))
-	server.AddRouteToAllVersions("/table/2/overlay/toggle/score", server.handleOverlayToggleScore(2))
-	server.AddRouteToAllVersions("/table/3/overlay/toggle/score", server.handleOverlayToggleScore(3))
+	server.AddRouteToAllVersions("/table/{tableNum}/overlay/toggle/score", server.handleOverlayToggleScore())
 
 	// web socket connection to telestrator
 	server.AddRouteToAllVersions("/telestrator", server.handleTelestrator())
@@ -74,40 +62,35 @@ func (server *Server) InitRoutes() {
 	// flags
 	server.AddRouteToAllVersions("/flags", server.handleFlags())
 
+	// table/count
+	server.AddRouteToAllVersions("/table/count", server.handleTableCount())
+
+	// table/add
+	server.AddRouteToAllVersions("/table/add", server.handleTableAdd())
+
+	// table/remove
+	server.AddRouteToAllVersions("/table/remove", server.handleTableRemove())
+
 	// table/swap
-	server.AddRouteToAllVersions("/table/1/swap/{newTable}", server.handleTableSwap(1))
-	server.AddRouteToAllVersions("/table/2/swap/{newTable}", server.handleTableSwap(2))
-	server.AddRouteToAllVersions("/table/3/swap/{newTable}", server.handleTableSwap(3))
+	server.AddRouteToAllVersions("/table/{tableNum}/swap/{newTable}", server.handleTableSwap())
 
 	// game
-	server.AddRouteToAllVersions("/table/1/game", server.handleGame(1))
-	server.AddRouteToAllVersions("/table/2/game", server.handleGame(2))
-	server.AddRouteToAllVersions("/table/3/game", server.handleGame(3))
+	server.AddRouteToAllVersions("/table/{tableNum}/game", server.handleGame())
 
 	// game/type
-	server.AddRouteToAllVersions("/table/1/game/type", server.handleGameType(1))
-	server.AddRouteToAllVersions("/table/2/game/type", server.handleGameType(2))
-	server.AddRouteToAllVersions("/table/3/game/type", server.handleGameType(3))
+	server.AddRouteToAllVersions("/table/{tableNum}/game/type", server.handleGameType())
 
 	// game/vs-mode
-	server.AddRouteToAllVersions("/table/1/game/vs-mode", server.handleGameVsMode(1))
-	server.AddRouteToAllVersions("/table/2/game/vs-mode", server.handleGameVsMode(2))
-	server.AddRouteToAllVersions("/table/3/game/vs-mode", server.handleGameVsMode(3))
+	server.AddRouteToAllVersions("/table/{tableNum}/game/vs-mode", server.handleGameVsMode())
 
 	// game/race-to
-	server.AddRouteToAllVersions("/table/1/game/race-to", server.handleGameRaceTo(1))
-	server.AddRouteToAllVersions("/table/2/game/race-to", server.handleGameRaceTo(2))
-	server.AddRouteToAllVersions("/table/3/game/race-to", server.handleGameRaceTo(3))
+	server.AddRouteToAllVersions("/table/{tableNum}/game/race-to", server.handleGameRaceTo())
 
 	// game/score
-	server.AddRouteToAllVersions("/table/1/game/score", server.handleGameScore(1))
-	server.AddRouteToAllVersions("/table/2/game/score", server.handleGameScore(2))
-	server.AddRouteToAllVersions("/table/3/game/score", server.handleGameScore(3))
+	server.AddRouteToAllVersions("/table/{tableNum}/game/score", server.handleGameScore())
 
 	// game/players
-	server.AddRouteToAllVersions("/table/1/game/players", server.handleGamePlayers(1))
-	server.AddRouteToAllVersions("/table/2/game/players", server.handleGamePlayers(2))
-	server.AddRouteToAllVersions("/table/3/game/players", server.handleGamePlayers(3))
+	server.AddRouteToAllVersions("/table/{tableNum}/game/players", server.handleGamePlayers())
 
 	// game/players/flag
 	server.AddRouteToAllVersions("/game/players/flag", server.handleGamePlayersFlag())
@@ -119,9 +102,7 @@ func (server *Server) InitRoutes() {
 	server.AddRouteToAllVersions("/game/teams", server.handleGameTeams())
 
 	// game/fargo-hot-handicap
-	server.AddRouteToAllVersions("/table/1/game/fargo-hot-handicap", server.handleGameFargoHotHandicap(1))
-	server.AddRouteToAllVersions("/table/2/game/fargo-hot-handicap", server.handleGameFargoHotHandicap(2))
-	server.AddRouteToAllVersions("/table/3/game/fargo-hot-handicap", server.handleGameFargoHotHandicap(3))
+	server.AddRouteToAllVersions("/table/{tableNum}/game/fargo-hot-handicap", server.handleGameFargoHotHandicap())
 
 	// tournament
 	server.AddRouteToAllVersions("/tournament", server.handleTournament())
diff --git a/libs/go/api/tables.go b/libs/go/api/tables.go
index 27fb841..2684944 100644
--- a/libs/go/api/tables.go
+++ b/libs/go/api/tables.go
@@ -6,17 +6,95 @@ import (
 
 	"github.com/codephobia/pool-overlay/libs/go/events"
 	"github.com/codephobia/pool-overlay/libs/go/overlay"
+	"github.com/codephobia/pool-overlay/libs/go/state"
 	"github.com/gorilla/mux"
 )
 
+// Handler for /table/count.
+func (server *Server) handleTableCount() http.Handler {
+	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		switch r.Method {
+		case "OPTIONS":
+			server.HandleOptions(w, r)
+		case "GET":
+			server.handleTableCountGet(w, r)
+		default:
+			server.handleError(w, r, http.StatusMethodNotAllowed, ErrEndpointMethodNotAllowed)
+		}
+	})
+}
+
+// Table count handler for GET method.
+func (server *Server) handleTableCountGet(w http.ResponseWriter, r *http.Request) {
+	count := int64(len(server.tables))
+	server.handleSuccess(w, r, &CountResp{
+		Count: count,
+	})
+}
+
+// Handler for /table/add.
+func (server *Server) handleTableAdd() http.Handler {
+	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		switch r.Method {
+		case "OPTIONS":
+			server.HandleOptions(w, r)
+		case "POST":
+			server.handleTableAddPost(w, r)
+		default:
+			server.handleError(w, r, http.StatusMethodNotAllowed, ErrEndpointMethodNotAllowed)
+		}
+	})
+}
+
+// Table add handler for POST method.
+func (server *Server) handleTableAddPost(w http.ResponseWriter, r *http.Request) {
+	count := len(server.tables)
+	server.tables[count+1] = state.NewState(server.db, count+1)
+
+	server.handleSuccess(w, r, &CountResp{
+		Count: int64(len(server.tables)),
+	})
+}
+
+// Handler for /table/remove.
+func (server *Server) handleTableRemove() http.Handler {
+	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		switch r.Method {
+		case "OPTIONS":
+			server.HandleOptions(w, r)
+		case "POST":
+			server.handleTableRemovePost(w, r)
+		default:
+			server.handleError(w, r, http.StatusMethodNotAllowed, ErrEndpointMethodNotAllowed)
+		}
+	})
+}
+
+// Table remove handler for POST method.
+func (server *Server) handleTableRemovePost(w http.ResponseWriter, r *http.Request) {
+	count := len(server.tables)
+
+	// don't allow removal of single table
+	if count == 1 {
+		server.handleError(w, r, http.StatusUnprocessableEntity, ErrRemoveOnlyTable)
+		return
+	}
+
+	delete(server.tables, count)
+
+	server.handleSuccess(w, r, &CountResp{
+		Count: int64(len(server.tables)),
+	})
+}
+
 // Handler for /table/swap.
-func (server *Server) handleTableSwap(table int) http.Handler {
+func (server *Server) handleTableSwap() http.Handler {
 	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 		switch r.Method {
 		case "OPTIONS":
 			server.HandleOptions(w, r)
 		case "GET":
-			server.handleTableSwapGet(w, r, table)
+			server.handleTableSwapGet(w, r)
 		default:
 			server.handleError(w, r, http.StatusMethodNotAllowed, ErrEndpointMethodNotAllowed)
 		}
@@ -24,9 +102,25 @@ func (server *Server) handleTableSwap(table int) http.Handler {
 }
 
 // Table swap handler for GET method.
-func (server *Server) handleTableSwapGet(w http.ResponseWriter, r *http.Request, table int) {
-	// get param for new table num from url
+func (server *Server) handleTableSwapGet(w http.ResponseWriter, r *http.Request) {
+	// get params for table numbers from url
 	params := mux.Vars(r)
+
+	// get table number
+	tableNumValue, ok := params["tableNum"]
+	if !ok || len(tableNumValue) < 1 {
+		server.handleError(w, r, http.StatusUnprocessableEntity, ErrInvalidTableNumber)
+		return
+	}
+
+	// convert table number to int
+	tableNum, err := strconv.Atoi(tableNumValue)
+	if err != nil || tableNum > len(server.tables) {
+		server.handleError(w, r, http.StatusUnprocessableEntity, ErrInvalidTableNumber)
+		return
+	}
+
+	// get new table number
 	newTableValue, ok := params["newTable"]
 	if !ok || len(newTableValue) < 1 {
 		server.handleError(w, r, http.StatusUnprocessableEntity, ErrInvalidTableNumber)
@@ -41,23 +135,23 @@ func (server *Server) handleTableSwapGet(w http.ResponseWriter, r *http.Request,
 	}
 
 	// swap table states
-	server.tables[table].Table = newTable
-	server.tables[table].Game.Table = newTable
-	server.tables[table].Overlay.Table = newTable
+	server.tables[tableNum].Table = newTable
+	server.tables[tableNum].Game.Table = newTable
+	server.tables[tableNum].Overlay.Table = newTable
 
-	server.tables[newTable].Table = table
-	server.tables[newTable].Game.Table = table
-	server.tables[newTable].Overlay.Table = table
+	server.tables[newTable].Table = tableNum
+	server.tables[newTable].Game.Table = tableNum
+	server.tables[newTable].Overlay.Table = tableNum
 
-	server.tables[table], server.tables[newTable] = server.tables[newTable], server.tables[table]
+	server.tables[tableNum], server.tables[newTable] = server.tables[newTable], server.tables[tableNum]
 
 	// update current matches in tournament mode if running
 	if server.challonge.InTournamentMode() {
-		server.challonge.CurrentMatches[table], server.challonge.CurrentMatches[newTable] = server.challonge.CurrentMatches[newTable], server.challonge.CurrentMatches[table]
+		server.challonge.CurrentMatches[tableNum], server.challonge.CurrentMatches[newTable] = server.challonge.CurrentMatches[newTable], server.challonge.CurrentMatches[tableNum]
 	}
 
 	// broadcast out changes for each table
-	changedTables := []int{table, newTable}
+	changedTables := []int{tableNum, newTable}
 
 	for i := 0; i < len(changedTables); i++ {
 		// Generate current game state message.
diff --git a/libs/go/apidocs/overlay-toggle_swagger.go b/libs/go/apidocs/overlay-toggle_swagger.go
index 723a457..ecac796 100644
--- a/libs/go/apidocs/overlay-toggle_swagger.go
+++ b/libs/go/apidocs/overlay-toggle_swagger.go
@@ -2,13 +2,23 @@ package apidocs
 
 import "github.com/codephobia/pool-overlay/libs/go/api"
 
-// swagger:route GET /overlay/toggle overlay OverlayToggle
+// swagger:route GET /table/{tableNum}/overlay/toggle overlay OverlayToggle
 // Toggles showing / hiding of the overlay.
 // responses:
 //   200: overlayToggleResp
 //   422: errorResp
 
-//nolint
+// swagger:parameters OverlayToggle
+type OverlayToggleGetParam struct {
+	// The table number to toggle the overlay on.
+	//
+	// in: path
+	// required: true
+	// example: 1
+	TableNum int `json:"tableNum"`
+}
+
+// nolint
 // Returns updated value for Hidden.
 // swagger:response overlayToggleResp
 type overlayToggleRespWrapper struct {
diff --git a/libs/go/apidocs/table-count_swagger.go b/libs/go/apidocs/table-count_swagger.go
new file mode 100644
index 0000000..a84594a
--- /dev/null
+++ b/libs/go/apidocs/table-count_swagger.go
@@ -0,0 +1,17 @@
+package apidocs
+
+import (
+	"github.com/codephobia/pool-overlay/libs/go/api"
+)
+
+// swagger:route GET /table/count table TableCount
+// The number of current tables in use.
+// responses:
+//   200: tableCountResp
+
+// The number of tables in use.
+// swagger:response tableCountResp
+type TableCountRespWrapper struct {
+	// in: body
+	Body api.CountResp
+}
diff --git a/libs/go/apidocs/table_swagger.go b/libs/go/apidocs/table_swagger.go
new file mode 100644
index 0000000..2ec7477
--- /dev/null
+++ b/libs/go/apidocs/table_swagger.go
@@ -0,0 +1,30 @@
+package apidocs
+
+import (
+	"github.com/codephobia/pool-overlay/libs/go/api"
+)
+
+// swagger:route POST /table/add table TableAdd
+// Add a table to the current count.
+// responses:
+//   200: tableAddCountResp
+
+// The number of tables in use.
+// swagger:response tableAddCountResp
+type TableAddRespWrapper struct {
+	// in: body
+	Body api.CountResp
+}
+
+// swagger:route POST /table/remove table TableRemove
+// Removes a table from the current count.
+// responses:
+//   200: tableRemoveCountResp
+//   422: errorResp
+
+// The number of tables in use.
+// swagger:response tableRemoveCountResp
+type TableRemoveRespWrapper struct {
+	// in: body
+	Body api.CountResp
+}
diff --git a/package-lock.json b/package-lock.json
index d2d85ef..b5d6530 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -28,6 +28,7 @@
                 "@nativescript/angular": "^15.0.0",
                 "@nativescript/core": "~8.4.0",
                 "@ngrx/component-store": "15.0.0",
+                "@ngrx/effects": "^15.0.0",
                 "@ngrx/router-store": "15.0.0",
                 "@ngrx/store": "^15.0.0",
                 "@ngrx/store-devtools": "^15.0.0",
@@ -5518,6 +5519,19 @@
                 "rxjs": "^6.5.3 || ^7.5.0"
             }
         },
+        "node_modules/@ngrx/effects": {
+            "version": "15.0.0",
+            "resolved": "https://registry.npmjs.org/@ngrx/effects/-/effects-15.0.0.tgz",
+            "integrity": "sha512-EfYmGYFF1bNLPCRPnfAZnppT47RPwpprZK6HSQQ1fvV0sA0FYKQilg93ictNhDv+0IhMBZJEbR/hBMYyi4rkBg==",
+            "dependencies": {
+                "tslib": "^2.0.0"
+            },
+            "peerDependencies": {
+                "@angular/core": "^15.0.0",
+                "@ngrx/store": "15.0.0",
+                "rxjs": "^6.5.3 || ^7.5.0"
+            }
+        },
         "node_modules/@ngrx/router-store": {
             "version": "15.0.0",
             "resolved": "https://registry.npmjs.org/@ngrx/router-store/-/router-store-15.0.0.tgz",
@@ -28622,6 +28636,14 @@
                 "tslib": "^2.0.0"
             }
         },
+        "@ngrx/effects": {
+            "version": "15.0.0",
+            "resolved": "https://registry.npmjs.org/@ngrx/effects/-/effects-15.0.0.tgz",
+            "integrity": "sha512-EfYmGYFF1bNLPCRPnfAZnppT47RPwpprZK6HSQQ1fvV0sA0FYKQilg93ictNhDv+0IhMBZJEbR/hBMYyi4rkBg==",
+            "requires": {
+                "tslib": "^2.0.0"
+            }
+        },
         "@ngrx/router-store": {
             "version": "15.0.0",
             "resolved": "https://registry.npmjs.org/@ngrx/router-store/-/router-store-15.0.0.tgz",
diff --git a/package.json b/package.json
index 8fa75a6..842864e 100644
--- a/package.json
+++ b/package.json
@@ -52,6 +52,7 @@
         "@nativescript/angular": "^15.0.0",
         "@nativescript/core": "~8.4.0",
         "@ngrx/component-store": "15.0.0",
+        "@ngrx/effects": "^15.0.0",
         "@ngrx/router-store": "15.0.0",
         "@ngrx/store": "^15.0.0",
         "@ngrx/store-devtools": "^15.0.0",