Skip to content

InbarGazit/conway-gol-api

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Conway's Game of Life — REST API (.NET 8)

An ASP.NET Core Web API that hosts Conway's Game of Life. Boards are persisted to disk so the service can crash and restart without losing state.

Endpoints

Method Path Description
POST /api/boards Upload a new board state. Returns { id }.
GET /api/boards/{id} Get the originally uploaded board (generation 0).
GET /api/boards/{id}/next Get the next state (generation 1).
GET /api/boards/{id}/states/{steps} Get the state steps generations from the initial state.
GET /api/boards/{id}/final?maxAttempts=N Get the final state. Returns 422 Unprocessable Entity if the board does not reach a stable or cyclic conclusion within N generations (defaults to 1000).

Boards are uploaded as a 2D bool[][], where true means alive:

POST /api/boards
{
  "cells": [
    [false, false, false, false, false],
    [false, false, false, false, false],
    [false, true,  true,  true,  false],
    [false, false, false, false, false],
    [false, false, false, false, false]
  ]
}

The grid is finite and bounded (cells outside the grid are dead). Final-state queries detect both stable boards (period 1) and cyclic boards (any period > 1) — on a finite grid every Game-of-Life run eventually cycles, so reaching maxAttempts without finding one means we just need a larger budget.

Architecture

GameOfLife.Api/
├── Controllers/
│   └── BoardsController.cs       <- the four required endpoints
├── Data/
│   ├── AppDbContext.cs           <- EF Core DbContext
│   └── BoardEntity.cs            <- persisted record
├── Dtos/
│   └── Dtos.cs                   <- request/response shapes
├── Services/
│   ├── GameOfLifeEngine.cs       <- pure Game-of-Life rules + cycle detection
│   ├── GameOfLifeService.cs      <- glue between persistence and the engine
│   └── BoardSerialization.cs     <- bool[,] <-> bool[][] <-> packed-bits
└── Program.cs                    <- DI/wiring

Three layers, all dependency-injected:

  1. IGameOfLifeEngine — pure functions for NextGeneration, Advance(N), and ComputeFinalState. No I/O, deterministic, trivially unit-testable.
  2. IGameOfLifeService — talks to the database via EF Core, dispatches computation to the engine.
  3. BoardsController — translates HTTP <-> service calls and validates input.

Persistence

Boards are stored in SQLite via EF Core. We persist only the initial uploaded state — every "next/Nth/final" query is computed deterministically from that initial state on demand. This means:

  • All read endpoints are idempotent (no hidden mutation between calls).
  • The state model is trivially safe across restarts: there's nothing to reconcile, just a row per uploaded board.
  • Swapping the provider for Postgres / SQL Server is a one-line change in Program.cs; the rest of the code is provider-agnostic.

Database.EnsureCreated() runs at startup so the schema bootstraps itself the first time the API is launched. For a real production deployment you would replace this with dotnet ef migrations and apply them explicitly.

Cycle detection for the "final state" endpoint

ComputeFinalState runs the simulation step-by-step, hashing each generation (packed bits, base64) and storing hash -> generation in a Dictionary. As soon as a generation's hash is found in the dictionary we know we've entered a cycle — the cycle period is currentGeneration - firstSeenAt:

  • period == 1 -> Stable (board no longer changes; this includes the all-dead case)
  • period > 1 -> Cyclic (oscillator/spaceship behaviour)

If maxAttempts is exhausted without revisiting any state, we return DidNotTerminate, which the controller maps to HTTP 422.

Running the API

Requirements: .NET 8 SDK.

dotnet run --project src/GameOfLife.Api

The API listens on http://localhost:5xxx (the launch port chosen by Kestrel). Swagger UI is available at /swagger in Development.

A SQLite database file gameoflife.db is created next to the API binary on first launch. Stop and restart the process — uploaded boards will still be there.

Quick demo with curl

# 1) create a horizontal blinker
ID=$(curl -s -X POST -H 'Content-Type: application/json' \
  -d '{"cells":[[false,false,false,false,false],[false,false,false,false,false],[false,true,true,true,false],[false,false,false,false,false],[false,false,false,false,false]]}' \
  http://localhost:5099/api/boards | jq -r .id)

# 2) next state (vertical blinker)
curl http://localhost:5099/api/boards/$ID/next

# 3) state 5 generations away
curl http://localhost:5099/api/boards/$ID/states/5

# 4) final state (period-2 cycle)
curl "http://localhost:5099/api/boards/$ID/final?maxAttempts=20"

Tests / proof of completeness

dotnet test

dotnet test runs 32 tests across three test classes:

GameOfLifeEngineTests — Conway rules and patterns

Each of the four Game-of-Life rules is tested on its own:

  • Rule 1 — live cells with < 2 neighbors die (underpopulation)
  • Rule 2 — live cells with 2 or 3 neighbors survive
  • Rule 3 — live cells with > 3 neighbors die (overpopulation)
  • Rule 4 — dead cells with exactly 3 neighbors come alive (reproduction)

Plus the canonical patterns from the Game-of-Life literature:

  • The block is a still life (period 1).
  • The blinker oscillates with period 2 (horizontal <-> vertical).
  • The toad oscillates with period 2.
  • The glider returns to its original shape, shifted one cell down-right, every 4 generations.
  • An empty board stays empty.
  • A diagonal pair (no cell has > 1 neighbor) dies in 1 step → final state is empty.

ComputeFinalState is tested for stable, cyclic, and DidNotTerminate outcomes (the last using the famously chaotic R-pentomino with a deliberately low maxAttempts).

BoardsApiTests — full HTTP integration tests

Spins up the real ASP.NET Core pipeline via WebApplicationFactory<Program> and exercises every endpoint:

  • POST /api/boards returns 201 with an id, rejects ragged/empty input with 400.
  • GET .../next returns the next generation, 404 for unknown ids.
  • GET .../states/{n} returns the Nth generation; rejects negative N with 400.
  • GET .../final returns Stable for the block, Cyclic+period-2 for the blinker, 422 for the R-pentomino with a low maxAttempts, 404 for unknown ids.

PersistenceAcrossRestartTests — survives crash/restart

The most important test for the "should be able to restart/crash and retain state" requirement. It:

  1. Spins up an API instance pointing at a temp SQLite file, creates a board.
  2. Fully disposes the hostWebApplicationFactory<Program> shuts down the entire ASP.NET Core pipeline.
  3. Spins up a brand-new API instance pointing at the same file.
  4. Verifies that GET /boards/{id}, GET /boards/{id}/next, and GET /boards/{id}/final all still work for the original id and produce the same answers.

For an OS-level demonstration you can also kill -9 the running process and restart it with dotnet run — the same board ids continue to work, because they're stored in gameoflife.db on disk.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages