!cpu 6502
!to "gol_battler.prg", cbm
; =======================================================================
; CONWAY'S GAME OF LIFE - AUTO-BATTLER SCREENSAVER
; Copyright (c) nitetime.net
; Generated with the assistance of Gemini AI
; Target: Commodore 128 (Native Mode)
; Compiler: ACME Cross-Assembler
; =======================================================================
; --- System Constants ---
VIDRAM = $0400
COLORRAM = $D800
BORDER = $D020
BGCOLOR = $D021
SID_FREQ3_L = $D40E
SID_FREQ3_H = $D40F
SID_CTRL3 = $D412
SID_NOISE = $D41B
; --- Zero Page Variables ---
; Using safe workspace ranges for Native ML execution
PTR_TOP = $50
PTR_MID = $52
PTR_BOT = $54
PTR_NXT = $56
PTR_PRV = $58
DRAW_PTR = $5A
SCR_PTR = $5C
NEIGH_CNT = $5E ; [FIX 1] Holds neighbor sum to safely load current cell
PG_CUR = $5F ; [IMPROVEMENT] Rotating high-byte for Current Board ($20, $24, $28)
PG_NXT = $60 ; [IMPROVEMENT] Rotating high-byte for Next Board
PG_PRV = $61 ; [IMPROVEMENT] Rotating high-byte for Previous Board
SCORE_L = $62
SCORE_H = $63
HISCORE_L = $64
HISCORE_H = $65
FLAG_CHANGE = $66
FLAG_PRV_CH = $67
ROW_CNT = $68
; =======================================================================
; C128 BASIC Header (10 SYS 7181)
; [FIX 2] Properly structured BASIC loader with dynamic Next-Line link.
; =======================================================================
* = $1C01
!word BasicEnd ; Link to next BASIC line
!word 10 ; Line Number (10)
!byte $9E ; SYS token
!byte $37,$31,$38,$31 ; ASCII "7181" (Address $1C0D)
!byte 0 ; End of Line 10
BasicEnd:
!word 0 ; End of BASIC program markers
; Engine entry point starts at exactly $1C0D
* = $1C0D
Start:
; Init SID Voice 3 for Hardware RNG
lda #$FF
sta SID_FREQ3_L
sta SID_FREQ3_H
lda #$80 ; Noise Waveform
sta SID_CTRL3
; Init Variables
lda #0
sta HISCORE_L
sta HISCORE_H
sta BORDER
sta BGCOLOR
jsr ClearScreen
jsr InitColors
NewRun:
; Reset Run Variables
lda #0
sta SCORE_L
sta SCORE_H
sta BORDER
; Set initial memory banks for the 3 rolling buffers
lda #$20 ; $2000
sta PG_CUR
lda #$24 ; $2400
sta PG_NXT
lda #$28 ; $2800
sta PG_PRV
; [FIX 5] Completely wipe all three buffers to 0 before starting
; This ensures no leftover data triggers a false period-2 loop on Gen 1.
jsr ClearAllBoards
jsr SeedBoard
MainLoop:
jsr DrawBoard
jsr UpdateScore ; [FIX 3] Update scores here so the HUD remains visible during CalcNext
jsr CalcNext
; Loop Detection:
; FLAG_CHANGE = 0 -> NXT == CUR (Fixed Point)
; FLAG_PRV_CH = 0 -> NXT == PRV (Period-2 Oscillator)
lda FLAG_CHANGE
beq .endOfRun
lda FLAG_PRV_CH
beq .endOfRun
jsr SwapBoards ; [IMPROVEMENT] Swaps high-byte ZP pointers instead of 1000-byte copies
jmp MainLoop
.endOfRun:
; Delay briefly so the viewer sees the final resting state
ldx #0
ldy #0
.delay:
iny
bne .delay
inx
bne .delay
jmp NewRun
; =======================================================================
; Subroutines
; =======================================================================
ClearAllBoards:
; Wipes $2000 through $2BFF (all 3 buffers + padding)
lda #0
tay
.clrLp:
sta $2000,y
sta $2100,y
sta $2200,y
sta $2300,y
sta $2400,y
sta $2500,y
sta $2600,y
sta $2700,y
sta $2800,y
sta $2900,y
sta $2A00,y
sta $2B00,y
iny
bne .clrLp
rts
SeedBoard:
; [FIX 6] Only seeds rows 1-23 and cols 1-38 to preserve a dead outer border.
; [IMPROVEMENT] Adjusted density to 25% for better chaotic battles.
lda #40
sta DRAW_PTR
lda PG_NXT
sta DRAW_PTR+1
ldx #23 ; 23 inner rows
.sRow:
ldy #1 ; Skip col 0
.sCol:
lda SID_NOISE
and #$03 ; Mask to 0, 1, 2, or 3
cmp #$01 ; Is it 0?
bcc .sAlive ; 25% chance of spawning alive
lda #0
beq .sStore
.sAlive:
lda #1
.sStore:
sta (DRAW_PTR),y
iny
cpy #39 ; Skip col 39
bne .sCol
; Advance line
lda DRAW_PTR
clc
adc #40
sta DRAW_PTR
bcc +
inc DRAW_PTR+1
+ dex
bne .sRow
jsr SwapBoards
rts
ClearScreen:
; Clear entire screen to spaces ($20)
lda #$20
ldx #0
.csLoop:
sta VIDRAM,x
sta VIDRAM+250,x
sta VIDRAM+500,x
sta VIDRAM+750,x
inx
cpx #250
bne .csLoop
rts
InitColors:
; Paint the screen memory with Green ($05)
ldx #0
lda #$05
.colLp:
sta COLORRAM,x
sta COLORRAM+250,x
sta COLORRAM+500,x
sta COLORRAM+750,x
inx
cpx #250
bne .colLp
rts
DrawBoard:
; [FIX 3] Only draws rows 1-23. The HUD on Row 0 is never overwritten.
lda #40
sta DRAW_PTR
lda PG_CUR
sta DRAW_PTR+1
lda #<(VIDRAM+40)
sta SCR_PTR
lda #>(VIDRAM+40)
sta SCR_PTR+1
ldx #23 ; 23 Rows
.rLoop:
ldy #1 ; Skip col 0
.cLoop:
lda (DRAW_PTR),y
beq .space
lda #$51 ; Solid Ball
bne .draw
.space:
lda #$20 ; Empty Space
.draw:
sta (SCR_PTR),y
iny
cpy #39 ; Skip col 39
bne .cLoop
; Advance Pointers by 40
lda DRAW_PTR
clc
adc #40
sta DRAW_PTR
bcc +
inc DRAW_PTR+1
+
lda SCR_PTR
clc
adc #40
sta SCR_PTR
bcc +
inc SCR_PTR+1
+
dex
bne .rLoop
rts
SwapBoards:
; [IMPROVEMENT] Pointer rotation instead of 1000-byte RAM copy
; PRV = CUR, CUR = NXT, NXT = PRV (Recycle old PRV as new blank NXT)
ldx PG_PRV
lda PG_CUR
sta PG_PRV
ldy PG_NXT
sty PG_CUR
stx PG_NXT
rts
UpdateScore:
inc SCORE_L
bne +
inc SCORE_H
+
; [FIX 4] Fixed high-score tracking. Tying previous high-score does NOT break it.
lda SCORE_H
cmp HISCORE_H
bcc .checkProx ; Less than high score
bne .isNewHi ; Greater than high score (New Record!)
lda SCORE_L
cmp HISCORE_L
bcc .checkProx ; Less than high score
beq .checkProx ; Exact Tie. Do not trigger New Record yet.
.isNewHi:
lda SCORE_L
sta HISCORE_L
lda SCORE_H
sta HISCORE_H
inc BORDER ; Strobe the border on record break!
jmp .doneScore
.checkProx:
; Proximity Hype: if High Bytes match, we are within 255 ticks
lda HISCORE_H
cmp SCORE_H
bne .cold
lda #$07 ; Yellow border warning
sta BORDER
jmp .doneScore
.cold:
lda #$00 ; Black border
sta BORDER
.doneScore:
jsr DrawScores
rts
DrawScores:
; Current Score (Top Left - Row 0)
lda SCORE_H
jsr PrintByte
stx VIDRAM+0
sty VIDRAM+1
lda SCORE_L
jsr PrintByte
stx VIDRAM+2
sty VIDRAM+3
; High Score (Top Right - Row 0)
lda HISCORE_H
jsr PrintByte
stx VIDRAM+35
sty VIDRAM+36
lda HISCORE_L
jsr PrintByte
stx VIDRAM+37
sty VIDRAM+38
rts
PrintByte:
; Convert A to Hex Screen Codes (High nibble X, Low nibble Y)
pha
lsr
lsr
lsr
lsr
tax
lda HexChars,x
tax
pla
and #$0F
tay
lda HexChars,y
tay
rts
HexChars: !byte 48,49,50,51,52,53,54,55,56,57,1,2,3,4,5,6
CalcNext:
; Reset loop detection flags for this frame
lda #0
sta FLAG_CHANGE
sta FLAG_PRV_CH
; Setup 3-row sliding window pointers using dynamic Page Pointers
lda #0
sta PTR_TOP
lda PG_CUR
sta PTR_TOP+1
lda #40
sta PTR_MID
lda PG_CUR
sta PTR_MID+1
lda #80
sta PTR_BOT
lda PG_CUR
sta PTR_BOT+1
lda #40
sta PTR_NXT
lda PG_NXT
sta PTR_NXT+1
lda #40
sta PTR_PRV
lda PG_PRV
sta PTR_PRV+1
lda #23 ; Process 23 inner rows
sta ROW_CNT
.rowLoop:
ldy #1 ; Start at inner column 1 (skip left edge)
.colLoop:
; Neighbor math (Top Row)
dey ; Col - 1
lda (PTR_TOP),Y
clc
adc (PTR_BOT),Y
iny ; Col 0
clc
adc (PTR_TOP),Y
adc (PTR_BOT),Y
iny ; Col + 1
clc
adc (PTR_TOP),Y
adc (PTR_BOT),Y
dey ; Back to Col 0
; Neighbor math (Mid Row)
dey ; Col - 1
clc
adc (PTR_MID),Y
iny
iny ; Col + 1
clc
adc (PTR_MID),Y
dey ; Back to Col 0
; [FIX 1] Replaced illegal `LDX (PTR_MID),Y` with valid safe storage
sta NEIGH_CNT ; Save summed neighbor count (0-8)
lda (PTR_MID),y ; Load center cell state
tax ; Transfer cell state to X
lda NEIGH_CNT ; Restore neighbor count to Accumulator
; State Rules execution
cpx #0
beq .deadCell
.aliveCell:
cmp #2
beq .survive
cmp #3
beq .survive
jmp .die
.deadCell:
cmp #3
beq .survive
jmp .die
.survive:
lda #1
bne .store
.die:
lda #0
.store:
sta (PTR_NXT),Y
; Fixed Point Check
cmp (PTR_MID),Y
beq .chkPrv
pha
lda #1
sta FLAG_CHANGE
pla
.chkPrv:
; Period-2 Oscillator Check
cmp (PTR_PRV),Y
beq .nextCell
pha
lda #1
sta FLAG_PRV_CH
pla
.nextCell:
iny
cpy #39 ; Stop at inner column 38 (skip right edge)
bne .colLoop
jsr AdvPtrs
dec ROW_CNT
bne .rowLoop
rts
AdvPtrs:
; Steps all sliding windows down one 40-byte row
lda PTR_TOP
clc
adc #40
sta PTR_TOP
bcc +
inc PTR_TOP+1
+
lda PTR_MID
clc
adc #40
sta PTR_MID
bcc +
inc PTR_MID+1
+
lda PTR_BOT
clc
adc #40
sta PTR_BOT
bcc +
inc PTR_BOT+1
+
lda PTR_NXT
clc
adc #40
sta PTR_NXT
bcc +
inc PTR_NXT+1
+
lda PTR_PRV
clc
adc #40
sta PTR_PRV
bcc +
inc PTR_PRV+1
+
rts