A high-performance, text-based basketball simulation engine and franchise management hub. This project is built as a production-grade showcase of modern web engineering, featuring an in-memory batch simulation pipeline, context-aware CPU AI general managers, real-time asset optimization trading algorithms, database-backed save states, and stateful client controls. Built by and for Filipino basketball fans!
- Framework: Next.js 16 (App Router)
- Programming Language: TypeScript (Strict compilation, custom domain types)
- Database ORM: Drizzle ORM (Sequential queries, zero raw SQL)
- Database Driver: Neon serverless-postgres (neon-http serverless stateless client)
- State Management: Zustand (Franchise identity and timeline sync)
- Styling: Tailwind CSS 4 (Frosted glassmorphism, responsive tables, custom HSL color palette)
- Icons: Lucide React
To solve serverless driver latency and connection overhead, the simulation engine is architected around an in-memory array state pipeline:
- The Bottleneck: Simulating 7-day regular season blocks or best-of-7 playoff runs creates heavy read-write connection loops when querying and saving database rows sequentially.
- The Solution: Before entering a simulation chunk (in leagueEngine.ts), the engine pulls all active
PlayerandTeamrows into local JS memory state arrays. - In-Memory Sim: The daily CPU front-office AI checks, player injury probability tables, and statistical log generations mutate these local array indices directly.
- Batch Writer: At the conclusion of the simulated week, the engine pushes all modified records back to the database in a single round-trip using chunked
db.batch()queries, bypassing transaction limitations and maximizing IOPS efficiency.
graph TD
A[Start Sim Week] --> B[Fetch Active Rosters to memory]
B --> C[Loop Day 1 to 7]
C --> D[Run CPU AI Engine Signings/Trades]
D --> E[Simulate Daily Games & Stats]
E --> F[Update Injury/Roster States in Memory]
F --> C
C -- Loop Done --> G[Batch Write local state to DB]
G --> H[End Sim Week]
Rather than adding a saveSlotId foreign key to every single table in the database and modifying hundreds of SELECT/UPDATE queries across the app, FBM utilizes a single-table JSON state serialization system:
- Save Operation: saveGameAction queries all records from the 11 active tables (teams, players, games, stats, draft picks, etc.), packages them into a single structured JSON object, and writes it to a designated row in the
save_slotstable. - Load Operation: loadGameAction retrieves the slot's
gameStateJsonpayload, deletes the active tables sequentially in reverse dependency order, and inserts the saved records. - Neon HTTP Chunking: To prevent payload limitations and HTTP serverless request size errors, the insert queries are automatically batched in chunks of 200 records via
chunkedInsert.
sequenceDiagram
participant C as Client (Zustand/UI)
participant S as Server Action (saveEngine.ts)
participant DB as Neon PostgreSQL
Note over C,S: Save Game Flow
C->>S: saveGameAction(slotName, teamId, day)
S->>DB: Query all 11 active tables
DB-->>S: Active database rows
S->>S: Stringify rows to JSON payload
S->>DB: Insert/Overwrite save_slots row
DB-->>S: Success response
S-->>C: { success: true }
Note over C,S: Load Game Flow
C->>S: loadGameAction(slotId)
S->>DB: Fetch slot.gameStateJson
DB-->>S: JSON payload
S->>DB: Delete active tables (reverse dependency order)
S->>S: Parse payload and partition into chunks of 200
loop Chunked Inserts
S->>DB: Chunk insert to teams, players, games...
end
S-->>C: { success: true, userTeamId, day }
- The Problem: Clicking "Start New Franchise" triggers
resetActiveGameAction, which runs the database seeder to clean and re-populate the tables. Since Drizzle generates random UUID keys for seeded teams, the user's pre-selectedteamIdis deleted, leading to database mismatch crashes. - The Solution: The seeder now returns the array of newly generated teams with their post-seeding IDs. The frontend selector (TeamSelectorClient.tsx) automatically matches the selected team by city/mascot name, mapping the selection to the correct new post-seed
teamIdbefore routing to the dashboard.
FBM implements an authentic name generator powered by static literal pools in names.ts containing 6,000+ entries (exactly 1,000 single-word names per array) to guarantee cultural accuracy:
- Separated Pools: Includes distinct pools for Filipino Local First Names, Second Names, Surnames, and Fil-Am First Names, American Second Names, and Fil-Am Surnames.
- Contamination Controls: Prevents spaces inside the individual name elements (avoiding duplicate compound names like "Alex Paul Lopez" when second name logic is applied).
- Hyphenation Rules: Restricts hyphenation to exactly 1% of the Fil-Am surname pool (e.g.,
"Clarkson-Washington") and 0% for local Filipino players to maintain realistic roster name structures. - Probability Matrix: Local Filipino players have a 40% chance of receiving an optional second name drawn from the local pool, and a 30% chance of receiving a Fil-Am first name.
All default browser confirm and alert dialogs have been replaced with custom stateful overlays styled with Tailwind glassmorphism:
- Safeguards: Intercepts structural actions, such as starting a new franchise (warning about active database resets), deleting save slots, and exiting to the main menu.
- Branding: Uses warning-themed accents (Lucide
AlertTrianglein orange) and danger-themed accents (LucideTrash2/LogOutin red) with frosted backdrops and enter/exit micro-animations.
CPU-managed franchises act independently using a dual-layered, context-aware decision matrix:
- Positional Balance Layer: General GMs review their active rosters against positional requirements (
Guard,Forward,Center). If a team has a positional deficit, they actively scan the free-agent market. - Asset Optimization Layer: AI general managers execute trades using a context audit:
- Cap Clearing: Trading a high-salary player for a cheaper player of the same position who is younger or within 5 OVR points.
- Talent Upgrade: Trading a young asset or draft picks to acquire an OVR upgrade, keeping within the ₱50,000,000 salary cap.
filipinobasketballmanager/
├── drizzle/ # Database migrations output
└── src/
├── app/
│ ├── actions/ # High-performance server actions
│ │ ├── awardsEngine.ts # Regular season & Finals MVP awards
│ │ ├── cpuAiEngine.ts # CPU daily AI trade & signing behaviors
│ │ ├── leagueEngine.ts # Core simulator & game logic calculations
│ │ ├── offseasonEngine.ts # Draft pool generation & roster resets
│ │ ├── offseasonWizard.ts # Contract renewals & player retirement audits
│ │ ├── playoffEngine.ts # Playoff bracket scheduling & fast-forward sims
│ │ ├── saveEngine.ts # Database-backed save/load system
│ │ ├── statsEngine.ts # League stats aggregates & history logs
│ │ └── tradeEngine.ts # Trade Block Finder counter-offers & executions
│ ├── api/ # API endpoints
│ ├── dashboard/ # Franchise Front Office Views
│ │ ├── layout.tsx # Sidebar navigation & topnav header
│ │ └── page.tsx # Active Roster Sheet
│ ├── layout.tsx # Root HTML layout with custom vector favicon
│ └── page.tsx # Franchise Selection Home screen
├── db/ # Drizzle configuration & schema
│ ├── index.ts # Neon HTTP client initialization
│ ├── schema.ts # Database tables definitions
│ └── seed.ts # Culturally authentic league seeder
└── store/ # Client state store
└── useGameStore.ts # Zustand game variables
Ensure you have Node.js installed and a Postgres database instance from Neon.
Create a .env.local file in the root directory and supply your connection string:
DATABASE_URL=postgres://[user]:[password]@[host]/[database]?sslmode=requirenpm installPush tables and seed the database with 30 authentic teams and 450 initial players:
npx drizzle-kit push
npm run db:seednpm run devOpen http://localhost:3000 to select your franchise.
npm run build
npm run start