The memory map gave CosmOS 0x0000 through 0x1FFF of Program Memory and applications 0x2000 and above. CosmOS is 8141 bytes at the previous commit, which is fifty one bytes short of the line, and the next thing added to it went over. GOING OVER DOES NOT FAIL WHERE IT HAPPENS. Nothing enforces the division: an application says where it goes with #Base and the loader puts it there, so a CosmOS that has grown past 0x1FFF simply has the next program loaded written over the end of it. What breaks is whichever part of the shell that program happened to cover, at whatever later moment somebody uses it. It turned up here as the monitor's assemble command answering "I do not know" to valid instructions, several commands into a session, on a machine that had booted perfectly well. Both halves are doubled: applications now start at 0x4000 in Program Memory and 0x2000 in Data Memory. That is 16K of code and 8K of data for the system, against the 8775 and 2948 it uses today. Both were on the same trajectory, and moving them together means the twenty files that say #Base are edited once rather than twice. The standalone loader's loadable.asm keeps its old base: it belongs to the loader CosmOS grew out of, not to CosmOS, and its addresses answer to a different program. The unbased-segment diagnostic keeps its old base too - it exists to produce an error message that names the address, and the message is what is recorded. Tests/docs.sh now reads the two limits out of the table in the README and measures both segments against them. It reads them rather than being told them because the table is the specification, and this is the second time in this project that the thing nobody checked is the thing that rotted. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
675 lines
15 KiB
NASM
675 lines
15 KiB
NASM
; Snake, as an application CosmOS can load and run.
|
|
;
|
|
; The first program written for this machine that is played rather than watched. It needs
|
|
; key mode: in line mode the terminal holds what is typed until Return, so steering would
|
|
; mean pressing a direction and then Enter, and by the time it arrived the snake would
|
|
; have been into the wall for some time.
|
|
;
|
|
; It asks the console once a frame whether a key is waiting, and never waits for one. The
|
|
; console holds the next key until it is asked, so nothing typed between frames is lost,
|
|
; and a script of moves plays back one move to a frame.
|
|
;
|
|
; WHY IT POLLS RATHER THAN INTERRUPTS. When this was written a loaded program could not be
|
|
; interrupted at all: installing a handler means putting an address in the vector table,
|
|
; and the loadable format carried only code and data, so a program that was not the one the
|
|
; machine booted from had no way to say what its vectors were. That is no longer true - the
|
|
; format carries them now, and Keys.asm is the program that shows it.
|
|
;
|
|
; This still polls, and now by choice. Asking once a frame is what the machines this one is
|
|
; pretending to be actually did, it is the shape a game with a frame loop wants anyway, and
|
|
; having one of each in the same Apps directory is worth more than having two the same.
|
|
;
|
|
; THE BOARD IS A PAGE, and that is the whole trick this program turns on. Sixteen by
|
|
; sixteen is 256 squares, so a square number is a byte, and the board is aligned so that
|
|
; the square number IS the low byte of its address. Reaching a square is writing its
|
|
; number into the low half of a stored pointer and loading the pointer back - no
|
|
; multiplying, no carrying, and the row and column fall out as the two nibbles.
|
|
;
|
|
; The body is a second page, used as a ring of square numbers with the oldest segment at
|
|
; the tail. Moving is putting a square on the head end and taking one off the tail end,
|
|
; so the cost of a move does not depend on how long the snake is. The ring wraps at 256
|
|
; by itself, because an index into it is a byte and a byte is all it can be.
|
|
;
|
|
; Note that #Include console.asm comes at the END of this file. A loadable program starts
|
|
; at the first byte of its code, so the first instruction in this file has to be the one
|
|
; the program begins with.
|
|
|
|
#Include services.asm
|
|
|
|
#Program
|
|
|
|
#Base 0x4000
|
|
|
|
start:
|
|
; The two pointers whose low byte is a square number. Both regions are page aligned, so
|
|
; the high byte written here is the whole of what does not change, and nothing after
|
|
; this ever has to work out an address.
|
|
SETD.0 Board
|
|
SETD.1 CellAddress
|
|
STD.0.1
|
|
SETD.0 Body
|
|
SETD.1 BodyAddress
|
|
STD.0.1
|
|
|
|
; Everything is set here rather than trusted to be zero, because a program that is run
|
|
; twice without being loaded again finds its Data Segment exactly as the last run left
|
|
; it. The board is the obvious half of that; the score and the direction are the half
|
|
; that would be missed.
|
|
CALL resetState
|
|
CALL clearBoard
|
|
CALL placeSnake
|
|
CALL placeFood
|
|
|
|
SETD.0 ClearScreen
|
|
CALL printString
|
|
|
|
; Key mode, so that one key is one byte and arrives when it is pressed. It is put back
|
|
; before this returns, and CosmOS puts it back too in case a program stops without
|
|
; doing so.
|
|
INIA 0x01
|
|
OUTA 0x02
|
|
|
|
gameLoop:
|
|
CALL takeKey
|
|
SETD.0 Quitting
|
|
LDA.0
|
|
BNA gameOver
|
|
|
|
CALL advance
|
|
SETD.0 Dead
|
|
LDA.0
|
|
BNA gameOver
|
|
|
|
CALL draw
|
|
CALL pause
|
|
BRI gameLoop
|
|
|
|
gameOver:
|
|
; Drawn once more so the last thing on the screen is the position it ended in.
|
|
CALL draw
|
|
SETD.0 Won
|
|
LDA.0
|
|
BNA gameOverWon
|
|
SETD.0 Quitting
|
|
LDA.0
|
|
BNA gameOverQuit
|
|
SETD.0 DeadText
|
|
BRI gameOverSay
|
|
gameOverWon:
|
|
SETD.0 WonText
|
|
BRI gameOverSay
|
|
gameOverQuit:
|
|
SETD.0 QuitText
|
|
gameOverSay:
|
|
CALL printString
|
|
CALL newLine
|
|
RSTA
|
|
OUTA 0x02 ; Line mode, the way it was found.
|
|
SWI osExit
|
|
|
|
; ---- Reaching a square ----
|
|
;
|
|
; A holds a square number. Leaves DP3 pointing at that square of the board.
|
|
;
|
|
; DP3 because a subroutine cannot hand back any of the others: CALL saves DP0 through DP2
|
|
; and RET puts them back, so an assignment to one of them here would be undone on the way
|
|
; out. The same reason means a caller must not keep anything in DP3 across one of these.
|
|
cellPointer:
|
|
SETD.0 CellAddress
|
|
INCD.0
|
|
STA.0 ; The square number is the low half of its own address.
|
|
DECD.0
|
|
LDD.3.0
|
|
RET
|
|
|
|
; A holds a position in the body ring. Leaves DP3 pointing at it.
|
|
bodyPointer:
|
|
SETD.0 BodyAddress
|
|
INCD.0
|
|
STA.0
|
|
DECD.0
|
|
LDD.3.0
|
|
RET
|
|
|
|
; ---- Setting up ----
|
|
|
|
resetState:
|
|
RSTA
|
|
SETD.0 Score
|
|
STA.0
|
|
SETD.0 Dead
|
|
STA.0
|
|
SETD.0 Quitting
|
|
STA.0
|
|
SETD.0 Won
|
|
STA.0
|
|
SETD.0 Length
|
|
STA.0
|
|
SETD.0 BodyHead
|
|
STA.0
|
|
SETD.0 BodyTail
|
|
STA.0
|
|
INIA 0x03
|
|
SETD.0 Direction
|
|
STA.0 ; Moving right, which is where the three segments point.
|
|
|
|
; The seed is copied rather than used in place, so that a second run starts the same
|
|
; game as the first. A game that came out differently every time would be nicer to
|
|
; play and impossible to record.
|
|
SETD.0 RandomSeed
|
|
SETD.1 RandomState
|
|
LDA.0
|
|
STA.1
|
|
INCD.0
|
|
INCD.1
|
|
LDA.0
|
|
STA.1
|
|
RET
|
|
|
|
clearBoard:
|
|
SETD.0 Board
|
|
RSTB
|
|
clearBoardSquare:
|
|
RSTA
|
|
STA.0
|
|
INCD.0
|
|
INCB
|
|
BNB clearBoardSquare ; B comes back to zero after all 256 squares.
|
|
RET
|
|
|
|
; Three segments across the middle of the board, oldest first, so the leftmost is the
|
|
; tail and the rightmost is the head.
|
|
placeSnake:
|
|
INIA 0x86
|
|
CALL addSegment
|
|
INIA 0x87
|
|
CALL addSegment
|
|
INIA 0x88
|
|
CALL addSegment
|
|
RET
|
|
|
|
; A holds a square. Marks it as snake and puts it on the head end of the ring.
|
|
addSegment:
|
|
PSHA
|
|
CALL cellPointer
|
|
INIB 0x01
|
|
STB.3
|
|
|
|
SETD.0 Length
|
|
LDA.0
|
|
CALL bodyPointer ; The ring fills forwards from zero while setting up.
|
|
POPA
|
|
STA.3
|
|
|
|
SETD.0 HeadCell
|
|
STA.0 ; The newest segment is always the head.
|
|
SETD.0 Length
|
|
LDA.0
|
|
INCA
|
|
STA.0
|
|
DECA
|
|
SETD.0 BodyHead
|
|
STA.0 ; Which is at Length minus one.
|
|
RET
|
|
|
|
; ---- Food ----
|
|
;
|
|
; A square is chosen at random, and if something is already there the search walks
|
|
; forwards until it finds somewhere empty. That does two jobs with one loop: it keeps the
|
|
; food off the snake, and it means the generator never has to be asked twice.
|
|
placeFood:
|
|
CALL randomByte
|
|
MVQA
|
|
SETD.3 FoodStart
|
|
STA.3
|
|
placeFoodLook:
|
|
CALL cellPointer
|
|
LDB.3
|
|
BRB placeFoodHere ; Empty, and A still holds which square it was.
|
|
INCA
|
|
SETD.3 FoodStart
|
|
LDB.3
|
|
XOR ; All the way round to where the search began?
|
|
BRQ placeFoodFull
|
|
BRI placeFoodLook
|
|
|
|
placeFoodHere:
|
|
INIB 0x02
|
|
STB.3 ; DP3 is still on the square that was found empty.
|
|
RET
|
|
|
|
placeFoodFull:
|
|
; Nowhere to put it, which means the snake is the board. There is no way to lose from
|
|
; here and nothing left to do, so it counts as finishing rather than as an error.
|
|
SETD.0 Won
|
|
INIA 0x01
|
|
STA.0
|
|
SETD.0 Dead
|
|
STA.0
|
|
RET
|
|
|
|
; A sixteen bit shift register, rotated right one bit a time, with the bit that falls off
|
|
; the bottom fed back into four places along it. Q comes back holding the high half, which
|
|
; is the part that changes least predictably.
|
|
;
|
|
; SHR rotates A and B together as one sixteen bit register, so the bit that leaves the
|
|
; bottom of B arrives at the top of A. That is not the shift a shift register wants - it
|
|
; wants that bit gone - so where the bit came round is exactly where the feedback goes,
|
|
; and one XOR both clears it and applies the taps.
|
|
randomByte:
|
|
SETD.3 RandomState
|
|
LDA.3
|
|
INCD.3
|
|
LDB.3
|
|
SHR
|
|
PSHB
|
|
INIB 0x80
|
|
AND
|
|
POPB
|
|
BRQ randomNoFeedback
|
|
PSHB
|
|
INIB 0x34 ; 0x80 clears the bit that came round, 0xB4 is the taps.
|
|
XOR
|
|
MVQA
|
|
POPB
|
|
randomNoFeedback:
|
|
STB.3
|
|
DECD.3
|
|
STA.3
|
|
RSTB
|
|
OR ; Q is A, which is what a routine hands back in.
|
|
RET
|
|
|
|
; ---- Steering ----
|
|
;
|
|
; One key a frame, and never a wait for one. The console keeps the next key until it is
|
|
; asked for, so a key pressed while the snake was moving is still there next frame.
|
|
takeKey:
|
|
INA 0x01
|
|
INIB 0x01 ; READY: is there a byte to be had?
|
|
AND
|
|
BRQ takeKeyDone
|
|
INA 0x00
|
|
|
|
INIB 0x71 ; q
|
|
XOR
|
|
BRQ takeKeyQuit
|
|
INIB 0x77 ; w
|
|
XOR
|
|
BRQ takeKeyUp
|
|
INIB 0x73 ; s
|
|
XOR
|
|
BRQ takeKeyDown
|
|
INIB 0x61 ; a
|
|
XOR
|
|
BRQ takeKeyLeft
|
|
INIB 0x64 ; d
|
|
XOR
|
|
BRQ takeKeyRight
|
|
takeKeyDone:
|
|
RET
|
|
|
|
takeKeyUp:
|
|
RSTA
|
|
BRI takeKeyTurn
|
|
takeKeyDown:
|
|
INIA 0x01
|
|
BRI takeKeyTurn
|
|
takeKeyLeft:
|
|
INIA 0x02
|
|
BRI takeKeyTurn
|
|
takeKeyRight:
|
|
INIA 0x03
|
|
|
|
takeKeyTurn:
|
|
; A snake cannot turn back into itself. The four directions are numbered so that two
|
|
; opposite ones differ in exactly their lowest bit and nothing else, which makes the
|
|
; whole test one XOR against one.
|
|
PSHA
|
|
SETD.0 Direction
|
|
LDB.0
|
|
XOR
|
|
MVQA
|
|
INIB 0x01
|
|
XOR
|
|
POPA
|
|
BRQ takeKeyDone ; Opposite, so it is not a turn anybody can make.
|
|
SETD.0 Direction
|
|
STA.0
|
|
RET
|
|
|
|
takeKeyQuit:
|
|
SETD.0 Quitting
|
|
INIA 0x01
|
|
STA.0
|
|
RET
|
|
|
|
; ---- Moving ----
|
|
|
|
advance:
|
|
CALL step
|
|
SETD.0 Dead
|
|
LDA.0
|
|
BNA advanceDone ; Into a wall, and there is nowhere to move to.
|
|
|
|
; What is in the square the head is moving into?
|
|
SETD.0 NextCell
|
|
LDA.0
|
|
CALL cellPointer
|
|
LDB.3
|
|
BRB advanceMove ; Empty.
|
|
DECB
|
|
DECB
|
|
BRB advanceEat ; It held a two, which is food.
|
|
|
|
; A one, so it is the snake. There is exactly one square of itself a snake may move
|
|
; into, and that is the one the tail is standing on, because the tail is leaving it in
|
|
; the same move. This is what lets a snake follow itself round a corner instead of
|
|
; dying on the segment that is getting out of its way.
|
|
;
|
|
; Asked as a question about the tail rather than by taking the tail off and looking at
|
|
; what is left. Both give the same answer, but this one does not have to be undone when
|
|
; the answer is that the snake is dead, and a dead snake that had already lost its tail
|
|
; would be drawn a segment short in the last frame anybody sees.
|
|
CALL tailCell
|
|
MVQB
|
|
SETD.0 NextCell
|
|
LDA.0
|
|
XOR
|
|
BNQ advanceHitSelf
|
|
|
|
advanceMove:
|
|
CALL removeTail
|
|
CALL addHead
|
|
RET
|
|
|
|
advanceEat:
|
|
RSTB
|
|
STB.3 ; The food is gone. DP3 is still on that square.
|
|
; No tail comes off, and that is the whole of what growing is.
|
|
CALL addHead
|
|
SETD.0 Score
|
|
LDA.0
|
|
INCA
|
|
STA.0
|
|
CALL placeFood
|
|
advanceDone:
|
|
RET
|
|
|
|
advanceHitSelf:
|
|
SETD.0 Dead
|
|
INIA 0x01
|
|
STA.0
|
|
RET
|
|
|
|
; Where the head would go, or a wall. The row is the high nibble of a square number and
|
|
; the column is the low one, so every edge of the board is a question about one nibble.
|
|
step:
|
|
SETD.0 HeadCell
|
|
LDA.0
|
|
SETD.0 Direction
|
|
LDB.0
|
|
BRB stepUp
|
|
DECB
|
|
BRB stepDown
|
|
DECB
|
|
BRB stepLeft
|
|
BRI stepRight
|
|
|
|
stepUp:
|
|
INIB 0xF0
|
|
AND
|
|
BRQ stepWall ; The top row is where the high nibble is zero.
|
|
CCF
|
|
INIB 0x10
|
|
SUB
|
|
BRI stepMoved
|
|
|
|
stepDown:
|
|
PSHA
|
|
INIB 0xF0
|
|
AND
|
|
MVQA
|
|
INIB 0xF0
|
|
XOR
|
|
POPA
|
|
BRQ stepWall ; The bottom row is where the high nibble is fifteen.
|
|
CCF
|
|
INIB 0x10
|
|
ADD
|
|
BRI stepMoved
|
|
|
|
stepLeft:
|
|
INIB 0x0F
|
|
AND
|
|
BRQ stepWall
|
|
CCF
|
|
INIB 0x01
|
|
SUB
|
|
BRI stepMoved
|
|
|
|
stepRight:
|
|
PSHA
|
|
INIB 0x0F
|
|
AND
|
|
MVQA
|
|
INIB 0x0F
|
|
XOR
|
|
POPA
|
|
BRQ stepWall
|
|
CCF
|
|
INIB 0x01
|
|
ADD
|
|
|
|
stepMoved:
|
|
MVQA
|
|
SETD.0 NextCell
|
|
STA.0
|
|
RET
|
|
|
|
stepWall:
|
|
SETD.0 Dead
|
|
INIA 0x01
|
|
STA.0
|
|
RET
|
|
|
|
addHead:
|
|
SETD.0 NextCell
|
|
LDA.0
|
|
CALL cellPointer
|
|
INIB 0x01
|
|
STB.3
|
|
|
|
SETD.0 BodyHead
|
|
LDA.0
|
|
INCA
|
|
STA.0 ; The ring wraps at 256 on its own, which is why it is a page.
|
|
CALL bodyPointer
|
|
SETD.0 NextCell
|
|
LDA.0
|
|
STA.3
|
|
|
|
SETD.0 HeadCell
|
|
STA.0
|
|
SETD.0 Length
|
|
LDA.0
|
|
INCA
|
|
STA.0
|
|
RET
|
|
|
|
; Q is the square the oldest segment is standing on.
|
|
tailCell:
|
|
SETD.0 BodyTail
|
|
LDA.0
|
|
CALL bodyPointer
|
|
LDA.3
|
|
RSTB
|
|
OR
|
|
RET
|
|
|
|
removeTail:
|
|
SETD.0 BodyTail
|
|
LDA.0
|
|
CALL bodyPointer
|
|
LDA.3 ; Which square the oldest segment is standing on.
|
|
CALL cellPointer
|
|
RSTB
|
|
STB.3
|
|
|
|
SETD.0 BodyTail
|
|
LDA.0
|
|
INCA
|
|
STA.0
|
|
SETD.0 Length
|
|
LDA.0
|
|
DECA
|
|
STA.0
|
|
RET
|
|
|
|
; ---- Drawing ----
|
|
;
|
|
; The whole board, every frame, from the top left corner. Sixteen by sixteen is small
|
|
; enough that working out what changed would cost more than sending it all again.
|
|
draw:
|
|
SETD.0 CursorHome
|
|
CALL printString
|
|
SETD.0 BorderText
|
|
CALL printString
|
|
CALL newLine
|
|
|
|
SETD.2 Board ; Walks the board a square at a time, in order.
|
|
RSTB ; Which square, which is also its row and column.
|
|
drawRow:
|
|
INIA 0x7C ; |
|
|
OUTA 0x00
|
|
drawSquare:
|
|
SETD.0 HeadCell
|
|
LDA.0
|
|
XOR ; Zero on the one square the head is standing on.
|
|
BRQ drawHead
|
|
LDA.2
|
|
BRA drawEmpty
|
|
DECA
|
|
BRA drawBody
|
|
INIA 0x2A ; *
|
|
BRI drawPut
|
|
drawHead:
|
|
INIA 0x40 ; @
|
|
BRI drawPut
|
|
drawBody:
|
|
INIA 0x23 ; #
|
|
BRI drawPut
|
|
drawEmpty:
|
|
INIA 0x20
|
|
drawPut:
|
|
OUTA 0x00
|
|
INCD.2
|
|
INCB
|
|
INIA 0x0F
|
|
AND
|
|
BNQ drawSquare ; Sixteen to a row.
|
|
INIA 0x7C
|
|
OUTA 0x00
|
|
CALL newLine
|
|
BNB drawRow ; And sixteen rows, after which B is back to zero.
|
|
|
|
SETD.0 BorderText
|
|
CALL printString
|
|
CALL newLine
|
|
SETD.0 ScoreText
|
|
CALL printString
|
|
SETD.0 Score
|
|
LDA.0
|
|
CALL printByteDecimal
|
|
SETD.0 KeysText
|
|
CALL printString
|
|
CALL newLine
|
|
RET
|
|
|
|
; ---- Waiting ----
|
|
;
|
|
; There is no clock on this machine, so time is counted in instructions. At the emulated
|
|
; rate this is about an eighth of a second, which is a speed a person can play at. Running
|
|
; the emulator faster or slower moves it, and that is the honest answer: the machine has
|
|
; no way to know how long a second is and this program is not going to pretend it does.
|
|
pause:
|
|
RSTB
|
|
pauseOuter:
|
|
RSTA
|
|
pauseInner:
|
|
DECA
|
|
BNA pauseInner
|
|
DECB
|
|
BNB pauseOuter
|
|
RET
|
|
|
|
#Data
|
|
|
|
#Base 0x2000
|
|
|
|
HeadCell:
|
|
0x00
|
|
NextCell:
|
|
0x00
|
|
Direction:
|
|
0x00
|
|
BodyHead:
|
|
0x00
|
|
BodyTail:
|
|
0x00
|
|
Length:
|
|
0x00
|
|
Score:
|
|
0x00
|
|
Dead:
|
|
0x00
|
|
Quitting:
|
|
0x00
|
|
Won:
|
|
0x00
|
|
FoodStart:
|
|
0x00
|
|
|
|
; A square number written into the low byte of one of these makes it the address of that
|
|
; square. The high byte is set once at the start and never changes, which is the whole
|
|
; reason both regions are page aligned.
|
|
CellAddress:
|
|
0x00 0x00
|
|
BodyAddress:
|
|
0x00 0x00
|
|
|
|
; Anything but zero will do, because a shift register that reaches zero stays there.
|
|
RandomSeed:
|
|
0xAC 0xE1
|
|
RandomState:
|
|
0x00 0x00
|
|
|
|
ClearScreen:
|
|
0x1B
|
|
"[2J"
|
|
CursorHome:
|
|
0x1B
|
|
"[H"
|
|
|
|
BorderText:
|
|
"+----------------+"
|
|
ScoreText:
|
|
" score "
|
|
KeysText:
|
|
" wasd steers, q stops"
|
|
|
|
DeadText:
|
|
"you ran into something"
|
|
QuitText:
|
|
"stopped"
|
|
WonText:
|
|
"the board is full and there is nothing left to eat"
|
|
|
|
#Align 0x0100
|
|
Board:
|
|
#Reserve 0d256
|
|
Body:
|
|
#Reserve 0d256
|
|
|
|
#Include console.asm
|