A SimCity-inspired city builder written entirely in Haskell. The player zones land, lays roads and wires power across a 1920×1080 grid; the simulation then runs on its own, with each citizen holding a home, a workplace and a shop, walking between them along the road network, and leaving town when it runs out of money, food or sleep.
The interesting part is not the game loop — it is that the whole model is written in a design-by-contract style. Every type carries an invariant, every operation a pre and a post, and those same predicates are what the QuickCheck properties check.
You need Stack (which brings its own GHC) and the SDL2 development libraries.
# Debian / Ubuntu
sudo apt install libsdl2-dev libsdl2-ttf-dev
# macOS
brew install sdl2 sdl2_ttfgit clone https://github.com/Tinshea/Sims_City.git
cd Sims_City
chmod +x run.sh test.sh
./run.sh # build and launch the game (first build takes a while)
./test.sh # build and run the test suiteOn Windows, .\run.ps1 and .\test.ps1 do the same — but see the Windows note first: the test suite runs, the game currently does not.
The first build compiles roughly a hundred packages from scratch. Budget 20–40 minutes and a few GB; subsequent builds are incremental.
Every sprite the renderer loads, drawn here on the game's own background tile. Zones are 70×70, roads 30×30, and each zone fills itself with up to four buildings as it gets powered and populated.
The code is laid out as a strict MVC split, with the model kept free of any rendering concern — which is what makes the simulation testable on its own.
flowchart TD
Main["app/Main.hs<br/>game loop, tool panel, HUD"]
KB["Controllers.Keyboard"]
MS["Controllers.Mouse"]
GS["Model.Model<br/>GameState: money, pollution, safety"]
V["Model.Ville<br/>city, citizens, A*, economy"]
S["Model.Shapes<br/>Coord, Forme, Zone, Batiment"]
TX["View.TextureMap"]
SP["View.Sprite / SpriteMap"]
Main --> KB
Main --> MS
Main --> GS
Main --> SP
SP --> TX
GS --> V
V --> S
Model.Ville and Model.Shapes import no SDL at all. Everything above them does.
| Module | Lines | Role |
|---|---|---|
Model/Ville.hs |
686 | City state, citizen life cycle, A* routing, power, pollution, economy |
app/Main.hs |
257 | SDL loop, tool panel, build preview, HUD |
Model/Model.hs |
254 | GameState, build actions, money, tick counter |
Model/Shapes.hs |
194 | Geometric primitives and the zone/building algebra |
View/* |
206 | Texture and sprite maps, drawing |
Controllers/* |
72 | Keyboard and mouse event folding |
This is the spine of the project, and the reason it was written for a functional-programming course rather than a games one. Each type declares what it means for a value to be well formed, and each operation declares what it needs and what it guarantees:
invariantCoord :: Coord -> Bool
invariantCoord (C x y) = x >= 0 && y >= 0
preConstruit :: Ville -> Zone -> ZonId -> Bool
construit :: Ville -> Zone -> ZonId -> Ville
postConstruit :: Ville -> Zone -> ZonId -> Ville -> BoolThe same predicates are then reused as global city properties, so an invariant is stated once and checked from both the unit tests and the generators:
| Property | What it asserts |
|---|---|
prop_routes_connexes |
Every road tile is reachable from every other — the network is one component |
prop_ville_sansCollision |
No two zones overlap |
prop_zones_dijoints |
Zones are pairwise disjoint |
prop_zone_adjacent_route |
Every buildable zone touches a road |
prop_wire_wireorelectrique |
Every wire is adjacent to a plant or to another wire |
A citizen is a three-state machine, and its occupation is a second one nested inside the Habitant state.
stateDiagram-v2
[*] --> Immigrant: spawned when pollution is low,<br/>safety sufficient, population under 100
Immigrant --> Habitant: a residence, a workplace<br/>and a shop are assigned
Habitant --> Emigrant: money, hunger or fatigue hits 0
Emigrant --> [*]: expelled
state Habitant {
[*] --> Travailler
Travailler --> Shopping: hunger < 20
Travailler --> Dormir: fatigue < 20
Shopping --> Travailler: hunger restored
Dormir --> Travailler: rested
}
| Occupation | Trigger | Effect per cycle |
|---|---|---|
Travailler |
default | +$200, −5 hunger, −10 fatigue |
Shopping |
hunger < 20 | walks to its grocery, +20 hunger/tick |
Dormir |
fatigue < 20 | walks to its residence, +20 fatigue/tick |
Move |
destination changed | advances along the path to the target zone |
Citizen state is re-evaluated every 100 ticks; immigrants arrive at roughly 1% of ticks, 1–20 at a time.
Citizens do not walk in straight lines. The road tiles are lifted into a graph — type Graph = Map ZonId [ZonId], built by linking every road to its adjacent roads — and paths are found with A* over that graph, using Manhattan distance between zone centres as the heuristic and a min-priority queue as the frontier:
aStar :: Ville -> ZonId -> ZonId -> Maybe Path
aStar ville start goal =
let graph = createGraph ville
heuristic a b = manhattanDistance (coordZone ville a) (coordZone ville b)
in search graph heuristic start goal| Infrastructure | Size | Cost | Effect |
|---|---|---|---|
| Residential zone | 70×70 | $100 | Up to 4 houses or shacks; collects rent |
| Commercial zone | 70×70 | $100 | 4 groceries; restores citizen hunger |
| Industrial zone | 70×70 | $100 | 4 workshops; pays $200 per work cycle; +10 pollution |
| Road | 30×30 | $10 | Required adjacency for every zone; must connect to the existing network |
| Energy plant | 70×70 | $500 | Powers adjacent zones; +50 pollution |
| Wire | 30×30 | $5 | Carries power from a plant to distant zones |
| Police station | 70×70 | $100 | +1 safety per station within 500px of a residential or commercial zone |
A zone only fills with buildings once it is powered, and isPowered walks the wire chain back to a plant. Money, population, pollution and safety are shown live in the HUD, with pollution and safety colour-coded by severity.
$ ./test.sh
Compiling the model and its tests (no SDL) ...
Shapes Tests
...
Ville Tests
...
QuickCheck Properties
isPowered property [✘]
updateCitizens property [✘]
invariant_creer_imigrant property [✘]
60 examples, 4 failures56 of 60 pass. The four failures are all in the same place, and the cause is worth stating plainly: the Arbitrary instances generate values that violate the project's own invariants. QuickCheck happily produces C {cx = -15, cy = 8} for a coordinate, or RectangleP _ 0 7 for a shape — even though invariantCoord requires non-negative coordinates and invariantForme requires positive dimensions. The properties then fail on inputs the model was never meant to accept, and updateCitizens reaches a partial function (error "Building with BatId … not found") on a citizen whose residence id was invented by the generator.
The fix is in the generators, not in the model: constrain them to satisfy the invariants that are already written down a few lines above.
test.sh/test.ps1deliberately compile onlyModel.Ville,Model.Shapesand the specs rather than callingstack test. Nothing under test imports SDL, so the suite has no reason to require it — and this way it also runs on Windows.
The test suite runs. The game does not, and the blocker sits in the toolchain rather than in this code.
GHC 9.6 on Windows links with clang and ld.lld, while MSYS2 ships SDL2 built for two different environments — neither of which links cleanly against it:
| SDL2 flavour | Failure |
|---|---|
mingw-w64-clang-x86_64-SDL2 (what Stack 3.x looks for) |
ld.lld: error: -exclude-symbols:WinMain is not allowed in .drectve |
mingw-w64-x86_64-SDL2 (GCC build) |
ld.lld: error: undefined symbol: __stack_chk_guard, referenced from libSDL2main.a |
Both are known GHC/SDL2 interactions on Windows, not defects here. The path of least resistance is WSL or a Linux/macOS machine, where the apt/brew line at the top is all that is needed.
To get as far as possible on Windows:
stack exec -- pacman -Sy --noconfirm
stack exec -- pacman -S --noconfirm msys2-keyring # the bundled keyring has expired
stack exec -- pacman -S --noconfirm mingw-w64-clang-x86_64-pkg-config `
mingw-w64-clang-x86_64-SDL2 `
mingw-w64-clang-x86_64-SDL2_ttf
.\test.ps1 # this part worksSims_City/
├── run.sh / run.ps1 # build + launch
├── test.sh / test.ps1 # build + run the suite, without SDL
├── app/Main.hs # SDL loop, tool panel, HUD
├── src/
│ ├── Model/ # Ville, Model, Shapes — no SDL, fully testable
│ ├── View/ # TextureMap, SpriteMap, Sprite
│ └── Controllers/ # Keyboard, Mouse
├── test/ # HSpec specs + QuickCheck properties
├── assets/ # sprites (BMP) and the AVELIRE font
├── package.yaml # hpack manifest; minijeu.cabal is generated from it
└── stack.yaml # resolver lts-22.7 (GHC 9.6.4)
Rapport_PAF.pdf is the original coursework report.
- Malek Bouzarkouna
- Paul Mekhail
Sorbonne Université — Master STL, Programmation Avancée en style Fonctionnel, 2024.
Distributed under the BSD-3 license; see LICENSE.

