Skip to content

labcabrera/gen-philosophers

Repository files navigation

Gen Philosophers

1. Overview

Gen Philosophers is an experimental genetic tic-tac-toe platform. It models automated players as rule-based agents, runs simulations between them, records movement history, updates fitness, and exposes the results through a REST API and a React frontend.

The project is a TypeScript monorepo with two main applications:

Path Purpose Main stack

gen-api

Backend API, domain model, simulation runner, persistence, and event integration.

NestJS, CQRS, MongoDB, Kafka, Mongoose

gen-front

Browser UI for creating players, editing rules, running simulations, browsing history, and inspecting results.

React, Vite, TypeScript, MUI

The backend listens under /api by default. OpenAPI documentation is available at http://localhost:3000/api/docs when the API is running.

2. What the application does

The application lets users and automated trainers:

  • Create players with explicit rules or with randomly generated rules.

  • Edit a player phenotype, mutation settings, and individual rules from the frontend.

  • Validate rules so generated strategies do not target occupied cells or describe already-finished board states unless invalid rules are explicitly allowed.

  • Schedule simulations between two players for a configurable number of matches.

  • Persist simulation jobs and detailed turn-by-turn movement history.

  • Inspect a simulation history interactively by selecting turns and visualizing the board before and after a move.

  • Organize players into leagues and compute rankings with pluggable strategies.

  • Mutate players using different mutation strategies, including usage-aware mutation based on recent simulation history.

3. Conceptual model

3.1. Board encoding

The game board is a standard 3x3 tic-tac-toe board represented as an array of 9 positions. Cell indexes are ordered from left to right and top to bottom:

0 | 1 | 2
3 | 4 | 5
6 | 7 | 8

Rules use a perspective-normalized board encoding:

Code Name Meaning

1

OWN

A cell containing the current player’s piece.

-1

OPPONENT

A cell containing the opponent’s piece.

0

EMPTY

An empty cell.

2

ANY

Wildcard; any cell state matches.

3.2. Player

A Player is the main aggregate in the players bounded context. It owns:

  • id: player identifier.

  • name: display name.

  • rules: ordered genotype, represented as a list of rules.

  • phenotype: runtime configuration for rule selection and mutation.

  • fitness: score derived from simulation results.

  • version and lastUpdated: metadata used to apply newer fitness updates safely.

3.3. Rule

A Rule describes when a move applies and which board cell should be chosen.

{
  "id": "r-center",
  "pattern": [2, 2, 2, 2, 0, 2, 2, 2, 2],
  "move": 4,
  "weight": 9
}

Rule fields:

Field Required Description

id

Yes

Unique rule identifier inside a player.

pattern

Yes

Array of 9 board codes. The rule matches when every non-ANY position matches the current board from the player’s perspective.

move

Yes

Destination cell index from 0 to 8.

priority

No

Optional numeric priority.

weight

No

Optional non-negative weight used by weighted rule selection.

Rule consistency validation rejects rules whose destination is already occupied in their own pattern and rules whose pattern already contains a terminal winning line. For random player generation, rules are consistent by default. Use allowInvalidRules: true only for experiments that intentionally need invalid rule sets.

3.4. Phenotype

The phenotype configures how a player behaves and evolves.

{
  "selection": "deterministic",
  "selectionMode": "weighted",
  "mutation": {
    "type": "random",
    "rate": 0.05
  }
}

Supported concepts:

  • selection: legacy creation/runtime selection field (deterministic or random).

  • selectionMode: rule application mode (ordered or weighted).

  • mutation.type: random, crossover, fitness-based, or usage-aware.

  • mutation.rate: numeric rate between 0 and 1.

3.5. Simulation

A simulation runs a configured number of tic-tac-toe matches between two players. Each match alternates turns until the game is won or the board is full.

During a turn:

  1. The current board is normalized from the active player’s perspective.

  2. The player evaluates matching rules.

  3. If a valid rule applies, its move is used.

  4. If no rule applies, or the selected rule points to an occupied cell, a random legal move is selected from the available cells.

  5. The movement history entry is persisted with board state, player, rule metadata, chosen cell, and final match outcome.

Simulation results contain wins, draws, losses, score, and average fitness for both players.

3.6. League

A League groups players and computes rankings using a ranking strategy. The current model supports a simple win-rate/fitness-oriented ranking strategy and is designed so additional ranking algorithms can be plugged in.

4. Architecture

4.1. Backend

The backend follows a layered DDD/CQRS structure:

gen-api/src/modules
  players/
    domain/
    application/
    infrastructure/
    interfaces/
  simulations/
    domain/
    application/
    infrastructure/
    interfaces/
  leagues/
    domain/
    application/
    infrastructure/
    interfaces/

Main backend responsibilities:

  • REST controllers under interfaces/controllers.

  • Commands and queries under application.

  • Aggregates, entities, domain services, and events under domain.

  • MongoDB repositories, Kafka adapters, and persistence schemas under infrastructure.

4.2. Frontend

The frontend is a Vite React SPA using MUI components. It provides:

  • Player creation and detail views.

  • Paginated rule visualization.

  • Rule creation/editing dialogs with board-based controls.

  • Simulation scheduling and searchable simulation table.

  • Interactive simulation history viewer.

  • League creation, roster management, and ranking display.

5. REST API summary

Method Path Purpose

POST

/api/players

Create a player with explicit rules or generated rules.

GET

/api/players

List players with pagination and optional filter.

GET

/api/players/:id

Get a player by id.

PATCH / PUT

/api/players/:id

Update player metadata, phenotype, or rules.

DELETE

/api/players/:id

Delete a player.

POST

/api/players/:id/mutate

Mutate a player.

POST

/api/simulations

Schedule and run a simulation.

GET

/api/simulations

Search and list simulation jobs.

GET

/api/simulations/:id

Get simulation status and results.

GET

/api/simulations/:id/history

Browse paginated movement history.

POST

/api/leagues

Create a league.

GET

/api/leagues

List leagues.

POST

/api/leagues/:id/players

Add a player to a league.

DELETE

/api/leagues/:id/players/:playerId

Remove a player from a league.

GET

/api/leagues/:id/ranking

Compute league ranking.

6. Running locally

6.1. Prerequisites

Install:

  • Node.js 20 or newer recommended.

  • npm.

  • Docker and Docker Compose.

6.2. Start infrastructure

From the repository root:

docker-compose up -d

This starts:

  • MongoDB on localhost:27017.

  • Kafka on localhost:9093 from the host.

The API defaults to MongoDB database gen-p and URI mongodb://localhost:27017.

6.3. Start the backend API

cd gen-api
npm install
npm run start:dev

The API will be available at:

Useful environment variables:

Variable Default Description

PORT

3000

HTTP port for the NestJS API.

MONGO_URI

mongodb://localhost:27017

MongoDB server URI.

MONGO_DB

gen-p

MongoDB database name.

EVENT_PUBLISHER

not set

Set to kafka to publish domain events through Kafka.

KAFKA_PUBLISHER_ENABLED

not set

Set to true to enable Kafka publishing.

KAFKA_CONSUMER_ENABLED

not set

Set to true to enable Kafka consumers.

KAFKA_BROKERS

localhost:9092

Comma-separated Kafka broker list used by the backend.

For local HTTP-only development, Kafka consumers and publishers can remain disabled. If enabling Kafka with the provided Compose file from the host, use KAFKA_BROKERS=localhost:9093.

6.4. Start the frontend

In another terminal:

cd gen-front
npm install
npm run dev

The frontend runs at http://localhost:5173. Vite proxies /api to http://localhost:3000, so the default frontend configuration works with the default backend port.

To point the frontend directly to another backend URL:

VITE_API_BASE_URL=http://localhost:3000/api npm run dev

6.5. Quick smoke test

Create two players:

curl -s -X POST http://localhost:3000/api/players \
  -H 'Content-Type: application/json' \
  -d '{
    "name": "Weighted Alice",
    "rules": [
      { "id": "r-center", "pattern": [2,2,2,2,0,2,2,2,2], "move": 4, "weight": 9 },
      { "id": "r-corner", "pattern": [2,2,2,2,2,2,2,2,2], "move": 0, "weight": 1 }
    ],
    "phenotype": {
      "selectionMode": "weighted",
      "mutation": { "type": "random", "rate": 0.05 }
    }
  }' | jq .

curl -s -X POST http://localhost:3000/api/players \
  -H 'Content-Type: application/json' \
  -d '{
    "name": "Ordered Bob",
    "rulesCount": 20,
    "phenotype": {
      "selectionMode": "ordered",
      "mutation": { "type": "random", "rate": 0.05 }
    }
  }' | jq .

List players and copy the generated ids:

curl -s 'http://localhost:3000/api/players?page=0&size=20' | jq .

Run a simulation by replacing the ids:

curl -s -X POST http://localhost:3000/api/simulations \
  -H 'Content-Type: application/json' \
  -d '{
    "playerAId": "<player-a-id>",
    "playerBId": "<player-b-id>",
    "matches": 100
  }' | jq .

Browse simulations:

curl -s 'http://localhost:3000/api/simulations?page=0&size=20' | jq .

Browse movement history:

curl -s 'http://localhost:3000/api/simulations/<simulation-id>/history?page=1&pageSize=50' | jq .

7. Quality checks

Backend:

cd gen-api
npm run type-check
npm run lint
npm run test:unit
npm run build

Frontend:

cd gen-front
npm run type-check
npm run lint
npm run build

8. Repository structure

.
├── docker-compose.yml
├── docs/
├── gen-api/
├── gen-front/
└── specs/

9. Troubleshooting

9.1. The frontend cannot reach the API

Check that the backend is running on port 3000 and that requests use the /api prefix. If the backend uses a different port, start the frontend with VITE_API_BASE_URL.

9.2. MongoDB connection errors

Start infrastructure from the repository root:

docker-compose up -d gen-mongo

Then verify the API environment:

echo "$MONGO_URI"
echo "$MONGO_DB"

9.3. Kafka is not required for basic local use

The core REST API and frontend can run without Kafka consumers enabled. Kafka is used for event publishing and asynchronous simulation request consumption when the corresponding environment variables are enabled.

About

Spec Driven application demonstrating the use of genetic algorithms in a league of philosophers playing noughts and crosses.

Topics

Resources

Stars

Watchers

Forks

Packages

 
 
 

Contributors