George hat es so erklärt - ich verstehe nur wenig davon :rofl muss ich mir genauer ansehen.
It uses what you might call return-oriented programming.
It operates in two passes. The first pass, the census, goes over each cell and if the cell is live it increments the population of all its neighbours. That increment happens on a second array I call "nature" because it makes the life and death decisions.
In the second pass, nature, it knows if a cell was alive or dead and how many neighbours it has. If a cell was dead and has no neighbours it does nothing. If a cell was dead and is now alive then it draws it on screen and makes a note in the census. If a cell was alive and should be dead it erases it on sceen and updates the census. And, in any case, if the cell has any neighbours it sets the count back to zero. So when we're done we're all cleared out and ready for the next pass.
Major advantages here is that it does nothing with dead cells in the census pass and mostly nothing with unchanged cells in the nature pass.
The census is just 1024 addresses. To process it, you set the stack pointer to it and do a RET. This will go through all 1024 cells. For dead cells the address points to a RET. Which means do nothing and keep stepping through the census.
For live cells the address jumps to a subroutine which increments the population count for all the neighbours in the nature array.
The nature array is similar but instead of 2 states it has 18. That accounts for a cell having 0 .. 8 live neighbours and it being currently alive or dead. Initially it is cleared out to all $C000 or something like that. Bit 12 is set if the cell is alive. So that's $E000 and bits 8 through 11 are the number of live neighbours. So $C300 would be a dead cell with 3 neighbours. That means the $C300 is a subroutine that will draw a cell on the screen and put the live cell marker into the census.
G Phillips
—
1/25/26, 12:08 AM
When it has done that it sets itself to $E000 indicating that the cell is alive and has no neighbours (i.e., ready for the census pass).
What about, say, $E300. Well, that's just like $C300 except that the cell remains alive. So it does what $C300 does but doesn't draw a on the screen and doesn't update the census since nothing has changed in either place.
Since the $Cx and $Ex are the high byte of an address, all the census needs to do is increment the high byte of the right addresses in the nature array.
The census array in the program has some complications. It has special handlers for the corners and edges so they don't try to increment neighbours which don't exist.
And both arrays use 256 bytes for an entire row. Was more than the 128 (or 130) they need but it means it is easy to compute the next or previous row address. If HL points to a cell then INC H will go to the next row and DEC H to to the previous.
This also means that at the end of each row there are special addresses to skip the stack pointer to the start of the next row. Or, at the very end, it just goes to a routine that moves to the next phase.
Hier ist sein code für splife; die schnelleren Versionen sind noch abgefahrener, das spare ich mir hier:
;
; splife.z - abuse the stack to do Conway's Game of Life quickly.
;
; Starts with a random pattern. Key functions:
; R load a new random pattern
; G load a glider gun
; space-bar pause while held
; break return to TRS-DOS
;
; TODO:
; Torus geometry option.
; Measurements.
; Population count option.
; Generation count option.
; 64 x 48 resolution
; Self-generate code instead of leaning on zmac macros.
;
; Quick measurement:
; zmac splife.z
; trs80gp -m3 -fa do_census zout\splife.bds
org $6000
start: di
in a,($ff)
and ~$20 ; turn off video waits
out ($ec),a
ld (stack),sp
new_rand:
call clear
call randfill
jp do_nature
stack: defw 0
; HL = pattern
load_pattern:
ld de,nature+1
ld a,e
add (hl)
add (hl)
inc hl
ld e,a
ld a,d
add (hl)
inc hl
ld d,a
ld c,e
llp: ld a,(hl)
inc hl
cp '$'
ret z
or a
jr z,newline
cp '#'
jr nz,nocell
ld a,high(ntdead)+$3
ld (de),a
nocell: inc e
inc e
jr llp
newline:
ld e,c
inc d
jr llp
clear: ld hl,$3c00
ld de,$3c00+1
ld bc,1024-1
ld (hl),128
ldir
ld hl,census
ld c,16
clrow: ld b,64
clcol: res 1,(hl)
inc l
inc l
djnz clcol
inc h
ld l,0
dec c
jr nz,clrow
ret
glider_gun:
defb 1,0
ascii '........................#',0
ascii '......................#.#',0
ascii '............##......##............##',0
ascii '...........#...#....##............##',0
ascii '##........#.....#...##',0
ascii '##........#...#.##....#.#',0
ascii '..........#.....#.......#',0
ascii '...........#...#',0
ascii '............##'
ascii '$'
next_nature_row:
ld hl,256-(64+1)*2
add hl,sp
ld sp,hl
ret
end_nature:
ld a,($38ff)
or a
jr z,do_census ; no keys to check
ld sp,(stack)
ld hl,$0404 ; 'R'
call key
jp c,new_rand
ld hl,$0440 ; break
call key
ret c
ld hl,$8040 ; space
call key ; pause while held
ld hl,$8001 ; 'G'
call key
jr nc,do_census
call clear
ld hl,glider_gun
call load_pattern
jp do_nature
key: ld a,h
ld b,h
ld h,$38
and (hl)
ret z
wtkey: ld a,b
and (hl)
jr nz,wtkey
ccf
ret
; map from census to nature
c2n macro dx,dy
c2n_off = nature-census-2+256*dy+2*dx+1
endm
do_census:
ld sp,census
c2n -1,-1
ld de,c2n_off ; mapping from census to nature
ld bc,256-64*2 ; skip to next row of census
ret
; Handlers for census bytes. There are a pair of handlers dead/alive
; cells for each position on the grid. The pair must start on an four
; byte boundary so clearing bit 1 will kill it and setting bit 1 will
; bring it alive. The corners, sides and interior all have their own
; handler because of their different neighbourhoods.
;
; top_left top top_right
; +--------------------------------------------------------------+
; | |
; | |
; left | middle | right
; | |
; | |
; +--------------------------------------------------------------+
; bottom_left bottom bottom_right
;
; Right right-hand side handlers must also bring the stack to the next line.
; And the bottom-right finishes the census pass.
;
; The macro generating each grid case uses these numbers as the positions
; around a cell:
;
; 0 1 2
; 7 xxx 3
; 6 5 4
count macro name,pos,len,special
local special_op
org ($+3)/4*4
name:
; cell dead
if special
jr special_op ; TODO - small speedup by wasting space
else
ret
nop
endif
; cell alive
ldhloff pos
add hl,sp
inc (hl)
npos = pos
rept len-1
if npos == 0 || npos == 1
inc l
inc l
endif
if npos == 2 || npos == 3
inc h
endif
if npos == 4 || npos == 5
dec l
dec l
endif
if npos == 6 || npos == 7
dec h
endif
inc (hl)
npos++
npos %= 8
endm
special_op:
endm
ldhloff macro pos
if pos == 0
ld h,d ; offset for middle case is pre-loaded
ld l,e
endif
if pos == 1 ; .#.
c2n 0,-1 ; .+.
endif ; ...
if pos == 2 ; ..#
c2n 1,-1 ; .+.
endif ; ...
if pos == 3 ; ...
c2n 1,0 ; .+#
endif ; ...
if pos == 4 ; ...
c2n 1,1 ; .+.
endif ; ..#
if pos == 5 ; ...
c2n 0,1 ; .+.
endif ; .#.
if pos == 6 ; ...
c2n -1,1 ; .+.
endif ; #..
if pos == 7 ; ...
c2n -1,0 ; #+.
endif ; ...
if pos > 0
ld hl,c2n_off
endif
endm
census_next_row macro
ld h,b
ld l,c
add hl,sp
ld sp,hl
endm
count top_left,3,3,0
ret
count top,3,5,0
ret
count top_right,5,3,1
census_next_row
ret
count left,1,5,0
ret
count middle,0,8,0
ret
count right,5,5,1
census_next_row
ret
count bottom_left,1,3,0
ret
count bottom,7,5,0
ret
count bottom_right,7,3,1
do_nature:
ld sp,nature
ld de,ntdead
ld bc,ntlive
ret
census_row macro c_left,c_middle,c_right
defw c_left
rept 64-2
defw c_middle
endm
defw c_right
org ($+255) & 0xff00
endm
org $8000 ; start of census processor
census:
census_row top_left,top,top_right
rept 16-2
census_row left,middle,right
endm
census_row bottom_left,bottom,bottom_right
org $9200 ; start of nature processor
nature:
rept 16
base = $
rept 64
defw ntdead
endm
defw next_nature_row
org base+256
endm
base = $
defw end_nature
org base+256 ; safety zone (unnecessary?)
screen:
scr_addr = $3c00
rept 16
base = $
rept 64
defw scr_addr
scr_addr++
endm
org base+256
endm
change macro is_alive
if is_alive
do_change bc,$bf,set
else
do_change de,$80,res
endif
endm
do_change macro tmp,ch,setres
push tmp
; update screen
ld hl,screen-nature
add hl,sp
ld a,(hl)
inc hl
ld h,(hl)
ld l,a
ld (hl),ch
; update census
ld hl,census-nature
add hl,sp
setres 1,(hl)
pop tmp
endm
org $c000 ; start of nature handlers
ntdead:
st = 0
rept 18 ; 0 .. 8 for dead, 9 .. 17 for live
if st == 9
ntlive:
endif
neigh = st % 9
live = st / 9
new_live = neigh == 3 || (live && neigh == 2)
if new_live != live
change new_live
else
if neigh > 0
if new_live
push bc
pop bc
else
push de
pop de
endif
endif
endif
ret
st++
org ($+255) & $ff00
endm
; Borrowed and tweaked from game_of_life_m3.asm
randfill:
ld hl,nature+1
ld c,16
r0: ld b,64
r1: push hl
call xrnd
rr l
pop hl
jr nc,not_live
ld (hl),high(ntdead)+3
not_live:
inc l
inc l
djnz r1
inc h
ld l,low(nature+1)
dec c
jr nz,r0
ret
; 16-bit xorshift pseudorandom number generator by John Metcalf
; 20 bytes, 86 cycles (excluding ret)
; returns hl = pseudorandom number
; corrupts a
; generates 16-bit pseudorandom numbers with a period of 65535
; using the xorshift method:
; hl ^= hl << 7
; hl ^= hl >> 9
; hl ^= hl << 8
; some alternative shift triplets which also perform well are:
; 6, 7, 13; 7, 9, 13; 9, 7, 13.
xrnd:
ld hl,1
org $-2
defw 1 ; seed must not be 0
ld a,h
rra
ld a,l
rra
xor h
ld h,a
ld a,l
rra
ld a,h
rra
xor l
ld l,a
xor h
ld h,a
ld (xrnd+1),hl
ret
end start