7a55cfe151d73dfe8c3c283f989cbd8f35bd2b28
317
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
7a55cfe151 |
Say that an option's file is one the assembler writes, and check we said it
-S was added without a row in the Assembler Manual, and the usage it printed listed a bare "-S <file>" with no long name and no statement of what the file is for. That is not merely incomplete, it is misleading: "-S <file>" reads just as naturally as "dump the symbols of <file>", and asking for it that way hands the source to -S, leaves nothing positional behind it, and is answered with "No source file specified" on a command line that plainly names a source. The error described the hole the mistake left and hid the mistake. So the usage now prints the long names, says outright that every <file> is a path it writes and the source is the last argument on its own, and ends with a whole example command. When the source is missing and a file-taking option was given, the error says which options take a path to write. The manual gains the -S row it never had, a warning in the same words, and a sentence on what a symbol dump is for. Documenting it twice is how it went wrong once, so docs.sh now settles both against getopt's own option table: every option the assembler takes has a row in the manual and a line in its own usage. Verified with break.sh against the manual row and the usage line separately. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
cdee9acfae |
break.sh believes a suite's exit status, not the shape of its output
It decided whether a suite noticed by grepping its output for a "N passed, M failed" summary line. That is right for the suites that print one, since they print it only when something failed - and impossible for the three that never print one at all. docs, terminal and voyager report in their own words, so a break any of them caught loudly was reported as "NOTHING CAUGHT THE BREAK". That is the one wrong answer the tool exists never to give, and it was turning up in a third place: the header already tells the story of the first two. It made the whole docs suite unverifiable by the harness the project uses to decide whether a check is worth having. Every suite already exits nonzero when it fails, so that is the signal now. The summary line is still printed where a suite keeps a count, and the suite's own last line stands in where it does not. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
d361ea1e46 |
An LFO belongs to its channel, not to the whole device
The two LFOs lived in the Synth, so four channels shared them and whichever patch loaded last owned them for every voice at once. A sound with its LFO switched off silenced the trill under a sound that was still playing - which is what made Lunar Porter's low fuel warning intermittent: the first landing, docking or crash of a run took its trill away, and it was right again next time the machine started. The engine fix went upstream to soundThing and has come back. synth.c and synth.h are re-vendored at 71e3cb2, character for character bar the ASCII transliteration, and now carry two changes: the LFOs moved into the Voice, and synthSyncVoices carries a free LFO's cycle down alongside its rate. That second hunk does nothing here - it only matters to a caller that syncs voices, and this device never does, because syncing would flatten four channels into one instrument. It is taken so the vendored file stays identical in both trees, and it is commented as such. Upstream also found a bug in the original patch, in patchLoad, which is soundThing's own file and does not travel. Downstream the LFO parameter groups 0x60 and 0x70 now read the selected channel like every parameter beside them, so an LFO written to one channel is inaudible on the other three. Everything else about the device is unchanged. Lunar Porter keeps loading each patch immediately before its note, but for the smaller reason that now applies: the bang and the latch share channel three, and a channel used by two sounds has to be told which of them it is about to be. The comment that said otherwise, and the manual's warning about sharing, are rewritten as history rather than as a caveat. Tests/sound.sh's shared-LFO check is inverted to assert the fixed behaviour, with a third leg added: after proving another channel's patch leaves this one alone, it switches this channel's OWN LFO off and requires the pitch to move. Without that, both checks would pass on a device where writing an LFO did nothing at all. Routing either group back to voice 0 is caught. Cost, measured: four channels sounding continuously for 400 seconds of audio takes 5.0 s of wall clock against 4.59 s before, about 9% of total emulator time. Half of that is wasted on voices that cannot sound, since VOICE_COUNT is 8 and there are four channels; recovering it would mean diverging the vendored file, which is not worth it at this price. |
||
|
|
3ca5f193e6 |
The warning's trill, and why it only sometimes came out
An LFO belongs to the DEVICE and not to a channel. There are two of them against four voices, and a patch carries LFO settings the way it carries everything else - so whichever patch was loaded last owns both of them, for every voice at once. The warning's trill is a saw LFO on the pitch. The patches on channel three, the bang and the latch, carry an LFO that is switched off, and they load at the moment they are used. So the first landing, docking or crash of a run took the trill away and left a plain tone, and it was right again next time the machine started. Correct until something unrelated plays is the worst shape a fault can have. The same thing had already happened silently at startup: the instruments were set up in order, so the warning's LFO settings, written last, sat on top of the rumble's and the rumble never had its own at all. So nothing is set up once any more. Each sound loads its patch immediately before its note - forty-odd writes at a moment already making a sound - and is then whatever its patch says, whatever played before it. Measured after a landing: 1056, 660, 516 hertz, then up to 1698 and down again. Two sweeps of the saw, which is the trill. What it does not fix, because it cannot: two sounds overlapping still share the LFOs, so a warning going off mid-burn re-tunes the rumble for as long as it lasts. With two between four that is the device. Three checks at the device level, where the trap can be stated exactly: a routed LFO bends a pitch (184 Hz against the 262 the note asked for), another channel's patch takes it away (272, the note itself), and saying it again gets it back (184). Written up in the Programming Manual beside the LFO mode, since the next program to want two sounds will meet it too. |
||
|
|
5e85356245 |
Every thruster that catches pops, not only the first
A pilot already burning upwards who then adds a sideways thruster has lit an engine, and that is what an engine lighting sounds like. So what is watched is now the SET of thrusters rather than whether any of them is lit: the bits on now that were not on before, which is an exclusive or and an and and no comparison at all. The rumble still asks the old question, because it is the right question for it - struck when the first lights and not again until every one has gone out. Restriking it when a second joins would start its attack over, which is a stutter rather than an engine, so it keeps a flag of its own. Pitches are as tuned by ear: 36 for the pop, 60 for the rumble. Checked by two flights of the same length, one thruster held throughout against one that gains a second in the middle. The rumble is identical in both, so the whole of the difference is the extra pop: peak 11,452 against 15,540, and the recordings diverge at 3.19 seconds, which is the frame the second thruster lights. |
||
|
|
388faafd04 |
A thruster you can hear, and a latch on arriving and leaving
Three patches, and the last of the sounds that were asked for. The thruster bangs when it lights and rumbles while it burns. The rumble is this game's first HELD note: its gate goes down when a thruster lights and does not come up until every one is out. Only the edges matter - a rumble restruck every frame would never get past its own attack, and a bang struck every frame is a buzz - and the condition is the flames': a held button with a dry tank is a pilot doing nothing. Arriving latches two notes quickly. Middle C then the C above for taking hold of the station, the same pair reversed for letting go, and two octaves lower for the ground - the same shape in a different register, because setting down and taking hold are the same kind of event. The second note is PENDING rather than played: at an undocking the pilot is mid-burn, and stopping the world for an eighth of a second to fit a note in would be felt as the controls sticking. Where the game is stopping anyway, runPend simply pumps it out. Four channels for five sounds. The bang and the latch share one, because a lander arriving either arrives or does not, and a crash ends the run. That leaves the thruster its two, which it needs: a held note struck on the same channel as the ignition bang would cut the bang off at the moment it was meant to be heard. ---- A held note outlives the loop that was holding it ---- Landing while the thruster was still down ended the flying and then waited to be told the message had been read, so the frame that would have noticed the button coming up never ran. The engine roared under the verdict and went on roaring until the machine stopped. Anything that stops to wait hushes it now, and forgets last frame with it, so a thruster still held when the waiting ends counts as lighting again. ---- Three checks that had to be rebuilt around the new noise ---- A landing is not silent any more, so the crash is measured against the second BEFORE it rather than against a quiet landing. The dust samples moved eight frames later, because the latch plays first. And the warning check lost its measure twice: counting bursts could not tell a beep from a nag, and measuring total length stopped working the day the thruster got a rumble. It burns, stops, and listens AFTER - warned once there is nothing left, nagging the last beep is still fading. Nought against 5,679. |
||
|
|
6fe898b5fc |
A quarter-tank warning that says it once, and a gauge that keeps saying it
Two halves of the same number. The warning is the moment it happened and the colour is how things stand: it fires on the way down through a quarter of a tank and then holds its peace, and the gauge stays red until a base fills the lander up, which also allows the warning again. Once, because a lander is at its most careful in the last few seconds before it touches, and something repeating in its ear through that is not a warning, it is a distraction. Checked where the fuel actually moves rather than once a frame - the tank only changes in takeFuel and payFuel, so there is nowhere else it can cross a threshold. Channel two, with the crash on three. Separate channels rather than one reused, because a crash while the warning is still sounding should not cut it off, and on a machine with four voices there is no reason to be clever. Half and a tenth are the obvious next thresholds and are deliberately not here: one is enough to find out whether being told at all is welcome. ---- And the crash sound is the one that was designed for it ---- Crash.json, which carries voice_gate itself, so the program's own trigger write is gone - it would have masked a deliberate choice rather than backing one up. The note moved from 24 to 60 by ear, and the comment saying it was low went with it. ---- A check that could not tell a beep from a nag ---- Counting bursts of sound cannot: without the latch the warning fires every tick the tank drops, far faster than the sound decays, so the beeps run into each other and a burst counter sees one long burst either way. It measures the LENGTH now - 26,944 samples against 490,348 - which is the difference between six tenths of a second and ten seconds of it. |
||
|
|
de765267c5 |
Lunar Porter's crash is a designed sound now, not a guessed one
Crash1.json, made in soundThing and converted by SoundPatch. Two noise oscillators fifteen cents apart, a filter opened 11.6 kHz by the modulation envelope, and the LEVEL shaped by that same envelope rather than by the amplitude one - which is exactly the freedom the routed level was for, and not a thing the engine could express a week ago. It peaks at 10,501 and is gone in about six tenths of a second, inside the eight tenths the pieces are in the air. The guessed one wandered between 1600 and 8200 and sounded like a drain. The patch predates the gate field, so the program's own trigger write is what makes it a one-shot - which is the argument for having put it there rather than leaving it to the patch. LowFuel.json is here too, saved from the newer soundThing with the gate and LFO mode in it, and is not wired to anything yet. Both JSONs live beside the tables they produce. The build has never needed soundThing and still does not, but a sound can now be regenerated, or simply read, without going to find the editor first. |
||
|
|
b2ff8d64e5 |
Fold soundThing's changes back down, and expose the two new switches
The three changes that went up came back as part of soundThing, along with two more that they made possible. The engine here is now b73e5c0 character for character, except that em-dashes and arrows in comments are written as ASCII because this tree is ASCII only - a local rule, not an improvement, and not sent up. So synth.h's "what was changed" list is gone. There is nothing to list: what has to be kept current is only that if either copy changes, the other one has to be told. ---- What came back ---- A VOICE CAN END ITSELF. Naming the level's source said what shapes a voice; nothing said what ends one, so the only thing that could ever finish one was a key coming up. A game is nearly all one-shots and not one of them wants its length decided by how long a note was held. Exposed as parameter 0x51: 0 gated, 1 triggered. AND A ONE-SHOT IS THE SAME ONE-SHOT TWICE. A triggered voice re-arms its oscillators, and an LFO can be told to start over with each voice - parameter 3 of either LFO. Both halves are needed and the check proves it: with the LFO left free, two triggered hits still differ. Their note warned that whatever applies a patch to a channel has to set these or they hold synthInit's defaults. Checked: Voyager never calls synthSyncVoices, so their 0001 is a no-op here as they predicted, and nothing reaches into an LFO's phase, so the struct split is safe. ---- What it is for ---- Lander's crash is a triggered voice now, so boomOff is gone. Nothing has to remember to end a bang. SoundPatch learnt voice_levelSource, voice_gate and lfo<N>_mode, which the new soundThing writes - without that it would have refused every patch saved from it, since an unknown field stops the tool on purpose. A patch from before those fields still converts, and says in its own comments that it predates the level routing. Three checks, each seen to fail on its own break: a gated voice still sounding with nothing holding it, a triggered one down to nothing with no gate ever dropped, and two hits identical sample for sample. One test bug worth keeping: the first version of the repeatability check struck the second note while the first was still ringing, so what it found and compared as "the second hit" was a point in the middle of the first one's tail. It now looks for sound after SILENCE rather than sound after an offset. |
||
|
|
a8b6b09a59 |
SoundPatch: design a sound where it can be heard, then convert it
The bang was guessed at directly in bytes and came out as a low gurgle, which is what guessing at bytes gets you: a cutoff of 40 looks small and is 57 Hz, so the filter sweep ended almost shut. Voyager's sound device is soundThing's voice engine with the editor taken off, so a patch designed in soundThing - where there is a screen, a keyboard and a pair of ears - makes the same sound here. What differs is only how it arrives. SoundPatch converts one into the other. Every parameter the device takes is a documented function of a natural value and all of them invert: times are squared into four seconds, cutoff and rate are exponential, depths and detune are centred on 128. It writes a table of a count and that many parameter and value pairs, and playPatch hands it to the device - so every sound after this costs a table and a call rather than forty lines of its own. Two things the conversion has to say out loud. soundThing has no field for the level routing, because there it is always the amplitude envelope, so the table says so explicitly - a channel keeps its patch between notes and a leftover from a previous one would otherwise be carried in. And a field the tool does not recognise STOPS it: a patch format that has moved on would otherwise produce a table that quietly means something else. THE BUILD DOES NOT DEPEND ON IT. soundThing lives in its own repository and is not needed to build anything here; the tables are checked in and the tool is for when a sound is being changed. Lander's crash now plays Kick808 as a stand-in until a bang is designed for it, and the difference is the point: the hand-guessed patch wandered between 1600 and 8200 for eight tenths of a second, and this decays 3492, 2743, 2037, 1515, 1040, 614, 87, nothing. The docs check caught the tool count in two manuals, which is what it is for. |
||
|
|
9ae59bfccb |
A bang for the crash, which is this game's first sound
Noise through a low pass that the modulation envelope shuts as the level falls, so the bright part is only at the front of it: a boom rather than a hiss. Noise because every other waveform here has a pitch, and a pitched bang is a note. The instrument is built at startup the way the tiles are, and a crash only says "this channel, this note". That is what the selector and value registers are for - a patch is twenty odd writes and a note is two - and it is the shape every sound after this one should take. CHANNEL THREE. There are four, and effects count down from the top so that music, if it ever arrives, can take nought and count up and the two never have to negotiate. This is also the first program to drive the sound device while also doing something else; the only other customer is the patch editor, whose whole job is the device. ---- A byte of envelope is not seconds ---- It is squared and scaled to four of them, so the decay first written here was 200 - which is two and a half seconds. Over the eight tenths of a second the pieces are in the air that is not a bang fading, it is the FRONT THIRD of one, and it both sounded and measured as a flat wash of noise. Ninety is about half a second and it fades to silence with time to spare. Two checks, each seen to fail on its own break. The first is against SILENCE - a landing in the same conditions makes no sound at all, so it is measuring the crash and not the machine humming - and the second is that it is louder in its first half than its second, which is the difference the decay was getting wrong. |
||
|
|
0264b19a3d |
Dust on landing, gas on letting go, and a lander that stays let go
One particle system, three uses now: a lander coming apart, dust kicked up by a landing, and gas out of a docking port on release. Where they start, how fast they go, what colour they are and how long they last are arguments; everything else is shared. Dust goes sideways and UP off the lander's feet, because that is where kicked dust goes and there is ground in the way of the rest of it. Gas goes evenly in every direction, because nothing is in the way of a docking port. Neither happens on docking - a dock is a catch and not a touchdown, and there is nothing under it to kick. ---- Ticked from the frame loop, not run in place ---- The explosion can afford to stop the world; there is nothing left to fly. The undocking puff cannot, because it goes off on the frame a thruster is pressed, and freezing a quarter of a second exactly then is felt as the controls sticking. So a burst advances one frame at a time from the main loop, and the two that can afford to wait just pump that same tick until the air is clear. ---- And letting go did not let go ---- Which the puff is what found. A docked lander sits EXACTLY one tile under the station, so releasing upwards moved it towards the station and it docked again on the very next frame: took the fuel again, said so again, and waited to be told the message had been read - which reads as the controls locking up the instant they are used. It has to get clear now before it can take hold again, and the two distances have to differ: docking wants one tile, re-arming wants two. A single distance re-armed on the frame it let go, because a tile is exactly where it was sitting. Three checks, each seen to fail on its own break. The last of them measures HOW FAR the lander has got and not merely that it moved: a sixteen frame pause on release still leaves it climbing, four rows short of a free run, so "it moved" would pass for a stall that has been slept through. |
||
|
|
115efa1fa1 |
A crash takes the lander apart, instead of just saying so
The verdict used to be the whole of it: a line of text and a lander still sitting there in one piece, so somebody watching a recording had to read the words to know what had happened. That is the same problem the flames were for. The lander goes, and both flames with it, and six pieces of it leave in a rough hexagon at its own colour for about a second. Then they go too, rather than hanging over the words. The world is not running while it plays: it is a loop of its own, so nothing else in the program has to know how to be half destroyed. Yellow, which is the lander's own colour, because it IS the lander - and it is only free to use because the lander itself is hidden by then. The check that counts exactly forty pixels of yellow looks at a flying frame. ---- Two mistakes worth keeping ---- The block went in between touchdownCrash and touchdownStop, so a crash fell into the explosion and returned from there - no verdict, no end of run, and the lander sitting there being crashed into the ground again every frame. The linter caught it as a subroutine nothing called walking into, which is exactly what it was. And copyWord goes DP0 to DP1, so setting the pieces off from the lander's position had the pointers the wrong way round: it copied the empty pieces OVER ShipX. Since that is in the view block, it took the lander's own column with it, and the one piece that could be seen drifted out of the top left corner of the screen. Three checks, each seen to fail on its own break. They measure the SPREAD and not the count: two of the six leave the top of the screen on the way, so the count drops from twenty four to sixteen, which is correct and would make an exact count a check that breaks the day a lander crashes somewhere else. |
||
|
|
c408fc6cf6 |
Thruster flames, so a watcher can see what the pilot is doing
Every reading on this screen is a number drawn as a bar - how fast sideways, how fast down, how much sky, how much tank - and all of it says what is happening TO the lander. None of it says what the pilot is doing about it, so somebody watching over a shoulder has to read gauges to work out that a thruster is even lit. A plume hangs off whichever side the engine is pushing from: under the lander to lift, over it to retro, and on the far side from the way it is being pushed sideways, since that is the side the gas leaves. One tile does up and down, because a vertical flip turns one into the other, and a second does the sides, because a flip cannot rotate a tile a quarter turn. Twenty four pixels each, on purpose, so a count of them means something. HELD, NOT FIRED. The engine fires one frame in ten, because that is the tick gravity is applied on, and a flame that honest would be one frame of light six times a second - a fault lamp, not a rocket. What is drawn is the button being down, which is the truthful answer to "is the pilot burning": the tick is how the sum gets done, not what is happening. An empty tank draws nothing, and nor does the retro thruster on the ground, because in both cases the button really is doing nothing. The second of those was a lie the first version told. Red, and that is by elimination again: white is the ceiling warning, cyan the landing pads, magenta the instruments, blue the station, yellow the lander itself - and the lander is counted as exactly forty pixels of yellow, so a yellow flame would have broken it. Three checks, each seen to fail on its own break. Two things the fixtures taught: a lander placed at the world's origin sits BEHIND the two row window, which reads exactly like a flame that is not drawn; and red has to be looked for in a box round the lander rather than a column, because the speed bars go red and one of the four pads is red too. |
||
|
|
418631a221 |
The orbit check is back, and osFileRead says it reads in blocks
Placing a state retired the reason the orbit check was deleted. Reaching a given orbit through the controls takes a sustained burn while holding height, and the phase of that burn against the gravity tick - one frame in ten - decides whether the thruster is seen at all, so two pad files a frame apart fly differently. The old check passed against one disk and failed against another, which is a check measuring the boot time rather than the physics. Placed at eighty sideways it climbs to row 51, falls to row 190, and climbs again to row 20 - and the turning points are BROAD, tens of pixels across, so the samples have nothing like the margin problem the old one had. Three claims: it climbs, it turns over on its own, and it comes round again no lower than the first time. Both halves of the mechanic are separately caught. Without the outward push it sinks and lands and never climbs; without the exchange it climbs away and never comes back, which is the one way trip the whole thing exists to prevent. ---- And osFileRead writes whole blocks, which nothing said ---- A disk is read a block at a time, so a sixteen byte file still puts 256 bytes where it is told to. Reserving exactly the file's length writes over whatever follows - a quiet corruption rather than a refusal, and it looks like a bug somewhere else entirely. It cost an afternoon here: the state buffer sat in front of the view tables, so the program read its state, wiped the numbers every gauge draws from, and left immediately. Said now in services.asm beside the vector and in the CosmOS manual, along with the pattern that works: reserve the length rounded up to the next 256, read into that, and copy the parts wanted where they are wanted. The orbit fixture also needs its own keyboard file. The shared one holds a key down every forty eight bytes for the held-thruster check, which would fly this orbit as well as measure it. |
||
|
|
7e82b64d47 |
A dock that slides into line, and settles squarely on the port
Two things, and the second is the one that was actually wrong. The lander is slid into place at half a pixel a frame rather than put there. A dock is allowed eight pixels out in either direction, so snapping moved it a whole tile in a single frame - a jump, at the very moment the player was being told they had been careful. The worst gap closes in about half a second, which reads as the two of them settling together. And it settles SQUARELY now. It used to come to rest four pixels out however carefully it was flown, because the lander is drawn from half a screen less half a tile - which is what centres an eight pixel lander on the middle - while the station was drawn from half a screen exactly, so its left edge sat where the lander's centre was. Both come off the same origin now, and a gap of nothing puts one exactly above the other. stationGap is factored out along the way. Three callers wanted it: the one that draws the station, the one that decides whether it can be docked with, and now the one that slides the lander in under it. The sideways ease goes through that wrapped gap rather than the raw positions, because a dock made either side of the moon's seam has a raw difference of most of a moon. ---- And the fixtures moved to frame 400 ---- A dock waits to be told its message has been read, and reading a state file costs a disk read, so a placed run starts a good deal later than a plain one. The acknowledgement was at frame 100 and stopped working the day the state file arrived: the program had not reached the dock yet and the press went by unheard, which shows up as every sprite missing and reads like a drawing bug. Everything after it is sampled well clear of both ends. Two checks, each seen to fail on its own break - the alignment settles at 4,-8 without the shared origin, and the slide reads 0,-8 the whole way without the easing. Which of the two axes each one actually watches is written down beside them, because it is not the one you would guess. |
||
|
|
1928275f87 |
A state can be placed, which four things were queued behind
Lander reads sixteen bytes from /lander.state if the disk has one and starts from those: position, velocity, where the station is, fuel, and which screen to be in. A disk without the file is the game as it always was, which is every other flight in the suite. It exists because some states cannot be flown to. A successful dock needs the lander alongside the station and matched, and NINETY SIX pad files failed to get there - not for want of trying, but because climbing spends sideways speed, so a lander cannot rise while matched and arrives slower than orbital every time. Reaching it wants two burns and a phase. The orbit check had already been deleted for the same reason, and the strike check was flown on a nineteen frame window, which is the sort of fixture that ends up measuring the boot time rather than the physics. Binary, because that is what a file is on this machine. A tool to build one from readable text is a small job for another day; until then the tests write the bytes with the fields named, which reads plainly enough. ---- The buffer is a whole block, and that is not caution ---- osFileRead lands a file in Data Memory and a file is stored in blocks of 256, so reading sixteen bytes into sixteen bytes of room writes over whatever follows. It did: the first version put the buffer in front of the view tables, and the program read its state, wiped the numbers every gauge draws from, and left immediately - "finished" and back to the prompt, with nothing on the screen to say why. Four checks, each seen to fail on its own break: a state file places the lander (row 192 and twelve cells of gauge, against thirty one with no file), a matched approach docks and is paid once and not once a frame, it then rides the station a tile under it, and the same approach unmatched is a wreck. The flown strike fixture and its narrow window are gone. |
||
|
|
804dd3040e |
Docking, and a ceiling that is no longer the old screen's edge
The station can be docked with: close enough, and slow enough RELATIVE TO IT, or it is a wreck. Its speed is orbital speed, which is what the marks on the drift bar point at, so the instrument for this was on the screen before there was anything to dock with. A dock pays eighty units of fuel, once and not once a frame, and holds the lander a tile under the station until a thruster lets go - checked before the speeds are put back, or a burn would be wiped on the frame it was made and the lander could never leave. ---- And the ceiling went up, which is what made it work ---- Sixty four pixels was the whole of the sky a forty column screen had over the world's origin. It was never a fact about the world, it was a fact about the view, and the wide one starts twenty four rows higher: a lander stopped at the old line was stopped a long way short of the top of its own picture for no reason it could see. It is 192 pixels now, the top of the wide view. The station went from four rows up to twelve - about two thirds of the way from the ground to the ceiling. At four it sat exactly where anything climbing away from the surface had to pass, and being run down there is not a hazard, it is a toll: ELEVEN of this suite's flights were being run down as collateral, including the ceiling check, which has to climb past it to reach the ceiling at all. Moving both fixed all eleven at once. The height bar divides by sixty four rather than thirty two, because the band it describes is 368 pixels now and half a pixel of bar to a pixel of sky would stand 184 tall and run off the top of the forty column screen it is drawn beside. ---- What is checked, and what is written down instead ---- The wreck is flown. The fixture is narrow - the window measured 496 to 514 frames of climb and it sits at 505 - and that is said in place, along with the instruction to re-measure before believing the code is broken. It was not always narrow: at the old altitude any climb from 135 to 300 frames struck. Narrow is the right way round, because it means the station is hard to blunder into. The successful dock is NOT flown, and ninety six pad files failed to find it. That is not the search's fault: climbing spends sideways speed, so a lander cannot rise while matched. It is measured working instead - tank 100 to 180, message up, and the pair holding together one tile apart for two hundred and forty frames. |
||
|
|
922511c8a7 |
A retro thruster, at half the strength of the one that lifts
Arresting a rise meant a sideways burn and a wait for the orbit to come back round. That is how a rendezvous really is flown and it is a lot to ask of somebody who has not flown one before, so down now makes the correction directly. HALF THE STRENGTH, deliberately: one sixteenth a tick against two, which is exactly gravity's own step. So it can stop a climb and it can hurry a descent, and it can never turn a landing approach into a crash faster than simply letting go would - the cheap way out of a mistake stays the expensive one. Four against eight on a keyboard, which is the same ratio. It does NOTHING to a lander on the ground, and that guard is load bearing rather than tidy. touchdown has already had its say and returns early once a lander is down, so there is nothing underneath to stop it and no crash to say it happened: measured without the guard, holding it drives the lander clean off the picture and spends a fifth of the tank doing it. ---- And the fixture that hid all of that ---- The first version of the landed check passed just as happily with the guard deleted, and the reason is worth writing down. A lander that has landed WAITS to be told its message has been read, and the keyboard fixture pads with NULs, which are not keys. So the program sat in that loop for ever and the picture was frozen at the moment of touchdown - every thruster held afterwards did nothing, which reads exactly like a working guard. The pad now presses A to get past the wait before the check tests anything, and both the row AND the fuel are read, because an engine that fired and moved nothing would keep the row and one that moved the lander for free would keep the fuel. The altitude bar check upstream leans on that same freeze for its stable end, and its comment said "stays landed" when what it means is "is not running any more". Corrected, because that is the sort of comment that sends the next person looking in the wrong place. |
||
|
|
800f555ffb |
A station in orbit, going round and not yet dockable
S1 of the station: it exists, it orbits, it wraps, it is drawn. Docking is deliberately not here - the point of stopping at this rung is to fly up and find out whether matching a four pixel a frame target feels good before any rules are written about what happens when you reach it. IT NEEDS NO PHYSICS OF ITS OWN. A body at 64 sixteenths is exactly what a circular orbit is under the rules already here: the pull and the swing cancel at that speed at ANY height, because gravity never falls off and the moon is a cylinder that does not rotate. So the station is a position, a constant, and the same fourteen bit wrap the lander uses. That also means there is no prograde or retrograde to choose - nothing privileges a direction, which is what makes a second station going the other way a thing that can exist later. Four rows above the world's origin: off the top of a forty column screen and comfortably inside a wide one, so it is somewhere to go that the zoom is needed to see. It starts half a moon away and a lap is 256 frames, a little over four seconds, so it has to be found but will not stay lost. Its column is the middle of the screen plus how far round it is from the lander, wrapped to fourteen bits - which measures the long way round whenever it is behind, so anything past the half way point becomes a negative offset instead. Off the edge needs no test at all: a sprite's X is signed and sixteen bits, so a station three hundred pixels to the left is asked for at minus a hundred and forty and the device declines. Blue, and that is not taste. White is counted to find the ceiling warning, cyan to find the landing pads, magenta is the instruments and yellow is the lander. Blue is the one ink no check measures, and picking a measured one has broken a test twice already. toPixelsSigned is factored out of showLander, since the station wants the same sign-extended conversion. Four checks, each seen to fail on its own break. Measured going round at 244 pixels in sixty frames, off the far side of the moon, and back on the other edge. |
||
|
|
32559ce872 |
The room a zoom buys goes to the sky, not to the moon
Zooming out drew fifty rows DOWN from the world's origin, which put exactly the same sky on the screen as before with twice as much moon under it. Measured: 47 per cent rock zoomed in and 71 per cent zoomed out. A zoom that shows you more of the thing you cannot fly through is not worth a button. The eighty column screen is fifty rows and the flyable band is thirty - the ceiling is eight rows above the origin and the deepest valley is twenty two below it - so the twenty rows a zoom buys have to go somewhere. They go above. The wide view starts twenty four rows over the origin, the ground sits near the bottom, and the whole band is on the screen: 23 per cent rock instead of 71. Which needed three things. The moon is drawn from row minus twenty four rather than from nought, because rows above the origin are sky by definition and because whatever the shell left in them is otherwise still there. The row origin comes out of the view block like every other screen number. And the lander's own Y moves with the view, which meant making toPixels' answer SIGNED at last: it masks to twelve bits, so a lander above the origin came back as a large positive number rather than a small negative one - harmless while such a lander was off the picture either way, and wrong the moment the view moved up to include it. That is the point of the button. A lander at the ceiling is off the top of a forty column screen, which is where the orbit lives and why the altitude bar had to exist; zoomed out it is at row 149 and you can watch the whole orbit. Four checks, and the proportion is deliberately not on its own: measured alone it passes for a moon floating over a void, because pointing the view back at the origin leaves the rows under the terrain simply never drawn, and black counts as sky. Both breaks that matter went straight through it. What catches them is that the ground has to reach the bottom of the screen and the sky has to be empty - the latter only on a shell scrolled a hundred and eighteen lines deep, since that is what it takes to get anything into the rows the wide view moves into. The tap fixture also moved to frame 100. Fifty rows of moon take longer to draw than twenty five, and a six frame tap at frame thirty now lands before the program is reading a controller at all, which reads exactly like a button that has stopped working. |
||
|
|
437ddf8ebe |
z zooms too, so a keyboard is not shut out of it
B was the only way to swap the view, and the game is meant to be flyable without a controller - the arrows fly it when there is no pad. Somebody without one had no way to zoom at all. Tested ABOVE the pad test rather than beside the arrows, and that is the distinction: which way the lander is flown is a question a controller answers better, so the arrows stand aside for one. How much of the moon is on the screen is not that kind of question, and a player with a pad may still have a keyboard in front of them. No edge to remember here either. The console delivers a key ONCE, which is the whole difference between a key and a held button. The check puts the z forty bytes into the keyboard file, because the console hands over one key a frame: a z two hundred bytes in is a z two hundred frames away, which is past the end of the capture and reads exactly like a key that does nothing. It cost a wrong answer first time. |
||
|
|
a02d701efe |
Two zoom levels on a button, which the device already had
Forty columns and eighty are the same map, the same 8x8 cells and the same engine; only how many of them fit differs. The map is 128 by 128 either way and the moon is exactly 128 columns of it. And the front end scales whatever it is handed by the largest whole number that fits, so 320 by 200 at four times and 640 by 400 at twice fill the same glass. So the two modes ARE two zoom levels and nothing in the video device had to change to get them: forty columns shows under a third of the moon at twice the size, eighty shows nearly two thirds. Out for the orbit, in for the landing, and B says which. What did have to change is every screen coordinate in Lander, because the middle of the screen is 160 on one and 320 on the other. They now live in one block that setView copies over from whichever of two tables matches the mode, so a gauge reads a variable and never has to know which screen it is on. Several coordinates became sixteen bit on the way, since 620 will not go in a byte. follow already read HalfScreen and showLander already writes the world position straight through, so the lander itself needed nothing but its resting column. The moon is redrawn on a swap because it is filled as many rows deep as the mode shows, and a moon drawn 25 deep on a screen showing 50 floats over nothing. The swap is edge triggered. A pad is LEVEL and not an event, so a view that swapped while B was down would swap sixty times a second - which is not a zoom, it is a strobe, and it redraws the whole moon each time. The check for that holds the button for three hundred frames and requires the lander to land where a six frame tap left it; with the edge dropped it ends seventeen pixels adrift, which is the strobe costing it frames. Four checks, each seen to fail on its own break. The README's Lander entry also still described the relief orbit that the previous commit replaced, and now describes the one that is there. |
||
|
|
85029d3b85 |
An altitude bar, and orbital speed marked on the drift bar
The orbit takes the lander off the top of the screen, and the panel only worked while the ground was in sight. Both other bars are rates: they say how fast, and neither says where. The altitude bar is height above the surface underneath, up the left edge, half a pixel of bar to a pixel of sky - the flyable band is about 256 pixels and the screen is 200 tall, so pixel for pixel would run off the top of the very screen it describes. Above the surface rather than above some fixed line, so it reads NOUGHT the moment the lander is down. The marks say where 64 sixteenths is. Without one that number is folklore: a pilot can feel that somewhere around here the falling stops and has no way to see where. The bar is a pixel a sixteenth from the middle at 160, so the marks sit at 224 and at the two pixels before 96, adjacent rather than overlapping - a bar at orbital speed would otherwise hide the thing it is being measured against. Both in magenta. Red and green are taken and they MEAN something here, how fast and whether it can be landed with, and an altitude is neither. White was the first choice and the ceiling check counts white to find its warning, so it read a warning that was never up - the suite caught that. Cyan was the second and it is the colour of a landing pad, which is checked as whole cells of it. groundLevel is factored out of restOnSurface, which had the same sum. Three checks, each seen to fail on its own break. Two flights, because no one flight holds both ends of the bar well: the climb reads 219 pixels and the descent lands and then stays landed. They fly on their own keyboard file - the shared one holds a key down every forty eight bytes for the held-thruster check, so borrowing it flew the lander from the keyboard and the controller at once and turned the gentle descent into a crash. |
||
|
|
c514d5328e |
An orbit that comes back round, out of gravity minus the swing
The relief version could never make one. It only ever SUBTRACTED from gravity, so a lander a little too slow sank for ever and one a little too fast rose for ever - nothing in it could turn a fall around, because nothing in it ever pushed up. There was no periapse to have. Gravity minus the swing outwards has a sign change in it, and that is the whole mechanic. Below orbital speed the pull wins and the lander falls; above it the swing wins and the lander climbs; at 64 sixteenths they cancel and it circles. Falling buys sideways speed and climbing spends it, so a fall carries the lander past orbital and turns into a climb, and the climb pays it back and turns into a fall. The trade is the quarter square multiply, because the rate has to be the PRODUCT of the two speeds. Set by the vertical speed alone it drained a climb to nothing, and any minimum to stop that became a trap the climb spent its way into - measured freezing at 23 with a gate of 24 and at 3 with a gate of 4. With the product in it there is no gate: as the sideways speed goes to nothing the trade stops by itself. Half the product rather than a quarter or the high half. The high half alone is nought below a product of 256, which is a dead patch exactly where the turn begins, and a quarter still ran the lander into the roof before it came round - the whole sky is about 145 pixels. Measured, placed at 80 sideways and left alone: apoapse at -1024 with 59 sideways, periapse at -124 with 71, and round again at 165, 241, 299 and 369 ticks with no sign of decay. A period of about 22 seconds. The ceiling also spends one sideways when it wipes a climb. Without that it was a trap with no way out: the pin wipes the climb, so the trade sees neither fall nor climb and never touches the speed that is pushing the lander up. Measured pinned at the top with 117 sideways, unmoving, for the whole of a four minute flight. Tests/makedisks.sh needed the library path, since Lander now includes math.asm, and the app went from 2,924 bytes to 4,651 - mostly the 1,022 byte table - which is what the listing expectations move for. |
||
|
|
f5642f52b5 |
A ceiling that pins rather than ends
Climbing made a sixteen bit height count down past nought and round to 65535, so a lander that kept going came back through the bottom and hit the ground FROM ABOVE. Two thousand pixels of climb, which a full tank reaches easily. Pinned instead, and told so in the window. Leaving upward is RECOVERABLE - gravity is always there and a lander with fuel can always come back - so ending the run would punish a state the player can fly out of. What kills you out here is running dry a long way from the ground, which is a death somebody flew into rather than one a boundary handed them. TWO DIFFERENT LINES, and both were got wrong before they were got right. The warning covers being AT the ceiling or above it: compared against the ceiling itself, a pinned lander read as back inside the world the next frame and the warning was written and wiped sixty times a second, so it never appeared at all. The pin covers being STRICTLY above it: including the ceiling dragged the height back every frame and the lander could never descend, which is a lid nobody can leave and worse than the wrap. And only the climb is spent, never the fall. Zeroing the speed outright pinned it there for ever - gravity adds once a tick and a clamp running every frame wiped the pull nine times out of ten. The orbit check is gone, and the reason is in video.sh. Every window where the difference showed turned out to be a few frames wide: hold the thruster and both landers are pinned with their climbs spent, ease off and both land and freeze. A version of it passed against one disk and failed against another, which is a check measuring the boot time rather than the physics. Orbit is verified by measurement and said to be so, rather than left looking tested. cosmosLanderDry wants seventy million cycles now instead of forty: it spends the whole tank at the ceiling before it falls the length of the world. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
d896c9d433 |
Multiplying, on a machine with no multiplier
a * b = qs[a + b] - qs[|a - b|] where qs[n] is n squared over four because (a+b)^2/4 minus (a-b)^2/4 is exactly a*b, and the halves the flooring throws away cancel between the two terms. A multiply is two lookups and a subtract. AND THE TABLE IS BUILT BY ADDING, which is the part that makes it fit a machine with no multiplier at all. A table of squares would need squaring to fill; this one does not, because qs[n] = qs[n-1] + n/2, and n/2 goes 0, 1, 1, 2, 2, 3 - a number that steps up on every even n. So the whole thing is a running total and a toggle, and nothing harder than an add appears anywhere in building the thing that does the multiplying. 511 entries of two bytes, because a byte plus a byte reaches 510. That is 1,022 bytes of Data Memory, and it is the price: a kilobyte traded for an operation the hardware has not got. The operands go in memory rather than in registers. B cannot be stored and a product does not fit in one byte anyway, so two in and two out would spend more instructions shuffling than the multiply costs. Checked against nought, the commutation both ways round, a square, and 255 times 255 - which is 0xFE01 and the largest product two bytes hold. The square is the case the identity leans on hardest: the difference term is nought and the whole answer comes out of one entry. Wanted for Lunar Porter's orbit, where the trade between height and speed has to be proportional to vx times vy and could not be. Useful well beyond it: this is the routine every fixed point sum on this machine has been doing without. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
a069ee7a00 |
Orbit, and two bars that answer instead of reporting
Going sideways lifts the moon off you. Not because gravity weakened - because at speed the surface falls away underneath as fast as the lander falls towards it, which is what an orbit is. A GRADIENT OUT OF INTEGER ARITHMETIC. Gravity is one sixteenth of a pixel a tick and there is nothing between that and nothing, so it cannot be scaled down. Instead four times the sideways speed goes into a byte every tick and the tick's gravity is skipped whenever that byte carries: the fraction cancelled is the speed over 64, smoothly, with no multiply and no divide. At four pixels a frame it carries every time. That is the linear approximation; the honest one is the square, and wants a table. It did nothing at all for its first two versions. Once because the relief was a 256th a tick, so orbit wanted a speed no lander would reach; and once because A IS THE HIGH HALF of the shift register, so multiplying by four left the answer in A while the code read B, which is nought. The same trap as the scroll register and the pixel conversion before it. And a bar for the vertical speed beside the one for drift, both GREEN WHILE A LANDING WOULD SURVIVE AND RED WHILE IT WOULD NOT. That turns two numbers into one question - can I put down - and answers it at a glance. WHAT THIS COST: the flown delivery check. A recording is a list of buttons and not a flight, so replaying it under different gravity flies somewhere else; the delivery became a crash two columns short. The fixture is still there and is still a faithful record of what somebody did, and is no longer a record of what happens. That is the standing cost of a flown fixture, and it is worse than the transcript tests dropped earlier: those broke when an output moved, and this breaks whenever a NUMBER moves. Making the delivery reachable without flying - a way to start already carrying, or at a chosen base - is what would fix it properly. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
4b1c3d8e3f |
A missing file is an error, not a core dump
Naming a pad file that is not there printed the error and then said the machine had STARTED. MACHINE_OK is nought and the code returned nought, so the front end ran a machine whose clock had never been set up and divided by it: a typo in a path came out as a floating point exception and a core dump. The trap is two functions in one file with opposite conventions - machineStart returns MACHINE_OK for worked, machineRestart thirty lines up returns 1 for worked - and this copied the nearer one. Both of the returns I added last week had it. Checked now for all three files the replay suite is about, because the same mistake fits all of them, and re-broken to be sure: the check comes back exit 136, which is a signal 8, which is the crash. Found by somebody typing a path that was not there, which is the fourth thing this week that no test would have reached. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
60e0196fe2 |
A delivery, flown by hand and kept
Cyan base to red base: twenty five seconds of steering, recorded with --record-pad and replayed as a test. THE FIRST FIXTURE HERE THAT WAS PLAYED RATHER THAN WRITTEN. It is the only check that a cargo ever reaches anywhere. Several attempts at authoring a flight like it by hand got within two columns and no closer, which is a piloting exercise rather than a test - and the whole reason the recorder exists. What is checked is the FIRST LETTER of what the base answers. Delivered, Loaded, Nowhere and Not are 30, 22, 37 and 37 pixels of white in that cell, so a D is a delivery and nothing else is. Counting the whole message would pass on any message of the same length, and comparing the picture would fail the next time anything about a font changed. It took three flights to get here and two of them were lost to bugs in the recorder: one that recorded the wrong pad, and one that recorded a pad sampled on a different clock from the one the machine read. Both were found by somebody watching a replay and saying it was not what they flew, which nothing in this suite could have said. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
ee77d79780 |
A pad is sampled once a frame, and the recorder writes what was sampled
The live state is written by whatever watches real hardware, on ITS clock: a window polls its keyboard once a HOST frame, which is not a machine frame. Read straight through, that made a pad whose value could change in the middle of a machine frame - breaking the one promise the device makes, that asking twice in a frame gives the same answer both times. The manual said it could not happen and the code allowed it. It also made recordings that were not of the flight. The recorder sampled on a frame boundary and the program read whenever it read, so the two saw different bytes. A replay of that is a DIFFERENT FLIGHT, faithfully reproduced: it flew a lander off the top of the screen that had never gone there, and every check said the replay was deterministic and re-recorded as itself, because it was. Both were true and neither was the point. So the live state is latched once a frame. What the machine reads and what the recorder writes are now the same thing by construction rather than by two clocks happening to agree. Real hardware latches a controller once a frame for the same reason. WHAT IS STILL NOT COVERED: the latch itself. Every recorded pad already changes only on a frame, so the tests cannot tell a latched live pad from an unlatched one - the case that went wrong is the one with a real hand on a real controller, which is the case a headless suite has none of. Said here rather than left to look tested. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
de1857f5f7 |
Record every pad, not the one that happened to be first
The first recording ever made with this came back 1,766 frames of nothing. It recorded pad NOUGHT and the controller was somewhere else - which pad one lands on is an accident of the host, the same accident that made Lunar Porter read all four in the first place - and a flight flown for the purpose was lost to it. So every pad is or-ed into the byte. A demo is a record of what somebody DID, and on a machine one person is playing the number it arrived on is not part of that. It plays back on pad nought, where --pad puts the first file given, and any program that reads more than one pad reads them or-ed anyway for exactly the same reason. --record-pad takes one file now rather than filling pads in turn, because there is nothing left for the second one to mean. The check for it plays a recording on pad ONE with nought holding nothing and requires the bytes back. That is the case that was missing: the round trip was tested and passed, on pad nought, which is the only pad it could not have gone wrong on. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
eb695a3f3b |
--keyboard was a flag that did nothing behind a window
Voyager installed its own input hook after machineStart, which had already installed the keyboard file's - so naming a keyboard file and running the window silently got the window, and the flag said nothing about being ignored. Both together is the combination a demo wants. Recording a flight needs the typing that STARTS it to be the same every time, because a human reaching the shell a moment later shifts every frame of the recording after it - while the flying itself has to come from whatever is actually in somebody's hands. So the window only takes the keyboard when no file was named. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
a163c670d0 |
A demo recorder: --record-pad writes what --pad reads
One byte a frame, in exactly the format the player takes, so a recording needs no conversion and there is no second format to keep in step. That symmetry is the feature, and it makes the strongest form of the claim testable: a recording is made OF a playback, and the bytes coming out have to be the bytes that went in. It exists because some inputs cannot sensibly be written by hand. Flying a lander from one base to another is a few hundred frames of steering that has to arrive somewhere eight cells wide, and several attempts at authoring one got within two columns and no closer. That is a piloting exercise rather than a test. Playing it once and keeping what happened is the answer. A BYTE FOR EVERY FRAME, written inside the loop that advances the recordings rather than after it, so a machine that jumped several frames at once still writes one for each. A recording is a timeline: one that skipped the frames nobody looked at would play back faster than it was flown. What is recorded is what the DEVICE WOULD REPORT, not the live state - a recording of a playback that wrote the live state would be a file of noughts. And it is flushed as it goes, because a recording is usually stopped by whoever is playing rather than by the program ending, and a demo lost to a buffer is a demo flown twice. Tests/replay.sh is where this and whatever follows it are checked. Twelve scripts now. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
caf5e1f99d |
A base speaks in the window, not into the world
Two bugs with one cause. The console draws into the map, so a message printed while flying was a message the lander then flew over - and printing scrolls, so every one of them moved the whole world up a row. The window is at a screen position and forty cells wide, and neither is true of it. So the window is two rows now: the gauge, and whatever there is to say. The letters are ordinary tiles, because the character generator starts at the space and glyph n is character n less thirty two. The rest of the row is blanked after every message, or a short one would leave the tail of a long one behind it. Opening the throttle wipes the line, because a message that outlived the moment would be read as describing this one. The crash still goes to the console, deliberately: it is the last thing the program says and it should survive the program. A or Start continues from a message as readily as a key does. Somebody flying on a controller should not have to reach for the keyboard to say they have read something. AND TWO TESTS WENT WITH IT, which is the interesting part. cosmosLanderSoft and cosmosLanderPadOne asserted on lines in a transcript, and the lines moved off the console - so both went on passing while checking nothing at all. A test that asserts a side effect rather than the thing itself is always one refactor from being decorative. What they were for is now checked in the picture, where the message actually is. The lander check moved earlier too. The window grew to two rows, so by 1.5 million cycles the lander had climbed behind the status bar - the window doing exactly what it should, and leaving the check counting six pixels of a forty pixel lander. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
bfb515e23b |
Cargo: bases with names, and a landing that is not an ending
Four bases, told apart by the scheme their pad is drawn in, so "the cyan one" is a thing a person can say and a thing the machine already knows. Yellow is missing on purpose: it is the lander, and a base the same colour as the thing landing on it would be a poor joke. Land empty at a base and it loads cargo for the base ACROSS THE MOON, two along - so the pairs are cyan with red and green with blue, and the wrapping surface is a route rather than scenery. Land carrying at the right one and it takes the cargo and pays eighty units of fuel. Land at the wrong one and nothing happens, which is why the destination will want to be on the screen. A LANDING NO LONGER ENDS THE RUN. The lander rests where it is, exactly on the surface with both speeds zeroed, until the throttle opens again - which is the only way to stop being landed. Gravity does not pull on something already sitting down, and a base does not hand out cargo sixty times a second to a lander parked on it. The pad array holds the base's number plus one rather than a flag. Nought still means no pad, so it is still one lookup, and a flag would have to be followed by "and which of the four" - the same walk done twice for an answer already in hand. Pads are eight columns rather than four. Four was 32 pixels in a moon 1024 round, which is a target somebody flying by feel misses over and over. WHAT IS NOT COVERED, and why: the delivery and wrong-base paths need a lander flown from one base to another, and hand-authoring a recorded pad input that hits an eight column pad across a 128 column moon is a piloting exercise rather than a correctness one. Several attempts got within two columns. Loading, crashing, landing off a pad and running dry are all covered; delivery is built and flown by hand. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
7257ad369c |
Landing pads, carved rather than looked for
A random walk does not leave flat ground and a lander wants some. Four pads are cut into the moon after it is generated, each four columns levelled to whatever height its first column happened to have - so they sit in the landscape rather than on a shelf above it. The moon decides where they are; this only decides that they are flat. Searching for flat spots was the alternative and it can fail, which means a fallback that carves anyway - the carving, plus a search nobody needed. They are marked by an ATTRIBUTE and not a tile of their own, which costs no art at all: a nibble is added to every index in a tile, so one solid block is grey moon or a cyan pad depending on the byte beside it. Which columns are pads is an array, because asking has to be one lookup. Four comparisons per column per row is 12,800 of them for one screen, and the landing verdict asks the same question again. THE LANDER STARTS ABOVE ONE, because that is where a porter's day begins. Starting in the middle of nowhere meant a straight descent landed in the middle of nowhere, which is a fine thing to be able to do and a poor thing to have to. That change cost the crash test its teeth, and the way it did is worth keeping. It held nothing at all and let the lander fall - and a short drop onto the high ground of the base you started above is survivable, which is correct, and left the test saying nothing. It holds Right now: lateral speed has no limit and nothing slows it, so a slide always ends badly however the rest is tuned. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
6073086584 |
The system takes a leftover window down
Lunar Porter put a fuel gauge up and never took it away, so the shell came back with FUEL across the top and the cursor underneath. Clearing did not help: a window is a layer at a SCREEN position that does not scroll, which is exactly what makes one left behind unpleasant - it sits over whatever comes next and cannot be scrolled off, cleared away or typed past. Taken away rather than given back, like the sprite table and for the same reason: nothing the shell draws is a window, so there is nothing to restore. And a program that FAULTED while one was up could not have taken it down itself, which is why this belongs to the system rather than to whichever programs remember. The check for it needed writing twice, and the first version was the familiar kind of wrong. It counted the gauge BAR's colour - and the bar disappears on its own whatever happens, because it is drawn with a tile the screen save puts back, so the check passed with the teardown deleted. What actually survives is the LABEL, in font tiles the shell needs anyway. So the screen is cleared afterwards and read cell by cell. With the window down a cleared row is "> " and a cursor; with it up the same row is F, U, E, L. Cells one and three being empty is the whole difference, and it is 29 and 22 pixels of it rather than a threshold somebody has to believe. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
2274be4b68 |
Lunar Porter, rung three and a half: fuel
Every thruster costs a unit every tick it fires, so holding two at once costs two - the honest price, and it makes a drift you corrected expensive in a way a drift you avoided is not. AN EMPTY TANK IS NOT AN ENDING. There is no message and nothing stops: a lander with no fuel is still flying, it just cannot do anything about where. What happens next is gravity, and gravity is patient. The test for it holds the thruster from the first frame to the last and crashes anyway, which is what says the fuel is real - a lander that could hold Up for ever would land every time, and the economy this is the first half of would have nothing to buy. The gauge is in the window, which is what the window was built for two commits ago: a bar at a SCREEN position, so the moon turning underneath does not carry it off. Thirty five cells after a label, redrawn whole every frame because seventy bytes out of one port is cheaper than working out which of them changed. A byte of fuel, and a byte is enough. Over eight it is a bar of up to thirty one cells - a shift, because there is no divide - and at a unit a thruster a tick it is about forty seconds of holding the engine open. Sixteen bits would be more arithmetic for a number nobody reads to the unit. Cargo and the bases are the other half. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
17c8da111f |
A window: a layer that does not scroll
The map moves and this does not, which the map alone cannot express. The scroll registers move ALL of it, so a score printed into the map slides away, and one printed into whichever rows the view happens to be showing jumps a pixel at a time as the fine offset changes. Port 0x3D is how many rows tall and 0x3E is which row it starts at. Nought tall is no window, so a cleared screen has none and every program written before this means what it meant. A start row is a register because a status bar along the bottom is as common as one along the top. IT HAS ITS OWN MEMORY, and that is the argument for it. The cheaper design draws the top rows of the MAP without the scroll applied - no new memory, one register - and makes those rows part of the playfield's ring, so a game that scrolls vertically has to route its world around its own scoreboard for ever. The point of a status bar is that it is not somewhere in the level. Lunar Porter does not scroll vertically today and will the moment an orbit is a thing you can reach. 0xC000 in the screen bank, which the map does not reach: it ends at 0xBFFF. Same cells, same tiles, same pages, same schemes. Being in the screen bank makes it per screen, so flipping the buffer flips the status bar with it - what a double buffered game wants, and surprising the other way round. Drawn over everything, sprites included. A sprite that could cover the fuel gauge would be a bug in every game that had both. Tile modes only. In bitmap mode the picture is using that memory, so a bitmap program pins things to the screen with sprites, which are in screen coordinates for the same reason. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
e3eccd17a8 |
Settle the view before saying how the landing went
The console draws into the map and the map is what is being scrolled, so a message printed while the view was three pixels into a cell came out three pixels off the top, with as much of its first row missing as the cell above it had lost. The flying is over by then, so the fractional part of the view has no more work to do. Putting it back is what makes the whole message visible. This is not the general problem. A status bar that has to stay readable WHILE the map moves is a different thing entirely, and nothing here solves it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
a074a831f0 |
The shell scrolls, and the moon was drawn where nobody was looking
Lunar Porter never put the row origin back. The map is a ring 128 rows tall that the screen shows 25 of, and the shell leaves that origin wherever its last command finished - so a moon drawn into rows nought to 24 while the screen is reading from row forty is a moon nobody can see. It came out as terrain missing, or half there, depending on how far down the prompt had got. Running Pad first was enough; so was holding Return. Nothing here is tidiness. It is the difference between the rows a program WRITES and the rows the screen READS, and only one of those is under the program's control. Grid has always known this; Lander did not. The check for it needed writing twice. Forty returns caught nothing, because the shell runs an eighty column screen which is FIFTY rows tall - forty returns fill it and never scroll it, so the origin was still nought and the test passed against a build with the fix taken out. The screenful that matters is the one the shell is using, not the one the program is about to ask for. At eighty it is 28,608 pixels of moon with the fix and none at all without it. break.sh is what said so. The first version of this check looked exactly like a passing test. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
f7be843ed9 |
Lunar Porter takes any controller, not the first one
A controller does not always arrive on pad nought. The front end hands out the numbers the host gave it, so a game that reads only the first one works on the machine it was written on and silently does nothing on the next - which is the shape of "the pad is detected, Pad shows it, and the game ignores it". Four reads and three ORs. One person flies this and which socket they plugged into is not a thing they should have to know. Presence is any of the four bits rather than the low one, for the same reason. The manifest's pad column takes several fixtures now, comma separated, and they fill the pads in turn. So cosmosLanderPadOne holds nothing on pad nought and flies the whole landing on pad one - a test that fails on the version of this program that shipped an hour ago. Also confirmed while looking: raylib 6 does refresh which gamepads are ready every frame in PollInputEvents, so a hot-plugged pad should be seen. Whatever is stopping that is above us and worth a separate look. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
6def343e97 |
Find Raylib's link flags instead of guessing one set
Raylib 6 calls X11 directly from GetClipboardImage, so a static libraylib needs -lX11 on a Linux desktop where 5.6 did not - and because it is an archive, one function nobody calls drags in the whole object and every X symbol with it. THE OLD SHAPE FAILED IN THE WORST AVAILABLE WAY. One hardcoded guess was test-linked, and when the guess went short the probe reported that Raylib was not installed - so make quietly stopped building Voyager and said so in the words it keeps for a machine with no graphics library at all. The answer looked like an absence and was a missing flag. So the candidates are tried in order and the first that links is the answer. The probe and the flags are the same thing now rather than two facts that can disagree, which is the property that was actually missing: a probe that tests something other than what gets built can only ever be a coincidence. Verified against 6.1-dev: Voyager builds and all 210 tests pass under it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
df50c2f0f8 |
The pad was working; the game was told there was not one
0x64 counted only the RECORDED pads. So a controller plugged into Voyager reported its buttons perfectly, and every game asking whether there was a controller was told no - which is exactly what Lunar Porter asked, once, at startup, before falling back to the console for the rest of the run. The cause is worth naming: a front end calls padSet every frame for every pad, so "held nothing" is the commonest thing it says and cannot also mean "there is no pad here". Connected is said separately now. Pad nought is always there behind a window, because the keyboard is behind it - which is the useful answer rather than the literal one. And Pad.asm, which is what should have existed before any of that guessing began. It prints a line whenever a pad changes, and tells apart the three states that look identical from inside a game that will not respond: one nobody noticed, one mapped to nothing, and a mapping that is wrong. WHY A PROGRAM AND NOT A PRINT IN THE FRONT END: because the question is what the MACHINE can see. A front end reporting what it thinks it is sending answers a different question, and the gap between those two is the whole of this bug. It also found that osPrintNumber takes A as the HIGH half - the same way round as the shift register and every other pair here, and not what a byte in A wants. Every value came out 256 times too big. Gravity is one frame in ten rather than six. The ratio between thrust and gravity is the feel; how often the tick comes round is how fast that feel arrives, and one in six was still touchy. Same lander, more time to think. And the verdict waits for a key. It printed and left immediately, taking the screen with it - so the one thing worth seeing, the lander sitting on the ground it had just reached, was gone before it could be looked at. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
8eb4e4d67e |
Lunar Porter, rung two: it lands, or it does not
The terrain is an array in Data Memory rather than something read back out of the map, and that is the whole reason this is cheap: the ground under the lander is one index into 128 bytes, where asking the screen would be a transfer through the controller every frame. The column is the world position over eight, masked to the moon's 128. The surface is that column's row times eight - three turns left of the shift register, since a row is at most 24 and 192 fits in the low half. The feet are the lander's top plus its eight pixels. WHAT DECIDES IS THE SPEED AT THE MOMENT IT ARRIVES. Both of them, and both have to be gentle: three quarters of a pixel a frame downwards and half of one sideways. Sideways is the tighter on purpose, because a landing that was soft downwards and sliding is a lander on its side - which is the interesting half of the difficulty, and the half the drift bar was blind about until it existed. Two fixtures say it works, and they differ only in what was held: one holds nothing and falls the whole way, the other pulses the thruster six frames in sixteen and survives. Same terrain, same seed, same keys. Also: the gamepad did nothing, and the reason is that the four direction buttons are the D-PAD. A lot of controllers made this century have one nobody uses - the thumb goes on the stick, which reports as an axis rather than a button - so a pad that was plugged in and working correctly did nothing at all. The stick counts as held past halfway now. Untested here, because there is no controller in this environment and the suite runs headless; Voyager also says at startup which controllers it can see, so a pad that still does nothing can be told apart from one nothing noticed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
db0c26e13f |
A bar for the drift, and a lighter touch sideways
A moon has no air, so a sideways drift never stops by itself and stopping one means cancelling the velocity EXACTLY. That is not hard to do; it is hard to do blind, which is what it was - a number nothing on the screen said anything about. So sprite one is a bar whose width is the drift. It runs right from the middle of the screen for a rightward one and left for a leftward one, so which way is as plain as how fast, and stopped is the one state with nothing drawn at all. The whole of it is a target width written once a frame; the device stretches one tile into it and the program draws nothing. Sideways thrust is one a tick rather than two. At two, the smallest correction available was twice the size it needed to be and overshooting was the normal outcome. WHICH ZERO MEANS NOTHING TURNED OUT TO MATTER. A target width of nought is the NATURAL width, not an empty sprite - so a bar with no drift in it came out eight pixels wide, sitting at the middle of the screen, saying "stopped" in the same shape it says "drifting slightly". What draws nothing is a SIZE of nought, which is the other zero in the other byte. Both meanings are deliberate and documented and it still caught me out inside a week of writing them down. The check for it earned its place by failing on that before it was found, which is the best evidence a check can offer. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
b1dde7908c |
Lunar Porter flies on a controller
A held thruster burns every tick it is held for, which is the whole reason the pad exists: the console can only say a key went down, so a thruster driven by it could be pumped and never leaned on. The burn happens on the same tick gravity does, and for the same reason - a sixteenth of a pixel is the smallest step this arithmetic takes, and applied sixty times a second it is an enormous acceleration. On the tick, thrust and gravity are two numbers whose RATIO is the whole feel of the thing. Position still moves every frame; only the acceleration is stepped, and nothing can see that. Two against gravity's one, so climbing and falling are the same speed. Three was the first try and it left the moon after about a second of holding. If there is a pad the console's arrows are ignored, because under a window the same keypress reaches both - the pad as a level, the console as a byte - and a thruster that fired twice for one press would be a mystery to anybody tuning it. q still quits, since a pad has no letter for it. With no pad the arrows still burn once a press, which is the most that can be done down a wire. And break.sh now rebuilds the disk images as well as the binaries. Half the things worth breaking here are SplitBit assembly rather than C, and those live on the fixture disks - so an edit to a .asm file changed nothing the suite could see, and the tool reported that nothing caught the break. That is the exact lie it was written to prevent, turning up in a new place. With the disks rebuilt it catches this one: the lander falls between the two captures instead of climbing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
fed1453e6e |
Controllers: four pads that say what is held
The console says WHICH KEY WENT DOWN, which is the right shape for typing and the wrong one for playing. A game wants to know what is being held, this frame, possibly several things at once, and a stream of presses cannot say that: a key that is down and staying down sends nothing at all. Lunar Porter's thrust is a burn per press for exactly that reason. So a pad is its own device on ports 0x60 to 0x6F, reporting a LEVEL. One read gives every button at once, holding is the natural thing to express, two directions together cost nothing, and reading does not consume it - a game may ask twice in a frame and be told the same thing both times. Four of them, because a party is four. They cost a port each and nothing at all when unused. The directions are the low nibble so "which way" is an AND with 0x0F; the buttons are the high nibble for the same reason. 0x64 says which are really there, so a game can ask for a controller rather than sitting silent while somebody presses things at it. They never interrupt: a game polls once a frame because that is when it draws. KEY-UP ON THE CONSOLE WAS THE OTHER WAY TO DO THIS AND WAS REJECTED. A terminal hands over characters and can never report a key coming up however it is asked, so it would have been a thing that worked behind a window and silently did not down a wire. A separate device can honestly say it is not there. Voyager drives pad nought from the keyboard as well as from any real controller, OR-ed rather than chosen between, so a game written for a pad is playable on a machine with none and unplugging one mid-game does not leave somebody holding nothing. And a recorded path, which is what makes any of it testable: --pad names a file of one byte a frame, and the manifest has an eighth column for it. A BYTE A FRAME AND NOT A BYTE A READ - a level asked twice in one frame has to answer the same both times, and a file that advanced per read would depend on how the program happened to be written. Voyager's own tests run headless with nobody holding anything, so without this the device would be exercised only by somebody playing: the state the console's line editing was in when it broke twice in two days. 0x50 is the timer, not free. The block this went in was chosen after looking rather than before. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
88ecb208f4 |
The coarse scroll register was being sent the wrong register
CALL eighth OUTA 0x36 RET puts A back the way it found it, so the column origin was written the high byte of the position that had been passed in, and the answer the subroutine had worked out went nowhere. The fine register was computed inline with OUTQ and was correct, which is exactly what it looked like from the outside: smooth scrolling within a cell that never advanced one. Q is the only register that crosses a RET. Every other answer in this program already came back in it; this one had been written as if A would do, and A very nearly does, which is what makes it worth a comment rather than a fix. Gravity was Jupiter's. A sixteenth of a pixel per frame per frame is the smallest step this arithmetic can take and it crossed the screen in a second, so it is applied one frame in six instead - which divides the pull by six and costs a byte and a compare. The alternative was a finer unit for velocity than for position, and that means a shift every time one is added to the other, twice a frame, for ever. And the check that catches all this now looks 1.5 million cycles in rather than twelve. The first number came from assuming a program that saves a whole screen takes a long time to start; it does not, and by twelve million the lander had flown seven hundred frames and left the picture. A capture near the beginning is worth more than a tuned one - there is less between it and the start that can move. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |