Commit Graph
15 Commits
Author SHA1 Message Date
AnachronautandClaude Opus 5 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
2026-09-03 18:42:34 -04:00
AnachronautandClaude Opus 5 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
2026-09-03 16:25:56 -04:00
AnachronautandClaude Opus 5 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
2026-09-03 12:23:12 -04:00
AnachronautandClaude Opus 5 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
2026-09-03 12:04:27 -04:00
AnachronautandClaude Opus 5 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
2026-09-03 11:45:30 -04:00
AnachronautandClaude Opus 5 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
2026-09-03 00:35:34 -04:00
AnachronautandClaude Opus 5 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
2026-09-03 00:02:59 -04:00
AnachronautandClaude Opus 5 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
2026-09-02 23:51:48 -04:00
AnachronautandClaude Opus 5 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
2026-09-02 23:36:05 -04:00
AnachronautandClaude Opus 5 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
2026-09-02 22:48:24 -04:00
AnachronautandClaude Opus 5 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
2026-09-02 22:33:32 -04:00
AnachronautandClaude Opus 5 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
2026-09-02 22:14:36 -04:00
AnachronautandClaude Opus 5 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
2026-09-02 21:58:31 -04:00
AnachronautandClaude Opus 5 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
2026-09-02 21:13:09 -04:00
AnachronautandClaude Opus 5 d6c81fa32c Lunar Porter, rung one: it flies
A lander over a moon that wraps. Landing, crashing, fuel, cargo and bases
are not here - this rung exists to answer whether it FEELS right, because
everything after it is bookkeeping and none of it is worth building on a
lander that is no fun to fly.

The moon comes for free. The map's column origin is a ring in hardware, so
128 cells is 1024 pixels of surface with no edge and no seam to cross.

Position and velocity are sixteen bit in SIXTEENTHS OF A PIXEL, and the
unit is the design: gravity is a small number added to a velocity and a
velocity is a number added to a position, with no multiply or divide
anywhere. 1024 pixels is 16,384 sixteenths, which is 2^14 - so going all
the way round is an AND with 0x3FFF rather than a comparison, and it is
never wrong at the seam.

The lander never moves sideways. The world scrolls under it and it sits at
the middle of the screen, which is a byte a frame instead of two and is
also what makes the wrap invisible: there is no moment where it jumps.

One key is one burn. The console says which key went down and there is no
such thing as a key coming up, so a thruster cannot be held - a press adds
to the velocity once. That is a property of the machine rather than a
choice this program made, and it reads as pumping the engine.

Four bugs found by running it, all worth keeping written down:

  B CANNOT BE A LOOP COUNTER here. Every comparison is an INIB, so the
  count was overwritten by whichever bound was last tested and the loop
  reset itself for ever. Counters that outlive arithmetic live in memory.

  A subroutine answers in Q, and the AND after it read A. The moon came
  out flat because it was testing the height against 1 instead of the
  random number.

  Row minus height, not height minus row: they are equal at the surface,
  equal does not borrow, and the surface row has to be ground.

  And the shift register has A as its HIGH half. Written the other way,
  the view scrolled by 256 cells for every one it should have.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
2026-09-02 16:52:01 -04:00