Programs can now pin specific routines to specific vectors in SplitBit assembly. Added snake game.

This commit is contained in:
Anachronaut
2026-08-17 19:12:42 -04:00
parent 08624925fe
commit 9e3425d34b
18 changed files with 1383 additions and 47 deletions
+672
View File
@@ -0,0 +1,672 @@
; 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. The console can raise an interrupt line when a byte
; arrives, which is the better shape for a game: the loop would never look at the console
; at all. A loaded program cannot use it. Installing a handler means putting an address in
; the vector table, and the loadable program format carries only code and data - a program
; that is not the one the machine booted from has no way to say what its vectors are. So
; interrupts belong to boot images for now, and this asks once a frame, which is what the
; machines this one is pretending to be did anyway.
;
; 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 0x2000
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 0x1000
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
+14 -7
View File
@@ -5,15 +5,22 @@
; name, because a line with a name and nothing after it declares what a vector is called
; and what number it has without claiming to implement it.
;
; The order here is what fixes the numbers, and it is fixed in one file, so the two sides
; cannot disagree about them and nobody has to write a number down. Adding a service goes
; at the end: putting one in the middle would renumber everything after it, and any
; program already assembled against the old numbers would call the wrong thing.
; THE NUMBERS ARE WRITTEN DOWN HERE, and that is the only place they are written. They
; used to be decided by the order of the lines, which worked and was quietly fragile: a
; service inserted in the middle renumbered everything after it, and a program already
; assembled against the old numbers would go on calling the number rather than the name.
; Worse, the numbers a program got for its OWN traps moved depending on whether it had
; included this file, and a program that had not was given 16 - which is osPrintString.
;
; So these are pinned. They come from the range set aside for numbers that two separately
; assembled programs have to agree about; everything a program names for itself is drawn
; from higher up and cannot collide with these however it is built. Adding a service takes
; the next free number here and disturbs nothing.
;
; Written by Anachronaut
#Vectors
osPrintString ; DP0 names a string. Prints it.
osReadLine ; DP0 names somewhere to put a line read from the console.
osExit ; Give the machine back to the system.
osPrintString 0d16 ; DP0 names a string. Prints it.
osReadLine 0d17 ; DP0 names somewhere to put a line read from the console.
osExit 0d18 ; Give the machine back to the system.
@@ -0,0 +1,19 @@
; A vector pinned to a number that is not anybody's to give.
;
; 100 is in the range the assembler hands out by itself. A program that pinned one there
; would be claiming a number that the assembler might also give to the next trap somebody
; declared, which is exactly the collision pinning exists to prevent.
#Program
start:
SWI mine
HALT
myHandler:
RETI
#Vectors
Boot start
mine 0d100 myHandler
@@ -0,0 +1,28 @@
; Two vectors pinned to the same number.
;
; This is the mistake pinning makes possible: numbers the assembler hands out cannot
; collide, and numbers a person writes down can. Since the whole point of a pinned number
; is that something outside this program is going to use it, two names sharing one is a
; disagreement that has to be caught here rather than discovered by whatever calls the
; wrong one.
;
; The service names in services.asm sit at 16, 17 and 18, so a program that includes them
; and pins its own trap at one of those is caught by this same check.
#Program
start:
SWI first
HALT
firstHandler:
RETI
secondHandler:
RETI
#Vectors
Boot start
first 0d20 firstHandler
second 0d20 secondHandler
@@ -0,0 +1,78 @@
; Vectors with numbers written down, and vectors numbered by the assembler, in one program.
;
; A software vector number matters only when two separately assembled programs have to
; agree about it. The system's services are the case that exists today: something calls
; osExit without having been assembled alongside whatever implements it, so both sides
; have to mean the same number by that name.
;
; Numbers from 16 to 63 are for exactly that and are never handed out. Everything from 64
; up is handed out in the order it is written, and belongs to one program.
;
; This pins one at each end of the reserved range and leaves a third to the assembler. All
; three are then called by name, which is the only way a program ever refers to one - the
; number is a fact about the machine, not something a program says twice.
;
; Correct output is:
; pinned vectors
; twenty
; sixty three
; automatic
; done
#Include console.asm
#Program
start:
SETD.0 Banner
CALL printString
CALL newLine
SWI atTwenty
SWI atSixtyThree
SWI automatic
SETD.0 DoneText
CALL printString
CALL newLine
HALT
; Each of these says which one it is, so a number going to the wrong place is a wrong word
; rather than a silence.
twentyHandler:
SETD.0 TwentyText
CALL printString
CALL newLine
RETI
sixtyThreeHandler:
SETD.0 SixtyThreeText
CALL printString
CALL newLine
RETI
automaticHandler:
SETD.0 AutomaticText
CALL printString
CALL newLine
RETI
#Data
Banner:
"pinned vectors"
TwentyText:
"twenty"
SixtyThreeText:
"sixty three"
AutomaticText:
"automatic"
DoneText:
"done"
#Vectors
Boot start
atTwenty 0d20 twentyHandler
atSixtyThree 0d63 sixtyThreeHandler
automatic automaticHandler