The console had grown an ANSI parser, and that was the wrong shape. ANSI exists because a screen used to be on the other end of a serial line and a byte stream was the only channel there was. This screen is memory the program can already address, so reaching it by sending characters for a state machine to take apart is a middleman for something the machine does better - and it meant accepting an open protocol somebody else defines, in hardware, with no natural end to it. Everything else on this machine is registers. So the console gets three: cursor row at 0x03, cursor column at 0x04, and a command port at 0x05 where 1 clears the screen. Both cursor registers are READ as well as written, which is the thing an escape cannot do without sending a query and parsing a reply - a routine that wants to put the cursor back where it found it can now ask. Clearing is one command against a thousand cells walked one at a time. Snake and Life are smaller for it: 2,168 bytes to 2,163 and 1,410 to 1,396. A HOST TERMINAL STILL SPEAKS ANSI, and bridging to the host is the emulator's job, the same job it does reading standard input. So the escapes are now GENERATED, outbound, for the set this device chooses, rather than parsed inbound as though the machine were a terminal. The set cannot grow behind our backs because we are the ones saying it. The cursor is announced lazily, at the next character rather than at the register write, so setting a row and a column costs one sequence rather than two. The console's block widens from three ports to six, which registryTest noticed: it had been asking about port 0x05 precisely BECAUSE nothing was there, and the console had just moved in. Re-blessing it would have left it checking nothing, so it asks about 0x80 instead - clear of the console, the disk, the screen, the controller, and the sound device coming to 0x40. Six checks in Tests/video.sh swapped from the sequences to the registers, including that the cursor reads back and that one sent past the edge is clamped rather than refusing. Those checks also stopped counting bytes from the ends of a file, which had quietly started measuring an escape the moment the console began announcing the cursor. SplitLint caught the one thing worth catching in the port: the clear command leaves A at 1 and key mode is also 1, so the second load looks redundant. Acting on it would tie a console command to a console mode by coincidence, and break silently if either ever moved, so it is suppressed with that reason rather than removed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
315 lines
3.8 KiB
NASM
315 lines
3.8 KiB
NASM
; Conway's Game of Life for SplitBit.
|
|
;
|
|
; The visible field is 16x16, surrounded by a permanently dead one-cell border.
|
|
; Each of the 18x18 allocated cells is stored as two adjacent bytes:
|
|
; current state, next state
|
|
; This makes double buffering possible with SplitBit's single Data Pointer.
|
|
;
|
|
; The initial pattern is a glider. ANSI terminal control codes redraw the field
|
|
; in place. Press Ctrl-C to stop the emulator.
|
|
|
|
#Include print.asm
|
|
|
|
#Program
|
|
|
|
start:
|
|
CALL seedGlider
|
|
INIA 0x01
|
|
OUTA 0x05 ; Console command: clear the screen
|
|
|
|
generationLoop:
|
|
CALL renderBoard
|
|
CALL evolveBoard
|
|
CALL commitBoard
|
|
CALL frameDelay
|
|
BRI generationLoop
|
|
|
|
seedGlider:
|
|
; Coordinates in the padded field:
|
|
; .#.
|
|
; ..#
|
|
; ###
|
|
SETD Board
|
|
DPUP 0d42
|
|
INIA 0x01
|
|
STA
|
|
SETD Board
|
|
DPUP 0d80
|
|
STA
|
|
SETD Board
|
|
DPUP 0d112
|
|
STA
|
|
SETD Board
|
|
DPUP 0d114
|
|
STA
|
|
SETD Board
|
|
DPUP 0d116
|
|
STA
|
|
RET
|
|
|
|
renderBoard:
|
|
RSTA
|
|
OUTA 0x03
|
|
OUTA 0x04 ; Cursor to row 0, column 0
|
|
SETD Board
|
|
DPUP 0d38
|
|
INIA 0d16
|
|
SETD RowCount
|
|
STA
|
|
SETD Board
|
|
DPUP 0d38
|
|
|
|
renderRow:
|
|
PSHD
|
|
INIA 0d16
|
|
SETD ColCount
|
|
STA
|
|
POPD
|
|
|
|
renderCell:
|
|
LDA
|
|
BRA renderDead
|
|
INIB 0x23
|
|
OUTB 0x00
|
|
BRI renderCellDone
|
|
renderDead:
|
|
INIB 0x20
|
|
OUTB 0x00
|
|
renderCellDone:
|
|
DPUP 0d02
|
|
PSHD
|
|
SETD ColCount
|
|
LDA
|
|
DECA
|
|
STA
|
|
BRA renderRowDone
|
|
POPD
|
|
BRI renderCell
|
|
|
|
renderRowDone:
|
|
POPD
|
|
CALL lineFeed
|
|
DPUP 0d04
|
|
PSHD
|
|
SETD RowCount
|
|
LDA
|
|
DECA
|
|
STA
|
|
BRA renderDone
|
|
POPD
|
|
BRI renderRow
|
|
|
|
renderDone:
|
|
POPD
|
|
RET
|
|
|
|
evolveBoard:
|
|
SETD Board
|
|
DPUP 0d38
|
|
INIA 0d16
|
|
SETD RowCount
|
|
STA
|
|
SETD Board
|
|
DPUP 0d38
|
|
|
|
evolveRow:
|
|
PSHD
|
|
INIA 0d16
|
|
SETD ColCount
|
|
STA
|
|
POPD
|
|
|
|
evolveCellLoop:
|
|
CALL evolveCell
|
|
DPUP 0d02
|
|
PSHD
|
|
SETD ColCount
|
|
LDA
|
|
DECA
|
|
STA
|
|
BRA evolveRowDone
|
|
POPD
|
|
BRI evolveCellLoop
|
|
|
|
evolveRowDone:
|
|
POPD
|
|
DPUP 0d04
|
|
PSHD
|
|
SETD RowCount
|
|
LDA
|
|
DECA
|
|
STA
|
|
BRA evolveDone
|
|
POPD
|
|
BRI evolveRow
|
|
|
|
evolveDone:
|
|
POPD
|
|
RET
|
|
|
|
; DP points to the current-state byte of a visible cell.
|
|
evolveCell:
|
|
CALL countNeighbors
|
|
PSHQ
|
|
LDA
|
|
POPB
|
|
|
|
; Three neighbors always produces a live cell.
|
|
INIA 0d03
|
|
CCF
|
|
SUB
|
|
BRQ makeAlive
|
|
|
|
; Two neighbors preserve the current state.
|
|
INIA 0d02
|
|
CCF
|
|
SUB
|
|
BRQ preserveCell
|
|
|
|
makeDead:
|
|
RSTA
|
|
INCD
|
|
STA
|
|
DECD
|
|
RET
|
|
|
|
preserveCell:
|
|
LDA
|
|
BRA makeDead
|
|
|
|
makeAlive:
|
|
INIA 0x01
|
|
INCD
|
|
STA
|
|
DECD
|
|
RET
|
|
|
|
; Return the sum of the eight neighboring current-state bytes in Q.
|
|
; With interleaved cells and an 18-cell row, the relative offsets are:
|
|
; -38, -36, -34, -2, +2, +34, +36, +38.
|
|
countNeighbors:
|
|
RSTA
|
|
DPDN 0d38
|
|
|
|
LDB
|
|
CCF
|
|
ADD
|
|
MVQA
|
|
DPUP 0d02
|
|
|
|
LDB
|
|
CCF
|
|
ADD
|
|
MVQA
|
|
DPUP 0d02
|
|
|
|
LDB
|
|
CCF
|
|
ADD
|
|
MVQA
|
|
DPUP 0d32
|
|
|
|
LDB
|
|
CCF
|
|
ADD
|
|
MVQA
|
|
DPUP 0d04
|
|
|
|
LDB
|
|
CCF
|
|
ADD
|
|
MVQA
|
|
DPUP 0d32
|
|
|
|
LDB
|
|
CCF
|
|
ADD
|
|
MVQA
|
|
DPUP 0d02
|
|
|
|
LDB
|
|
CCF
|
|
ADD
|
|
MVQA
|
|
DPUP 0d02
|
|
|
|
LDB
|
|
CCF
|
|
ADD
|
|
RET
|
|
|
|
; Copy every next-state byte to its adjacent current-state byte.
|
|
; The border's next bytes remain zero, so it stays permanently dead.
|
|
commitBoard:
|
|
SETD Board
|
|
INIA 0d18
|
|
SETD RowCount
|
|
STA
|
|
SETD Board
|
|
|
|
commitRow:
|
|
PSHD
|
|
INIA 0d18
|
|
SETD ColCount
|
|
STA
|
|
POPD
|
|
|
|
commitCell:
|
|
INCD
|
|
LDA
|
|
DECD
|
|
STA
|
|
DPUP 0d02
|
|
PSHD
|
|
SETD ColCount
|
|
LDA
|
|
DECA
|
|
STA
|
|
BRA commitRowDone
|
|
POPD
|
|
BRI commitCell
|
|
|
|
commitRowDone:
|
|
POPD
|
|
PSHD
|
|
SETD RowCount
|
|
LDA
|
|
DECA
|
|
STA
|
|
BRA commitDone
|
|
POPD
|
|
BRI commitRow
|
|
|
|
commitDone:
|
|
POPD
|
|
RET
|
|
|
|
; Approximately 0.2 seconds at the emulator's nominal 1 MHz rate.
|
|
frameDelay:
|
|
INIA 0xFF
|
|
delayOuter:
|
|
INIB 0xFF
|
|
delayInner:
|
|
DECB
|
|
BRB delayInnerDone
|
|
BRI delayInner
|
|
delayInnerDone:
|
|
DECA
|
|
BRA delayDone
|
|
BRI delayOuter
|
|
delayDone:
|
|
RET
|
|
|
|
#Data
|
|
|
|
RowCount:
|
|
0x00
|
|
ColCount:
|
|
0x00
|
|
|
|
|
|
; The emulator zero-fills the remainder of Data Memory. Board names the first
|
|
; byte of a 648-byte logical allocation (18 * 18 * 2).
|
|
Board:
|
|
0x00
|