About The Project

City Planner is a city planning game about roads. A town grows a little every round, and each building belongs to a colour. Your job is to keep every colour joined into one road network, paid for out of a budget that does not refill on its own.

Two rules do most of the work. Two colours may never share a tile, so every new building is a routing problem on a board with less room than it had last time. And road is stock rather than a purchase. Money buys tiles into an inventory, laying one spends a tile, and lifting one returns the tile but never the money. You can always undo a mistake. You cannot undo having paid for it.

Water and trees refuse road entirely, and a bridge is the only way across. A bridge is a separate layer that sits above the ground, it is allowed only where a road cannot go, and it has to grow out of something you already own. That one piece turns a flat routing puzzle into a question of where the crossings should be, and what they are worth.

What inspired it

City builders where the interesting decision is the layout rather than the menu. I wanted the whole game to be one gesture, drawing a road, and for the difficulty to come from the board filling up rather than from the game speeding up or from numbers being hidden. Everything else in the design was cut back until that gesture was carrying the session on its own.

How it was built

Almost entirely by prompting an AI, across ten sessions.

The stack is deliberately small. It is plain JavaScript with Canvas 2D for the board, HTML and CSS for the interface, and no libraries or frameworks at all. The game ships as a single readable HTML file with its assets beside it, runs from a plain file with no server, and makes no network requests once it is loaded.

The rules live in their own layer that knows nothing about drawing. Everything about connectivity, placement, growth and the economy is decided there, which means the whole game can be driven and tested without a screen. That layer is covered by 93 tests that run headlessly, and a separate suite of 32 tests drives the real interface in a browser, checking gestures, layout and screens.

All the art is generated by Python scripts rather than drawn or imported. There are 704 sprites packed into a single atlas of 2048 by 418 pixels, weighing 40 kilobytes. The blocked cells of every map are traced out of the map picture itself, so where the image shows water the model refuses road. The two cannot drift apart, because they are derived from one source.

The build tool assembles the source modules into that one readable file, and refuses to produce any output at all if the page contains an external URL, a module script, fetch, XMLHttpRequest, a dynamic import or sendBeacon. The finished build makes sixteen network requests while it loads and plays, every one of them to a file sitting inside its own package.

Challenges

A frame loop that stopped permanently was the hardest thing to find. Starting a city assigned the session and then waited for the map picture to load, which left a window one image load wide where the loop had a city to draw and no board to draw it with. The error did not merely drop a frame. It skipped the call that schedules the next one, so the game stopped for good on a blank canvas with nothing in the console. It looked exactly like a rendering bug and was nothing of the sort.

A hairline down the left of the screen took three attempts, because twice I fixed something that was wrong without checking whether it was the thing that was wrong. It was settled by measuring the pixels of a screen recording. A viewport 375 pixels wide was producing a stage 375.2 wide, so its left edge sat half a device pixel inside the screen, and the compositor had no choice but to blend the first column of the canvas with whatever was behind it.

The camera took longer than it should have, over one rule: the view may never show anything that is not part of the map. The first version asked whether the view fits inside the picture, which is a question about a centred view and about nothing else. Fitting and staying are different questions. Pulled all the way back and dragged into a corner, it showed a wide band of empty colour.

The tutorial could strand a beginner completely. Its shop lesson says to buy road tiles, the cart sells them five at a time, and nothing stopped a player buying packs until the money was gone. Two lessons later the script asks for a bridge they cannot afford, cannot skip, and cannot raise the money for, because lifting a road returns the tile and never the cash. The script now declares what it still has to buy and holds exactly that much back. Refused rather than topped up: handing a player money the moment they need it teaches the wrong thing, and the point of the tutorial city is that its rules are the real ones.

What I learned

Build visuals from references, not from descriptions. Every interface and every map I made from prose was rejected, and every one made against a pasted picture was accepted. That happened four separate times before I changed the approach. The tell is when the feedback keeps being about structure rather than about detail.

Watch for conventions that get imported without being checked. The first full sprite set came back in isometric projection, because that is the pixel art convention, for a game whose camera was already top down. It is the same class of mistake as resetting a canvas transform inside a drawing function because that is what one usually does.

Verify the verifier. Late in the build I found that the command I had been using to run the model tests was only importing the suites. A test file registers itself with the harness and never runs under that command, so it reported a cheerful pass for every file no matter what the assertions said. Switching to the real runner immediately surfaced three genuine failures. Every claim that the tests were passing, before that point, had come from a command that ran no assertions at all.

Where it stands, and what is next

What is here is a small prototype. There are four playable cities, a tutorial of forty steps, win and lose states, saves for each city, music and sound effects, and the whole loop from drawing a road through to the city being finished or lost.

Time was the limit, and there is a great deal I could not fit. I have far more planned than this shows. The infrastructure is meant to go well past roads and bridges, into tunnels, raised roads and level crossings, each with its own cost and its own reason to exist. Buildings are meant to differ from one another rather than only in colour, with demands of their own that shape where the routes want to go. I want many more cities, each built around one distinctive constraint rather than being a larger version of the last, and an endless mode underneath all of it.

Built With

Share this project:

Updates

Submission history