e20c9bac1f6d4a857b348bbe8fced84deec3a2a7
69
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
eaeb473176 |
ls says in green what the shell will start
Directories were already blue. Something the shell will run is green now, and it is asked by LOOKING rather than looked up. This was priced once as needing a runnable bit in the directory entry, set by the filesystem from a list of magic numbers it would have to be taught - declined twice, for putting format knowledge in the filesystem and giving two implementations a registry to keep in step. It costs nothing of the sort any more. The shell decides what to run by reading a file's first block, so "will this run" is a question with an answer already, and this asks the same one the shell would: SBEX for a program, "#!" for a script. Nothing is written down and nothing has to agree about anything. The price is a block read per file, which is exactly what the bit existed to avoid: a listing of thirty seven files went from 260,593 cycles to 635,321. It is paid in ls and not in dir on purpose - dir is the listing you audit and is built into the shell, this is the one you read and was loaded off the disk anyway. The fast one stays fast. plain.script is the proof on the test disk: it sits among a dozen scripts that are green and is not one, because it is the fixture with no shebang and the shell will not start it. The extension is decoration and the colour is the truth, which is the whole of what running by content means, finally visible. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
a13bbeb4db |
ls, a listing laid out to be read
> ls Copy.sbx Say.sbx Where.sbx Walk.sbx ls.sbx Lander where.sh dir says what is there one line each, with sizes and a tally of the disk. This says the names in columns and nothing else, which is what you want ninety times in a hundred. Two programs rather than one with a switch, because they answer different questions and neither answer is a worse version of the other. AND THIS IS A PROGRAM WHERE DIR IS BUILT IN, which is the difference that matters when the disk is what you are doubting: dir is already in memory and this has to be loaded off the disk it is about to list. The daily driver and the diagnostic. Two passes over the directory and no buffer at all. Columns need the longest name before the first line can be printed, which usually means holding every name - twenty four bytes each against a format that allows 1,024 entries. Walking twice costs a read of each directory block, into a buffer that is already there. Across and not down. Real listings go down the columns so that names next to each other alphabetically are next to each other on the screen, and that reason depends on sorting - which nothing here does. With the order arbitrary, down-and-across buys nothing and costs a division. Directories in blue and wearing a separator, unfinished saves in red, everything else plain: whether a file is runnable cannot be known without opening it, and opening every file in a directory to colour a listing is a price nobody agreed to pay. The colour reaches a terminal as well as the screen, so it is one mechanism and not two. ---- Three bugs, and one of them is this machine's oldest trap ---- nameLength answered in A, and a RET puts A back the way the caller had it. So it answered with nothing, and every gap between the columns came out the same width because the padding was subtracting whatever A happened to hold. Q is the ALU's output and nothing puts it back, which is why every answer here comes home in it. The padding subtracted the other way round - the name from the cell - which borrowed on every name that was not the longest, so all of them took the "wider than its cell" path and the listing came out separated by one space. And padding after a name left trailing spaces on every line that did not fill its last column. It goes before the next name now, so spaces only ever fall between two things and a line ends on a name. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
323d7a0330 |
A listing says what is left of the disk, and what will fit
dir said what was there and nothing about what was left. SplitDisk has printed the free figure since it was written, so the machine's own listing was the poorer of the two implementations at describing the same disk. 43 files, 3 directories 46 of 64 entries, 1893 blocks free Entries first, because they are the ceiling nobody notices until they hit it: a disk of small files runs out of directory slots long before it runs out of blocks. COUNTED RATHER THAN ASKED. The superblock keeps a free count and this file calls it "a note rather than the truth" in three places. sbfsSpace reads the whole directory table instead, which costs a read per directory block and is the answer rather than a guess. SplitDisk goes on reading the note and saying when it is stale, which is the right place for that check - the host tool is what you audit a disk with. ---- And it is a fact about the disk, not about where you are ---- The first version added the blocks up as the LISTING walked past them, which cost no extra read and was wrong: that walk stops only on entries in the working directory, so the same disk came out as 1,996 blocks free from the root and 2,025 from /Apps. Comparing against SplitDisk is what said so, which is what having two implementations is for. ---- The longest run, which is what decides whether a file fits ---- 4 of 16 entries, 37 blocks free the longest run is 25 Files are laid down contiguously, so the free total does not say whether a file will fit. Both implementations learn it, from one specification. Said only when it differs from the free total. Deleting is what fragments a contiguous store, and a disk that has only been appended to has one gap at the end - so on a healthy disk this is silent, and a line that appears only when something is wrong is a line somebody reads. There is no sort on this machine and the entries are in no order, so a candidate walks the disk: each pass finds the used extent nearest at or after it, and anything the candidate stands inside pushes it to the far end and starts the pass again. The same trick allocating uses. So it costs a pass per gap rather than per file - nearly nothing on a disk with one gap, more the more fragmented the disk is, which is the right way round. holes.img is six files with the second and fourth deleted, because no other disk here can show any of this: none of them has ever had anything deleted from it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
94bdf71356 |
Lunar Porter moves into a directory of its own
The first application with things of its own, and the first customer for all three rungs before it at once. /Apps/Lander a launcher /Packages/app.Lander/Lander.sbx the program /Packages/app.Lander/splash.tune its tune Typing "Lander" starts the launcher, which needs no new rule: the shell runs what it reads, so a launcher beginning "#!" and Say.sbx beginning "SBEX" go down the same road and neither the shell nor the person has to know which kind of thing they started. It passes on what it was told with $args, and says #quiet, because a launcher is machinery and not narration. The game then asks where it came from and joins its tune's name to that, so nothing anywhere names /splash.tune. Every recorded Lander test still types just "Lander" and knows nothing about any of this, which is exactly the claim. ---- Why the directory is not in /Apps ---- A launcher and a directory of the same name cannot both be there; SBFS refuses the second. And /Apps is the directory the shell walks for every word it does not know and Tab walks for every first word, so doubling what is in it is a cost on the path that runs most. The "app." goes in FRONT rather than behind because Tab matches the start of a name: a prefix is a namespace and a suffix is a collision. Copy.app beside Copy.sbx makes "Copy" and Tab complete to the shared "Copy." and hand you a broken word. The launcher names drive 0, so a game started from a disk of your own is looked for where the game is rather than where you are - its lines run on your disk, which is what makes everything else in a script work. /lander.state stays at the root. A saved position belongs to whoever saved it and is found where they are standing; a tune belongs to the program and is found beside it. That pair is the whole distinction. ---- And what it cost ---- Two hundred thousand cycles, about twelve frames, between the machine starting and the game drawing: a script opened, a deeper path walked. Six video captures moved out by that much. The drift bar needed something else - its pad is counted from the MACHINE starting rather than the game, so the burn is now twelve frames shorter from the game's point of view, and the recording holds the button longer instead. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
2defbb49e2 |
A program can ask where it came from
SWI osWhereAmI hands back the path the program was loaded from, on the same terms as osArgument, and Libraries/path.asm joins a name to the place another thing is in. Between them an application can find its own assets: ask where you are, then pathBeside that and the file's name. The answer FOLLOWS THE PROGRAM AND NOT THE PERSON, which is the whole point and the reason the working directory could not serve. A program's assets are relative to the program and its arguments are relative to whoever ran it, and cwd can only be one of them - setting it to the program's own would mean "Play mytune.tune", typed by somebody in their own directory, looked in Play's. It is made absolute before the program starts, because the path the search settled on may be a bare name: a program found where somebody was standing is named by the word that was typed, and a bare name means the working directory - which a program is entitled to move out of. Worked out once, at the start, since where a program came from is a fact about its start and cannot change afterwards. Joining is a LIBRARY and not a service. A service that opened a file relative to the program would need a twin for every file operation there is - read, save, info, block, start, write, done, delete, rename - while one service handing back a path composes with all of them. ---- And the root's own path was "//" ---- Found by the first caller that asks. shellPath prepends a separator in front of whatever string it is given, so being handed the separator itself wrote two of them. Nothing saw it while the only caller was the prompt, which asks where it is only when that is not the root. It is handed an empty string now, and cosmosWhere runs a program from the root. Where.sbx exists to be run rather than read, and is on the test disk twice: at the root, where it is found by the bare word typed, and in /Apps, where it is found by a path that already says where it is. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
d9ebc76cf5 |
A script is given words, and keeps them
"do build.sh cosmos.asm" looked for a file called "build.sh cosmos.asm", because scriptOpen copied the whole rest of the line into the name. So a script could be told nothing, and a launcher - a script whose entire job is to hand on what it was told - could not exist. Now the name is cut off the front and what follows is kept whole. $1 to $9 are the words of it, walked out on demand, and $args is all of them. Nothing is stored per parameter, so there is no limit on how many a script may be handed and no second number to keep in step. Both ways of starting a script pass them on: "do" and typing the name. A word that was not given is an error that stops the script, like every other name this shell does not know. Expanding it to nothing would let a command run with an argument missing and then report success, which is what stop-on-failure exists to prevent. $args is always set inside a script, empty if it was given nothing, so "if same $args" can be asked. ---- And the count that describes the block was already wrong ---- Found while adding a field to it. The state one script keeps for another is saved by a single copy of a fixed number of bytes, and that number was 71 against a block of 77: six bytes of line position had been added in the middle of it years after the count was written. So the tail of every saved script was never saved, and #quiet in a helper stayed behind in the script that called it - the opposite of what this file's own comment promises and the README documents. The unsaved line position turned out not to matter, because a loop keeps its own copy in the block record. Nothing said so. Three numbers describe this block and all three now say so in a comment, and cosmosScriptNest ends on a helper that goes quiet and a caller that must not stay that way. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
f351ee8844 |
Every score on the disk, not a named one
The makefile compiled splash.score by name, so intro.score sat beside it turning into nothing - which is exactly the failure the mirror three lines above exists to prevent, and its comment says so: a list in a makefile goes stale the moment somebody adds a file, and what they forgot is invisible until they go looking for it on the machine. Found by the user going looking for it on the machine. Every .score in Programs/Tunes is compiled and put on the disk now, and every patch in Programs/Sounds is converted first so a score can name any of them. The source is mirrored to /Source/Tunes with everything else that was written; the tune goes to the root, where a program looking for one expects it - which is worth writing down, because the two being in different places is the thing that sent somebody hunting. two.score moved to Tests. It is a fixture, it names patches that only exist in the test build, and it was in the music tree only because that is where scores were when it was written - which the game disk build found immediately by failing to compile it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
00b31b88af |
Lander opens on a splash, with music
The first customer for the player outside the program it was pulled out of, and the argument for doing it: a splash needs no resident player at all. Nothing else is happening while the logo is up, so Lander owns the timer and all four channels exactly as Play does. Music UNDER a running game is still deferred, and still the harder problem. It polls the timer rather than being interrupted by it. Lander has no vector segment and waits for the screen by reading port 0x30, so it waits for a beat by reading port 0x50 - the same shape, and it brings no handler that would have to be taken away before the game starts. The tune is read off the disk. A missing one means the logo and silence and the game starts anyway, the way a missing /lander.state means the defaults stand. FOUR THINGS THIS COST, each found by running it: A subroutine cannot answer in A. CALL saves and restores it, so splashSkip handed its caller back the A it already had - the channel number of the last stepVoice - and the splash ended on its first pass whatever anybody pressed. Q is what survives a RET, which nextRandom says twenty lines away and I did not read. Port 0x3D is how many window rows are SHOWN, and it is two once putGauge runs and nothing before. A line written to row eleven went somewhere real and was displayed nowhere. A nought is not a keypress. It is what a recorded keyboard file holds while nobody is typing, and a splash that took it for a key is one no test could ever watch. And skipping has to be free. Asked after blanking the window and reading the file, a skipped splash still cost a fifth of a second - enough to push the thruster test's early capture past the frame it looks at. Asked first, it costs a pad read. player.asm no longer asks a caller for Order0 to Order3: the voice records name NoOrder instead, so a program whose tune comes from a file does not have to define four order lists it never uses. That was the file case finding a wart in the contract. Every other Lander test now skips the splash with a space - a key the game itself ignores, since it answers to q, z and the arrows - so they go on testing what they tested. The one in sound.sh presses nothing, which is what makes it the one that hears the music. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
87467a7d3a |
Lander starts a flight where a flight starts
run does not reload, and that is right: run's job is to run the program that is loaded, which is why it is a separate word from load and why typing a program's name does both. What follows is that the numbers the assembler wrote into a Data Segment are LOAD-time values, true once, and a program that wants them true at every start has to say so itself. Lander did not. Flying is 0x01 in the data and is only ever cleared, so a second run began with the loop already over: the program started and handed the machine straight back. Reproduced by running it, quitting, and running again - the leftover game keys land at the shell prompt, which is what a program that never read them looks like. AND THE TEST FOUND A SECOND ONE, quieter and worse. The terrain seed is a written number, so a fresh load always walks out the same moon - but nextRandom moves it, and a second run generated a DIFFERENT moon. Nobody decided that. A test comparing the whole screen found it without anybody having had to think of it in advance, which is the argument for comparing the picture rather than the variables somebody remembered to check. Only what a flight needs to begin is put back. Anything not on the list keeps what the last run left it, which is deliberate - state surviving a run is sometimes exactly what is wanted, and the way to have that is for the list to be a decision rather than a sweep. The things that are simply nought are a table of NAMES that the assembler turns into addresses, so adding a variable that must start empty means adding it there and nowhere else. video.sh now runs Lander, quits, runs it again, and requires the same picture a hundred frames in - the same moon, the same lander in the same place, the same gauges. break.sh confirms it by taking the seed line back out: 37,995 bytes of the picture differ. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
bfc46d982e |
Play reads a tune from a file
The loader, and the reason it is small: everything in a tune is an offset from wherever it was put, so taking one means adding the base to two tables and pointing four voices at their order lists. No sequence is walked. Nothing inside one is an address to be found and corrected, which is the difference between a malformed tune that plays wrongly and one that takes the loader with it. "SBTU", version, the tick in cycles, how many patches and sequences, offsets to the two tables, an order list each, and the patch each voice starts on. The magic is checked before anything else, because from there on the loader follows what the offsets name. break.sh shows what that guard is worth: without it, handing Play a PROGRAM runs the machine away until the cycle limit, rather than saying it is not a tune. PatchTable, SequenceTable and VoiceStart became pointers, so the engine does not care whether a tune came out of a file or was assembled in. Play's built-in tune now hands over the same three addresses a loaded one would, in eight lines - which is what keeps the two paths from drifting, and what made this rung change no scheduler code at all. Tests/maketune.py lays the fixture out byte by byte. IT IS NOT THE COMPILER: the sequences and the patches are literal bytes and the only thing computed is where each piece lands. That is the point - the loader is checked by something that does not share its idea of the format, which is the same reason SplitDisk and sbfs.asm share nothing but a specification. Both notes in the fixture are number 60, so the octave between them is a 0x80 command loading the second patch out of the file: the header, the tick, the relocation, an order list and a patch from a file, measured in one go. Play and the tune live on quiet.img rather than cosmos.img, so a fixture does not move ten recordings every time it changes size. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
5d9b39514b |
The player speaks indices, which is the shape a file has to be
Order lists hold one-byte sequence indices and 0x80 holds a one-byte patch index. Two tables, PatchTable and SequenceTable, are the only places an address lives. THAT IS WHAT MAKES A TUNE LOADABLE WITHOUT WALKING IT. Nothing inside a sequence or an order list is an address, so putting one in memory means adding the load address to two arrays and nothing else. The alternative is a loader that parses every sequence looking for addresses to correct, which is a loader a malformed file can walk off a cliff. Doing it now, while the tune is still assembled in, means the file form and the assembled form are the same shape - so reading a tune from a file will change no engine code at all. The whole point of the rung. The patch each voice starts on moved from four calls in a row into VoiceStart, four declared bytes, and loadStartPatches reads them. A starting instrument is state and belongs where state goes: the user's ruling is that a voice with undefined state is an error, prompted by noticing that a program run a second time starts with the memory the first run left, because loading is what initialises and running is not. Order lists also halved in size, which was not the reason but is welcome. Hand-writing the two tables is exactly the tedium the compiler exists to remove - every sequence counted into its place, and moving one means renumbering. Better to feel that here than after a tool has baked the shape in. Verified by rendering: bar for bar the same piece. break.sh confirms the scaling, since an index is doubled to reach a two-byte entry and halving that step lands on the wrong sequence and fails four checks. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
8029063bdc |
Sequences, order lists, and a command that changes the instrument
The rung between M3 and M4, and the point of doing it before the format: the engine learns the tracker's model with the tune still assembled in, so M4 becomes serialising a thing that exists rather than designing a thing that does not. A voice no longer walks one long track. 0xFF now means THIS SEQUENCE ended, and the voice takes the next address from an order list of its own. That is where repetition comes from, and it costs no notation: the bass plays the same sequence in the first bar and the last and it is written once. Per voice rather than one shared table of four-column rows, because a voice's order cursor is then a pointer it advances by itself - the same LDD and STD move everything else here makes. Four columns is how it reads, not how it is stored. Sequences also carry COMMANDS, which take no tick: the reader acts and reads the next event on the same boundary. One is defined, 0x80, which plays the rest of that voice on another patch, and the other 125 values are left alone. A patch change reshapes whatever is still ringing on the voice and nothing can be done about that - a channel has one set of parameters and a note in its release is using them - so it is a fact about the hardware and the cure is a rest, which is the composer's. The engine moved to Libraries/player.asm rather than being copied into the test a second time, now that it is big enough to drift. Play supplies the tune and the beat; the library supplies the scheduler, the patch loader and a voice's state. Verified by rendering: bar for bar identical across the move. AND THE TEST FOUND A REAL FLAW IN THE FORMAT, which is the whole argument for building the reader first. The order list ended with 0x0000, on the reasoning that no sequence could live below the 0x3000 this program is based at. True of a loaded program, false of a boot image whose data starts at zero - so the first test written against it read its own first sequence as the end of the list and played nothing at all. It is 0xFFFF now, which mirrors the 0xFF ending a sequence and is impossible everywhere: a sequence at 0xFF00 or above has fewer bytes left than it needs. An address is a poor place to hide a flag unless the address is impossible in every program, not just this one. One order list in the test now really ends, because otherwise nothing reached the terminator at all: every voice sat on a long rest and the break went unnoticed. With it, breaking the test reads garbage past the end and the counter sums two notes at 781 hertz. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
ce1c0517aa |
M3: an instrument for each voice
Play loads a patch per channel before a note is played: an oboe for the
melody, strings under it, a square wave for the bass and a kalimba for
the arpeggio. It reads a count and that many parameter and value pairs -
the format SoundPatch writes - and understands nothing else about them,
which keeps SoundPatch the only thing that knows what soundThing's JSON
means.
FOUR PATCHES CAN BE UP AT ONCE, and that is the whole rung. It is the
first use of
|
||
|
|
1687422619 |
A voice starts pointed at its track, without code to point it
There is no assembler bug. I reported one and was wrong. A label written in the Data Segment does come out as its address, two bytes, most significant first - implemented in populateOutputBuffers, documented in the Assembler Manual, and correct. What misled me was the test I checked it with: the label was the first thing in an unbased Data Segment, so its address really was 0x0000, and I read the right answer as an unfilled placeholder. So Play was doing at run time what the assembler had already offered to do at assembly time. The voice records now carry their track labels directly, which is exactly the shape LDD reads, and nothing relocates on this machine so the address written is the address it will have. That takes out startVoice, its four call sites, and the eight SETDs that fed them: 450 bytes to 379, and the initial state of a voice is now something you can read rather than something you have to follow the code to work out. The comment claiming otherwise is gone from Play.asm, and the same change is made in fourVoiceTest. The music is unchanged - bar by bar the render matches to within one per cent, which is the program loading a shade sooner because it is smaller. Ten recordings moved for the same reason: 446 to 379, and nothing else. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
79d1e2639b |
M2: four voices on one clock
Play is the scheduler: one tick, four cursors. Every voice keeps its own place in its own track and its own count of how much longer the note it is holding lasts, so a voice playing whole notes and a voice playing eighths cost the same and never have to know about each other. They share the tick and nothing else. That is what makes the tick the smallest subdivision in the piece rather than a note length - it is the only unit four parts can agree on. A track is pairs of bytes: what to play, then how many ticks it lasts. MIDI notes stop at 127, so the top of the byte was free and neither the rest nor the end marker had to be invented - 0 is a rest and 0xFF ends the track. Examples/tune.asm spent zero on its end marker and so could not write a rest at all, which one voice can live with and four cannot: the silences are what make them separate parts rather than a chord. The state is four bytes a voice, cursor first because that is what LDD and STD move - a pointer through a pointer, which is what lets this be a loop over four voices instead of the same code four times. CALL preserves A and DP0-2, so a caller says which voice it means in two instructions. The piece is four bars of C, F, G, C with the parts moving at four different rates, because that is the thing one channel cannot do. All four channels get the same instrument, which is exactly what M3 replaces. fourVoiceTest staggers two voices so each gets a stretch alone: middle C while the other rests, the octave while the first is silent, then both. The first two are measured for pitch and the third for level, because TWO NOTES CANNOT BE ASKED THEIR PITCH - the crossing counter adds them and answers 785 hertz, which is 262 plus 523 and a fact about nothing. Verified with break.sh: dropping the channel select trips one check, pointing both voices at one cursor trips three. It shares Play's design and not its code, and is smaller - no track ends in it, so there is no live flag and no count of what is still playing. Play.sbx on the disk is why ten recordings moved: one added line each, and the file count with it. Nothing else in them changed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
1b251fb290 |
M1: the tune keeps a beat it sets, instead of one it borrowed
tune.asm counted the screen's frames, because until
|
||
|
|
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. |
||
|
|
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. |
||
|
|
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 |
||
|
|
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 |
||
|
|
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 |
||
|
|
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 |
||
|
|
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 |
||
|
|
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 |
||
|
|
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 |