ElCity is a small, turn-based city builder that runs entirely inside Emacs. The UI is ASCII-based and optimized for terminal Emacs sessions. The core simulation is deterministic and pure, while the UI handles rendering and input.
This is an excercise in implementing the “functional core / imperative shell” architecture in a moderately sized project that with a developed UI. Every tile type is defined through a DSL, with a strong separation between state and effects. Most functions in the core are either pure or pure-ish.
Benefits to this approach:
- easy to debug
- scalable in terms of code: reduced cognitive load on both people and LLMs
- easy UX/UI as state is always localized
- easy to extend (with some discipline)
- easy to autotest
- Emacs 30.1+
- Optional: Eask for dependency management
Clone the repository and add it to your Emacs load path. Example with
use-package:
(use-package elcity :load-path "/path/to/elcity" :commands (elcity-start))
If you are not using use-package, add the directory to load-path
and require the entry point:
(add-to-list 'load-path "/path/to/elcity") (require 'elcity) (elcity-start)
From the project root:
Or from Emacs: M-x elcity-start
- The game is turn-based. Press
nto advance one turn. - Funds increase each turn by: (Population / 2) + (Commercial level + Industrial level).
- City Hall is the source of road connectivity and is unique and non-demolishable.
- Roads are only connected if they trace through other roads to City Hall.
- Power plants provide a Manhattan-radius of 6 tiles.
- Zones grow by 1 level per turn if powered and road-adjacent.
- Zones decay by 1 level per turn if they lose power or road adjacency.
- Maximum zone level is 3.
- Residential (R) supplies Workers in a radius and dislikes Pollution.
- Industrial (I) supplies Goods and Pollution and requires Workers.
- Commercial (C) requires Workers and Goods.
Rselect Residential and place onceCselect Commercial and place onceIselect Industrial and place oncerselect Road and place oncepselect Power plant and place oncehselect City Hall and place onceSPCorRETplace selected tool at cursorddemolish at cursoruundo last actionnadvance one turn- Arrow keys move the cursor
ocycle overlays (goods, polution, connectivity, etc)
This is a simplified snapshot to show the general layout.
Funds: 1000 | Pop: 0 | Income: 0 | Turn: 0 | Overlay: none | Tool: none | Unpowered: 0 | Disconnected: 0 | Polluted: 0
Cursor: (0,0) | Tile: HH City Hall | Level: 0 | Build: N | Demo: N | Unique: Y | Cost: 150 | Pop: 0 | Inc: 0
Legend: R res C com I ind == road Overlay: Pwr Conn Poll Work Goods
Keys: R/C/I zone (select tool) | r road | p power | h city hall | d demolish | n next turn
Place: SPC/RET place selected tool | u undo
Overlay: o cycle (none/power/connectivity/pollution/workers/goods)
Move: arrows
00 01 02 03 04 05 06 07 08 09
+--------------------+
00 |HH==R0..PP..........|
01 |....................|
02 |~~~~~~..............|
+--------------------+
- Start with a custom map by calling
elcity-startwith a list of row strings. - Example call:
(elcity-start '("H=R0" "....")) - Map tokens are defined in tile definitions.
- Canonical tokens include
..,~~,==,PP,HH,R0-=R3=,C0-=C3=,I0-=I3=. - Short aliases are also accepted:
.,~,=,P,H,R,C,I. - Default map rows live in
elcity-maps.el(elcity-map-default-rows). - Default map size is set by
elcity-core-map-widthandelcity-core-map-height.
make testruns ERT tests.make lintruns package lint, checkdoc, and byte compilation.make compilebyte-compiles non-test files.
elcity.elentry point that wires core and UIelcity-core.elpure simulation and state transitionselcity-tiles.eltile definitions and effect metadataelcity-ui.elUI shell and input handlingelcity-maps.elmap presetstest/ERT tests