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 |
|---|---|---|
|
Backend API, domain model, simulation runner, persistence, and event integration. |
NestJS, CQRS, MongoDB, Kafka, Mongoose |
|
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.
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.
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 | 8Rules use a perspective-normalized board encoding:
| Code | Name | Meaning |
|---|---|---|
|
OWN |
A cell containing the current player’s piece. |
|
OPPONENT |
A cell containing the opponent’s piece. |
|
EMPTY |
An empty cell. |
|
ANY |
Wildcard; any cell state matches. |
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. -
versionandlastUpdated: metadata used to apply newer fitness updates safely.
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 |
|---|---|---|
|
Yes |
Unique rule identifier inside a player. |
|
Yes |
Array of 9 board codes. The rule matches when every non- |
|
Yes |
Destination cell index from 0 to 8. |
|
No |
Optional numeric priority. |
|
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.
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 (deterministicorrandom). -
selectionMode: rule application mode (orderedorweighted). -
mutation.type:random,crossover,fitness-based, orusage-aware. -
mutation.rate: numeric rate between 0 and 1.
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:
-
The current board is normalized from the active player’s perspective.
-
The player evaluates matching rules.
-
If a valid rule applies, its move is used.
-
If no rule applies, or the selected rule points to an occupied cell, a random legal move is selected from the available cells.
-
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.
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.
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.
| Method | Path | Purpose |
|---|---|---|
|
|
Create a player with explicit rules or generated rules. |
|
|
List players with pagination and optional filter. |
|
|
Get a player by id. |
|
|
Update player metadata, phenotype, or rules. |
|
|
Delete a player. |
|
|
Mutate a player. |
|
|
Schedule and run a simulation. |
|
|
Search and list simulation jobs. |
|
|
Get simulation status and results. |
|
|
Browse paginated movement history. |
|
|
Create a league. |
|
|
List leagues. |
|
|
Add a player to a league. |
|
|
Remove a player from a league. |
|
|
Compute league ranking. |
From the repository root:
docker-compose up -dThis starts:
-
MongoDB on
localhost:27017. -
Kafka on
localhost:9093from the host.
The API defaults to MongoDB database gen-p and URI mongodb://localhost:27017.
cd gen-api
npm install
npm run start:devThe API will be available at:
Useful environment variables:
| Variable | Default | Description |
|---|---|---|
|
|
HTTP port for the NestJS API. |
|
|
MongoDB server URI. |
|
|
MongoDB database name. |
|
not set |
Set to |
|
not set |
Set to |
|
not set |
Set to |
|
|
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.
In another terminal:
cd gen-front
npm install
npm run devThe 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 devCreate 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 .Backend:
cd gen-api
npm run type-check
npm run lint
npm run test:unit
npm run buildFrontend:
cd gen-front
npm run type-check
npm run lint
npm run buildCheck 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.
Start infrastructure from the repository root:
docker-compose up -d gen-mongoThen verify the API environment:
echo "$MONGO_URI"
echo "$MONGO_DB"