ascitty's README, made a man page by copal-readme-man
| ASCITTY(1) | Copal: the programs in ~/code | ASCITTY(1) |
NAME
ascitty - A city built entirely out of typeable characters, rendered in real time, on a colour terminal and on a Commodore Plus/4 — and a taxi game inside it
DESCRIPTION
A city built entirely out of typeable characters, rendered in real time, on a colour terminal and on a Commodore Plus/4 — and a taxi game inside it. Rays are cast out from the camera every frame and walk the grid front to back; nearby things are larger and brighter, distant ones fade into the dark. There are no 3D models, no textures, no shaders, and no artwork: the 128 shapes it draws with are generated by a function.
Every picture below was made by the program itself, by make shot and make gif, from the build named in its caption. ascitty --version prints the same string.
*Street level in block elements and colour, which is what you actually get. ascitty --shot 520 --size 140x40 --seed 99 --tour --walk --sky 0 --day 0 — v0.8.0, 19 Aug 2026.*
Watch it drive itself
*Eight seconds of make demo, recorded by make gif — v0.8.0, 19 Aug
- 2026.
- Small and short because a GIF is a whole frame every frame; make cast records the same run as an asciinema file, which stays sharp at any size and is a tenth of the bytes.*
Start it and leave it alone and it plays itself. The cab takes a fare, plans a route over the carriageway, drives the right-hand lane to it, pulls up at the circle on the pavement, and does it again. Touch any key and you are driving. There is no menu and no mode to choose: the attract mode and the game are the same program in the same mode, and the only difference between them is whether your hands are on the keyboard. \ hands it back.
make run # the game. It drives itself until you don't let it make demo # the same thing, named for what it does when you leave it make walk # the older tour: a camera on foot instead of in a cab make cast # record it, twice, to build/tour-lo.cast and tour-hi.cast make gif # ...or to docs/media/demo.gif
make cast writes two recordings of the same drive — same city, same seed, same route — at the two ends of what the renderer will do:
| build/tour-lo.cast | 64×20 | a Plus/4-sized window: 1,280 characters a frame |
| build/tour-hi.cast | 200×56 | a full terminal: 11,200 characters a frame, and nine times the detail |
Neither is a different program, a different setting or a different asset set. The city is a height field and the cars are functions, so the picture is sampled at whatever resolution the window gives it: twenty rows of cab get twenty rows of detail and six rows get six. That is the thing worth showing, and a still cannot show it.
Both are forty seconds of driving played back in twenty — the simulation still steps at thirty frames a second, and --speed 2 divides the timestamps in the .cast, so twice the driving arrives in the same length of watching. Gzipped they are 0.7 MB and 4.2 MB.
asciinema play build/tour-lo.cast asciinema play build/tour-hi.cast
*The chase camera, from make demo at --sky 3 — v0.8.0, 19 Aug 2026. The cab’s heading and the camera’s are not the same thing: the boom lags a turn by a few frames, so a slide is watched from outside the spin.*
The walking tour reads the city rather than following a baked path — it probes ahead, turns at junctions, keeps to the middle of the street, and stops to look up at whatever is tallest nearby. The trick that makes it look like a person rather than a dolly is that heading and gaze are separate: the feet go one way while the head turns to watch a tower go past, and the walking never stops to let the look happen. It is deterministic, so a recording of it is reproducible. docs/media/tour-strip.txt is one walk sampled every few seconds.
The features of ASCITTY
It is a raytraced ASCII city with a game in it — a crazy ASCII prototype, played in a terminal, taking itself exactly as seriously as that deserves. Everything below is generated from one seed and driven by one set of rules, which is the point of the list: it is a system, not a pile of features.
Three ways to be in it
It starts in the first one.
| Drive | third person behind a taxi that tracks its nose through town and hangs the tail out at speed, with a fare on the clock |
| Walk | first person, eye height, down the avenues |
| Copter | free flight above the roofline at thirty-two cells a second, looking down |
*The copter, from --copter --haze 1 --sky 5 — v0.8.0, 19 Aug 2026. How far down it looks is worked out from how high it is, how far the haze lets it see and how many rows the frame has, rather than being a fixed tilt — see docs/camera.md.*
The shift
A run is a shift on the clock, and the clock is the whole of the pressure.
| Starting time | 60 s |
| A coin | +2 s, +$1, and three seconds of boost |
| Picking up, and setting down | +12 s each |
| The fare itself | $3 a cell of distance, plus $10 |
| How far away | the pickup is at least a block from the cab, and the drop-off at least a block from the pickup |
| Petrol | −$1 a second, for as long as the shift lasts |
| Combo | consecutive things hit without touching a wall; a car pays 2 × combo, a lamp post pays combo |
Time comes only from working. There is no other source — nothing lying about to be collected, no bonus for driving well. The only way to keep playing is to keep taking fares, which is why the clock never has to be explained.
And the meter runs on petrol as well as on time. The fare pays a fixed amount for the distance and the clock pays nothing, so without a running cost the fastest route and the slowest are worth the same. A trip is profitable to the extent that it was quick and that you picked things up on the way — and the coins strung along it are laid in the lane you should be driving in, so collecting them and keeping right are the same manoeuvre.
The boost is the fourth step of the throttle’s wind-up, for three seconds, spent only while you are on the throttle: a coin taken into a corner is still worth something coming out of it. Holding the pedal is worth three times the base top speed after three seconds; a coin is worth four.
And the clock going does not stop you. It runs past zero into overtime and the takings run past nothing into the red — -14s OT and -$8 on the status line — because a shift you have overrun is one you are working at a loss, which is a state you can drive your way out of. Fares still pay. The alternative, which is what it used to do, was to freeze the simulation on the tick the clock ran out, with the cab stopped mid-corner and the traffic stopped around it and nothing left to do but quit.
The machines
| Length | Mass | Drawn as | |
| The cab | 12 m | 10, and 30 when it is doing the hitting | Taxi, chequer band and roof sign, from any angle |
| Traffic | 9.6 m | 9 | Car end-on, JeepSide or MuscleSide in profile, Wreck once dented |
| A bus | 13.5 m | 40, and 120 in a collision | Bus, BusSide |
| People | — | — | Ped, on the pavements and over the crossings |
The cab ploughs. It weighs three times as much in a collision as it does the rest of the time, which is not physics, it is the game: a taxi that loses half its speed to every saloon it clips cannot cross town, and the fare is on a clock. The car it hit is bounced back and takes the whole separation, and then reverses out of the way for a quarter of a shift. The bus is tripled along with it, because the bus is the thing you cannot plough through.
White lights are coming towards you, red are going away, and bright red is braking — which is the only cue in the frame that says which way a car is pointing, and “am I about to hit that” is a question about direction rather than about position. Your own cab has them too: the chase camera looks at the back of it, so its brake lights are the one thing in the frame that says the car heard the key.
Twelve vehicles and forty-eight people, and the same twelve and forty-eight all shift: when one falls more than about forty cells behind it is picked up and put back down somewhere ahead. A fixed-size pool rather than a streaming world, because a fixed pool is the shape a 64 KB machine can also run.
They keep the right-hand lane, take their direction from the side of the road they are on, ease off for whatever is in front of them, give way to anything crossing from their right, and collide with each other as well as with you. A bus does not care that you hit it.
The street
| Class | Width | |
| Alley | 2 cells, 12 m | service access between buildings; no pavement, no paint |
| Street | 4 cells, 24 m | two lanes each way, or one lane and somewhere to stop |
| Avenue | 6 cells, 36 m | |
| Boulevard | 8–10 cells, 48–60 m | |
| Arterial | 24–32 cells, 144–192 m | one or two per city, running its whole length |
Every one of those numbers doubled, and so did the blocks between them and the map they sit on — 728 cells a side rather than 364, keeping the same count of blocks at twice the size. The car did not change, which is the whole point of the exercise. The cab is two cells long and a street used to be two cells wide: a two-lane street the exact length of the car is one you cannot turn round in, cannot pass anything on, and cannot pull up at a fare on without stopping across both lanes. Relative to the thing driving on them, the roads are now twice as wide.
Ground is Road, Sidewalk, Building, Park, Plaza, Sand, Water, Field, Hedge or Gap, and the difference matters to everything: cars belong on the first, people on most of the rest, fares wait on the pavement, and the walking network is a second map over the same grid so that a pedestrian crosses at the crossing. Three of them stop a car — the sea, the treeline round a field, and the slot between two buildings — and none of those three is a building.
A pavement is drawn in bands from the kerb inwards — kerb, planted verge, paving, and a dark seam at the building line so the wall does not appear to float. Standing on it: LampPost, Signal, Hydrant and Bollard at the kerb, Tree in the verge behind them, Mailbox and Meter on the paving. All of it goes over when you hit it, at a velocity and a lean, and none of it slows the car down — which is the whole appeal.
It does not stop at the kerb
The map is twenty-eight blocks square and the city is sixteen of them. What is in between is the answer to a question the edge of a map always asks:
ring 0-3 downtown - towers, the arterials
ring 4-7 the rest of the city, falling away
ring 8-10 suburb - one-storey houses on wide plots, gardens between
ring 11 farmland - standing crop inside a treeline, a farmhouse in
about half of them
ring 12 the last houses, and no road runs through them
ring 13+ nothing
A city that stops at a kerb with nothing beyond it reads as a diorama on a table. This one thins out instead, so that by the time you have driven far enough to find the edge, the answer to why not further is “there is nothing out there” — which is the true answer and does not need a wall to make it. The last ring has no roads at all: those houses are reached across the fields or not at all.
The farmland is planted rather than mown: every field is standing crop, in rows that run the way the drill went, inside a treeline you cannot drive through, with a gate on each side where a farm track would meet the road. That turns the outer ring into a maze of lanes, which is what farmland is, and it means the map has one edge rather than two — the sea and the treeline are both open ground as far as the height field is concerned, so until they were made solid the cab could be driven out to sea and could cut the corner off the whole map through a field.
The south is a coast — beach, then sea, always the south so that “drive towards the water” is a direction you can learn. And the draw distance runs from 104 cells in soup to 1,300 in the clear - the whole map, sea included - set live with the 0–9 keys, for the same reason all of this is here: from the fields the towers have to be visible, so the way back to the middle is something you can see rather than something you have to remember.
The terminal is the other bottleneck
A frame at 250×98 is 114 KB of escape codes. The renderer produces it in one millisecond and a terminal needs twenty to draw it, so at that size the terminal is what you are watching — and what you are watching it do is fall behind, which looks like bands of the picture updating on different frames. Vertical bands, because a frame is written in row order and a row is one long run: a terminal that gives up part way through leaves the right of the screen showing the frame before.
So the painter keeps the last frame and sends only what changed, with a cursor move to skip the rest and short gaps repainted rather than skipped — a cursor move costs about eight bytes and a character costs one. Measured on a turning camera at 250×98: 114 KB a frame down to 27.
It is checked by replaying what each painter emits into a model of the part of a terminal this program uses, and comparing the screens rather than the escapes. The escapes are deliberately different; the screen must not be.
The distance dissolves
A cell has one colour, so there is no alpha to blend with. The far end of the world used to fade towards black, which is right on a dark sky and wrong on every other one — a tower at the draw distance went black against a pale morning and read as a hole, and the moment it crossed the limit it stopped being drawn at all. That hard edge is the pop-in, and the skyline is where it shows, because the skyline is the part of the frame made entirely of things at the limit.
So the last two fifths of the range is an ordered dither. A 4×4 Bayer threshold on the screen position decides, per cell, whether it is the building or the air in front of the building, and the fraction that goes to air climbs to all of it at the limit. A distant tower arrives as a scattering of its own colour through the sky colour and thickens as you drive at it. Ordered rather than random because a random threshold crawls, and a crawling one boils the whole skyline.
0–9 set how far you can see, counted the way a player thinks about it: 0 is the next block, 9 is the far side of the city. --distance N does the same from the command line.
The ray walk’s step cap has to come from that number and not from a constant of its own, or it becomes a second draw distance — a shorter one, applied to some columns and not others. It was a flat 512 steps, which on the ground is further than anything is ever drawn; from a helicopter, looking down a long shallow ray, the average column stepped 513, so half the frame was hitting the cap, which half changed as the camera moved, and distant buildings flickered in and out of the city. A ray crossing n cells steps at most 2n of them, so twice the draw distance is the figure that can never bind.
The blocks have seams
The lot subdivision used to butt every building against its neighbours, so a block came out as one continuous mass of facade with the joins showing only where the colour happened to change — which on a single-palette downtown block is often nowhere. From the street that reads as one enormous building per block.
A slot is now cut on about a third of the boundaries, and on every boundary where the two neighbours would be different colours: a colour change with no gap in it reads as one building with a paint fault, and a colour change with a slot in it reads as two buildings. The hues are drawn before anything is built so the boundary can be asked about them.
The slots are solid at ground level and empty above it, which is not a compromise but the requirement. A one-cell slot is narrower than the car is long, so a drivable one is a trap and a walkable one is somewhere a fare gets posted that no cab can reach. Both happened: one city went from six fares in five minutes to one, with the cab wedged for a third of the run.
The architecture
Six archetypes, each a rule about what a wall does rather than a model:
| CurtainWall | continuous glass grid, corner to corner, no expressed floors |
| Slab | full-height piers with window slots between them — the zipper |
| Prewar | brick, punched windows, a cornice, a fire escape on the street face |
| Setback | steps inward as it rises: the wedding cake |
| Deco | setback plus a crown, piers carrying past the top floor |
| LowRise | two or three storeys of shopfront |
…crossed with three uses — Commercial, Residential, Civic — which decide the window pattern, the colour, and how the lights come on at night. A twelve-storey office and a twelve-storey block of flats share an archetype and are not the same building.
The sky, and the day
The sky is a gradient, palest at the horizon where the light is and darkening to the zenith, and it turns through twelve phases on a cycle:
| 0 NIGHT | 1 MORNING | 2 AWAKENING | 3 SUNRISE |
| 4 DUST | 5 NOON | 6 AFTERNOON | 7 OVERCAST |
| 8 SUNSET | 9 AFTERGLOW | 10 GLOAMING | 11 DEEP NIGHT |
Dark blue, to grey, to a blue-grey waking up, to gold, to a red dust storm, to a yellow noon, to blue, to a light grey overcast afternoon, to a green sunset, to pink after it, to a dark green closing over, to a darker blue — and round again. Four minutes a turn by default: --day TICKS sets it, --day 0 holds the sky still, and --sky N picks a phase. --sky list prints them.
*--sky 8 --day 0 — v0.8.0, 19 Aug 2026. The green sunset, held still for the picture.*
A phase change does not cross-fade. Two hues cannot be mixed in a palette that gives a cell one colour, and dithering them together would cost a colour escape per cell across half the frame — so the new sky rises, climbing out of the horizon over the first half of a phase and holding for the second. Which is what a sky does anyway.
The city is lit by whatever is overhead: two luminance steps of ambient at noon, one at dawn, none at night. A lit sky over a black street is the one thing a day cycle can get obviously wrong.
A helicopter has no throttle in the sense a car does: it goes forward by tipping the disc forward and putting some of its lift into thrust, so pointing down is how you accelerate. The nose is the accelerator, so holding w pushes it further over and the speed keeps building; letting go levels off and holds the speed, because there is no drag and level flight is what maintaining a speed looks like; s points the nose up and takes the speed away at the same rate the power put it on. The nose takes a second and a half to reach thirty degrees down or fifteen up, and the two are deliberately not mirror images — the nose-over is a commitment you hold for the length of a flight and the flare is a moment.
And the rest of it: a draw distance you set live from 0 to 9, stars 0–8 (they stop being drawn as the sky brightens, which is what happens), and a moon that can be switched off.
There was rain here, and it is gone. Not because it did not work: a character cell is a large pixel, so a raindrop is a large raindrop, and a couple of hundred of them leaning across the frame is a picture of the weather rather than a picture of the city. The city is the thing being drawn.
What the car does
| An engine with a curve | a torque curve through a gearbox: a third of its force at rest, all of it by 66 mph, tapering away above that. 0–60 in about a second, not a third of one |
| A corner you can place | it goes where it points at every speed; the drift is the handbrake’s, not the car’s |
| A held turn that tightens | keep the wheel over and the lock winds on for the next second, a quarter again — the first second is the corner you asked for |
| A corner that widens with speed | radius grows with the square of it, so getting round a junction means slowing down or hanging the tail out |
| Acceleration bobbing | the camera carries the driver’s head on a spring: back under power, forward under braking, still at any constant speed |
| Grip you have to ask to lose | 0.06 to 0.15 of slip through a corner at any speed with your hands off the handbrake, and 0.84 to 0.93 with it |
| A throttle that winds up | hold it and the top speed steps up every second, three times over, each step landing as a shove: 83 mph at one second, 171 at two, 242 at three, settling at 311 — and 414 with a coin. Four plateaus you arrive at and sit on, not one ramp |
| Drag with a squared term | the top speed is found, where the engine and the air balance, rather than set by a clamp |
| A car drawn by a function | not eight rows of art scaled up: paint_car is evaluated at whatever size the car is on screen, so twenty rows of cab get twenty rows of detail |
| A cab you can pick out of traffic | a lit roof sign on a bracket, with TAXI set in it when there is room to set it and a dark bar in a lit box when there is not - which is what writing on an illuminated sign resolves to before you can read it |
| A body with a lit side and a dark one | the flank is a curved panel: it faces you in the middle of the card and turns away at the edges, so the light lands on one side of it and not the other. Which side is the sun’s, and the sun rises in the east and sets in the west, so the highlight walks round the bodywork over a day and the shade stays opposite it |
| An edge that ends rather than stopping | the outermost sixth of the width is at a grazing angle, and a painted panel at a grazing angle is a mirror — so it is drawn in the sky’s colour, through a dither, so the cell is only partly covered and the background comes through. That is the highlight and the anti-aliasing at once: the silhouette used to stop dead on a character boundary in the body’s own colour, which is a staircase of solid blocks |
| A three-box profile | bonnet, cabin, boot, with the glass only in the cabin. Without it a side view is a slab of glass the whole length of the vehicle, which reads as a van whatever colour it is painted |
| …that agrees with itself from the back | the bottom of the backlight is the front of the boot lid, so the rear view has a bright deck between the window and the tail panel, at the height the profile puts it. One table of figures, read by the roofline in profile and by the deck seen edge-on, or the car changes shape as you drive round it |
| A car seen from wherever you stand | the card is split between the end of the car and its flank in the proportions they actually appear in, so a three-quarter view is a boot and a flank side by side, and it moves continuously through every angle instead of flipping between two pictures |
| Three bodies that differ in shape | a jeep is short, tall and glassy; a land yacht is a quarter longer and a fifth lower; a saloon is between them. Fixed per vehicle, so a car does not change shape when the pool recycles it |
| A collision box on the wheels | five eighths of the length, which is the wheelbase — a car’s overhangs are the part you can put past another car without touching it, which is why two of them can turn across each other in a junction |
| Full throttle ploughs | the cab’s collision weight triples with the pedal down. It does not fire other cars into the sky — the impulse between two bodies is set mostly by the lighter of them — it keeps the cab’s own momentum: coasting into a saloon costs it two and a half units a second, flat out it costs one. The bus still wins |
| Windows that reflect the sky | the glass takes the sky’s hue, so it is blue in the afternoon and gold at sunrise |
| Damage that is only paint | it accumulates and it shows; the car never stops working |
| A chase boom that gets out of the way | shortened until it is clear of the wall it would otherwise show you the inside of |
| A wall that turns you rather than throwing you back | a building takes the speed you drove into it, leaves the speed you had along it, and points the car along itself at about 70°/s for as long as you are scraping. It used to rebound at a third of the impact speed, which in an alley is the run-up to the far wall |
The head is the one to watch for. It answers to acceleration and to nothing else, so a hundred and fifty miles an hour down a straight looks exactly like standing still, and the moment you lift off is the moment you feel — three rows of horizon under power, four under braking, and a bob past level and back when the throttle comes off.
The traffic, and the driver in it
Every other car on the road gets the same three controls you do, works them out from what is in front of it, and knows nothing about where it is going.
They drive in lanes. Every car used to ask road::lane for its target and get the same answer, so a dozen of them queued nose to tail on one painted line with two cells of empty carriageway beside them, and any car that could not get past the one in front simply stopped. Each now picks a whole lane within its own half and sits in the middle of it — a lane has a middle for a reason, and interpolating smoothly across the half instead puts cars half a cell from the crown, which is inside the wobble of a car being jostled. Traffic on the correct side of the road: 94 to 96 per cent.
They have their own speeds, so the street has to overtake and give way to itself rather than moving as one block. A bus takes the middle of its half because it does not fit anywhere else.
They give way to the right — the junction rule everywhere that drives on the right — and they have the nerve to go anyway after about two seconds. The rule on its own deadlocks in exactly the way the real one does: four cars arrive at a crossroads, each has somebody on its right, and each waits for the other three forever. Which car goes first is stable per vehicle, so the four take turns in the same order instead of all lunging together. A car that has not moved at all for four seconds backs out and tries again, which is the backstop for every other reason to be stuck.
They arrive from three blocks away. The pool used to recycle a car eight cells out — a car length and a half — so it spent its life materialising a saloon in the next lane while you were looking at it, which is the one thing a traffic system must never do. Cars now appear at or beyond the far end of the street you are on, where the frame is already a haze of distant buildings, and drive towards you the way traffic does. Sixty-four of them rather than twelve, which is the same density over a disc nine times the area.
Steering is a controller, not a rule
Both the cab and the traffic hold their lane with a PD controller: one proportional term on how far off parallel the car is, one on how far off the line, and a derivative that gives lock back in proportion to how fast the car is already coming round.
The derivative is the part that was missing. A controller made only of position terms cannot tell a car heading back to the lane from one sitting still beside it, so it asks for the same lock in both cases and arrives at the line with the wheel still over. That is the weave, and no retuning of the two proportional gains removes it — it makes them either slow or unstable, which is the pair of failures the tuning went through. With damping in, the proportional terms could go back up: the offset term had been divided by speed twice over, so at open-road pace the lane controller was almost pure damping and had no opinion about which lane it was in.
There is no integral term and there should not be one. What an integrator fixes is a steady-state offset from a constant disturbance, and a lane has none: the only persistent offsets here are deliberate — a dodge, an overtake — and an integrator would spend them winding up and then unwind into the kerb on the way out.
The cab aims at the kerb-side lane on a straight and fades back to the middle of its half for a corner. Every movement it makes to pass something is towards the crown, so starting from the kerb means a routine overtake uses the second lane rather than the oncoming one; but the kerb line is the outside of a left-hander, so a cab that held it into a junction had the least room exactly where it needed the most.
The rules underneath
The features above are consequences of six decisions, and the decisions are the actual design.
Nothing is drawn. Not the city, not the font, not the cars — the cars least of all, since paint_car is a function of where you are on the card rather than a picture of one. The 128 shapes the renderer draws with come out of a function too, and on the Plus/4 the same function’s output is installed as the machine’s own character set.
One rule for everybody. The autopilot and your hands give the car the same three controls, and nothing downstream can tell which is which. The cab and the traffic take which half of the road is theirs from the same module — and that module’s two halves, which way does this lane go and where should a car going this way be, are tested against each other on every carriageway cell of four cities, because the whole benefit is that the thing which places cars and the thing which steers them cannot disagree.
Time comes only from working, so pace is the only strategy.
Nothing ends the run at all. The clock used to freeze the whole simulation on the tick it ran out — the cab stopped mid-corner, the traffic stopped around it, and the only thing left to do was quit. That is a scoreboard, not an ending. It now runs past zero into overtime and the takings run past nothing into the red, so a shift you have overrun is one you are working at a loss and can drive your way out of, because fares still pay. No fuel, no damage model, no fail state that is about the vehicle instead of about you.
The measurement decides. Every figure in this README and in docs/ was measured by a test that is still in the tree, and several of them are recorded as worse than what they replaced until somebody worked out why.
It has to fit in 64 KB. Fixed point end to end, fixed-size pools, no allocation per frame, and no dependencies at all — the crate list is the three crates in this repository and nothing else. The Plus/4 is not a port; it is the constraint the whole thing is designed against.
The mode the name is about
The same renderer with --mode ascii, in 7-bit characters and no colour at all:
!""#!!.""==========!!!!...===...|\\\\\\\.....=====......!!!!.......======....===||="!==~~=/= !.##!"###======.==!!!!!...===...\\\\\\\\.....=====...==o!!!!.......==..==....=.=||==!===~~=X !.""!!###===....==!!!!!.........\\\\\\==.....==...=====o!!!!=......==....=...=..||==!="=="\. "...!!##..==....==!!!!!.........\|||====..........=====o!!!!=====..==....==..=..||==!=."===. !...!!....==....==!!!!!.........|||/////..........=====o!!!!=======.=....==.....||==!=.=="/. !.##!!....==..====!!!!!.........////////..........=====o!!!!=======......==...=.||""!=.==.=~ !###!""~~.....====!!!!!.........////////..........=====o!!!!=======.......=...=.||~~"".==.\= !""~!!~~~.....====!!!!!.........////====................!!!!=======........==.=.||~~!~""=./= =~~~!!~"".........!!!!!.........||||=========...........!!!!.....==........==.=.||""!~~~~"/. =~""""###.........!!!!!.........########=====...........!!!!.........=.....==.=.||.=!""~~~\. #...!!###.........!!!!!...===...########=====...........!!!!.........==....==.=.||.=!==.""\. #...!!###...==....!!!!!...===...########=====...........!!!!.........==....==...||==!==.==/. #...!!.##...==....!!!!!...===...########=====...........!!!!.........==....==...||==!=..===. .#..!!.##...==....!!!!!...===...#####HHH##########HHHHH88HHHHHHHHHH..==....==...||"=!=..==#. .""""""""...=HHHHHHHHHHHHHHHHHHHHHHHHHHH##########HHHHH88HHHHHHHHHHHHHH##HHHH##H||::::""""#. :::::H:::HHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH##########HHHHH88HHHHHHHHHHHHHH##HHHH##____::::::::: :::::H:::HHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH##########HHHHH88HHHHHHHHHHHHHH##HHHH##____~~~~~~~~~ @@@@@@@@@@~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~____@@@@@@@@@ """""""++++++++~~@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@~~~~@@@@@@@@@@@@@@@@@@@@~____++++"|""" ........----_____""""""""""++++++++++::~~~~@@@@@@@@@@@@@@@@@@@@@~~~++++++++++""____"""____--
*ascitty --shot 520 --size 92x20 --seed 99 --tour --walk --mode ascii — v0.8.0, 19 Aug 2026. The mode the name is about, and the one that runs anywhere a terminal runs.*
Play it
make run # or: ./target/release/ascitty
You arrive on the pavement beside a waiting cab, in it, with a fare already on the clock — and it pulls away on its own until you touch something.
wasd is the vehicle, wherever you are. Forward, back, and left and right meaning whatever they mean to the thing you are in: the wheel in the cab, a step sideways on foot and in the air. The arrows are the view.
A yellow arrow lies on the road in front of the cab, pointing at the fare — projected through the same arithmetic the ground is drawn with, so it converges with the street it is lying on and swings across the road the way a compass needle laid flat would. It runs from the bottom of the frame to about the middle of the taxi, and its point is orange, because an arrow is symmetrical enough at a glance to be read the wrong way round in the corner of your eye. “Which way now” is a question you ask at ninety miles an hour, and reading a word is not an answer you have time for.
Driving:
| w or ↑ | throttle |
| s or ↓ | brake, then reverse |
| a | steer left |
| d | steer right |
| q | hard left |
| e | hard right |
| space | brake — it stops the car and leaves it stopped |
| z | handbrake |
| ← → | swing the camera round the cab; let go and it centres |
| t | get out and walk |
There are two pairs of steering keys because there are two kinds of corner. a and d are the wheel: lock winds on while you hold them, which is the corner you saw coming. q and e are all of the lock on the frame you press them, which is the one you did not — the junction you were going to miss, the alley you are being pushed into.
The pedals split the same way. s is the brake with reverse behind it, for backing out of things; the space bar is the brake on its own, and it applies whichever pedal opposes the motion, so it brings the car to a stop and leaves it there instead of pulling away backwards the moment it is still.
Hold them. The throttle winds on while the key is down, and the engine has to get the car rolling before it pulls, so pulling away takes about a second and the top of the range takes four. a and w together is a left-hander taken under power, which is what holding two keys at once is for.
That last part wants a terminal that reports key releases — kitty, ghostty, WezTerm and foot do, and the program asks yours at startup. Where it is not on offer a press is held for half a second and topped up by the terminal’s own autorepeat, which is close but is not the same thing: a terminal repeats the most recently pressed key only, so the earlier of two held keys is a coast rather than a hold.
On foot and in the air:
| w s | forward, back |
| a d or ← → | step sideways |
| q e | turn |
| ↑ ↓ | look up and down |
| space z | up, down |
| t | get in the cab |
| c | walk / copter |
Any time: g switches ASCII and block elements, 0–9 set the draw distance (0 is the next block, 9 is the far side of the city), m is the moon, \ hands the camera back to the autopilot, esc quits.
A terminal cannot see a bare Shift — it sends no bytes at all — so z descends where you might expect Shift to.
Options
--seed N which city to generate --mode ascii 7-bit ASCII only — the mode the name is about --mode unicode block elements (default) --color true|16|none --size WxH fix the frame size; without it the frame is the window --distance 0..9 draw distance: 0 the next block, 9 the whole city --haze 0..9 --stars 0..8 --no-moon --day TICKS one turn of the sky (default 7200, four minutes); 0 holds --sky N start at phase N of 12; --sky list names them --drive --copter --walk --demo, --tour let the cab drive itself, taking fares (the default) --play take the wheel from the first frame, with no autopilot --anim play the demonstration and exit --record FILE write the demonstration to an asciinema .cast --gif FILE ...and/or as an animated GIF --frames N how long, for --anim, --record and --gif (default 900) --shot [N] render N frames, print the last as plain text, exit --png FILE write that shot as a picture instead of printing it --bench 200 frames as fast as possible, and report -V, --version which build this is
It is the size of your window
There is no resolution to choose: the frame is the terminal, every cell of it, and it follows the window when you drag it. --size WxH fixes it, which is what the pictures here use so that they come out the same every time.
Following a resize means asking, because there is no signal to wait for without a libc, and the program asks in whichever way your terminal supports: one that answers CSI 18 t answers on the stream its keys already arrive on, for six bytes twice a second; one that does not gets stty size instead. Which of the two you have is settled in the same round trip as the keyboard handshake, at startup. It used to fork stty every single frame.
--shot needs no terminal at all, which is what makes it usable from a Makefile and from CI. Neither do --png and --gif: every picture in this README was made by one of them, at 8×16 pixels a cell, with the renderer’s own glyphs and the Plus/4’s own palette — so they are the output rather than a photograph of it. Both encoders are written here, because the workspace has no dependencies and adding one to write a file format would be a strange place to start.
Which build is this
$ ascitty --version ascitty 0.9.0 (seed 0xa5c1771e)
Releases are tagged v0.9.0 and so on, and every picture in this README says which build drew it. Regenerating them is make shot and make gif; the seed is fixed, so the same tag renders the same frames.
On a Commodore Plus/4
*The .prg booted in VICE and photographed by tools/viceshot.sh — a 40×25 city on a 1.76 MHz 7501. v0.8.0, 19 Aug 2026.*
build/ascitty.prg and build/ascitty.d64 are the real thing: a 40×25 city, in colour, on a 1.76 MHz 7501, with a character set generated on a laptop and baked into the program.
make disk make run4 # boots it in xplus4 make demo4 # boots it and leaves it alone - it drives itself
demo4 is the attract mode: the program walks the streets on its own from boot and stops the moment you touch a key.
It runs at about 1.3 frames a second — measured, not estimated; the method is in docs/plus4.md. The next factor of three is a hand-written assembly inner loop, and it is at the top of the backlog.
| 2026-08-24 | ascitty |