!cpu 6502
!to "gol_battler.prg", cbm

; =======================================================================
; CONWAY'S GAME OF LIFE - AUTO-BATTLER SCREENSAVER
; Period 1-8 Exact Loop Detection Upgrade
; 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

; --- Memory Map Constants ---
; Reverted to $2000s to respect default BASIC MMU memory map.
BASE_CUR    = $2000
BASE_NXT    = $2400
BASE_PRV    = $2800

HIST_BASE   = $2C00     ; 8 * 125 bytes = $2C00 - $2FE7
HASH_BASE   = $2FE8     ; 8 * 4 bytes   = $2FE8 - $3007
TEMP_PACK   = $3008     ; 125 bytes     = $3008 - $3084

; --- Zero Page Variables ---
PTR_TOP     = $50
PTR_MID     = $52
PTR_BOT     = $54
PTR_NXT     = $56
DRAW_PTR    = $5A
SCR_PTR     = $5C
NEIGH_CNT   = $5E

PG_CUR      = $5F       ; Rotating high-byte for Current Board
PG_NXT      = $60       ; Rotating high-byte for Next Board
PG_PRV      = $61       ; Rotating high-byte for Previous Board

SCORE_L     = $62
SCORE_H     = $63
HISCORE_L   = $64
HISCORE_H   = $65
ROW_CNT     = $68

; --- Loop Detection Variables ---
HIST_HEAD   = $69       ; (0-7) Next slot to write
HIST_COUNT  = $6A       ; (0-8) Number of valid history generations
LOOP_PERIOD = $6B       ; (0-8) Detected loop period

CUR_HASH    = $6C       ; $6C-$6F (4 bytes)
PACK_PTR    = $70       ; $70-$71
HIST_PTR    = $72       ; $72-$73
PACK_BYTE   = $76
BIT_COUNT   = $77
HIST_DIST   = $78
HIST_SLOT   = $79

; =======================================================================
; C128 BASIC Header (10 SYS 7181)
; =======================================================================
* = $1C01
!word BasicEnd          
!word 10                
!byte $9E               
!byte $37,$31,$38,$31   
!byte 0                 
BasicEnd:
!word 0                 

; Engine entry point starts at exactly $1C0D
* = $1C0D

Start:
    sei             ; Disable interrupts to protect $50-$79 ZP Workspace

    lda #$FF
    sta SID_FREQ3_L
    sta SID_FREQ3_H
    lda #$80        ; Noise Waveform
    sta SID_CTRL3

    lda #0
    sta HISCORE_L
    sta HISCORE_H
    sta BORDER
    sta BGCOLOR

    jsr ClearScreen
    jsr InitColors

NewRun:
    lda #0
    sta SCORE_L
    sta SCORE_H
    sta BORDER

    ; Clear HUD loop indicator
    lda #$20
    sta VIDRAM+18
    sta VIDRAM+19

    ; Setup initial 3 rotating pages
    lda #>BASE_CUR
    sta PG_CUR
    lda #>BASE_NXT
    sta PG_NXT
    lda #>BASE_PRV
    sta PG_PRV

    ; Debug Marker 2: Initialization started
    lda #2
    sta BORDER

    jsr ClearAllBoards

    ; Debug Marker 3: Memory cleared
    lda #3
    sta BORDER

    jsr InitHistory
    jsr SeedBoard

    ; Debug Marker 4: Board seeded
    lda #4
    sta BORDER

    ; Seed random board & treat it as Generation 0
    lda PG_CUR
    jsr PackBoard       ; Pack the initial seed
    
    ; Debug Marker 5: PackBoard survived!
    lda #5
    sta BORDER

    jsr CommitHistory   ; Save to history slot 0

    ; Debug Marker 6: Entering Main Loop
    lda #6
    sta BORDER
    
MainLoop:
    jsr DrawBoard
    jsr UpdateScore
    jsr CalcNext

    ; Pack the completed candidate generation
    lda PG_NXT
    jsr PackBoard

    ; Search for 1-8 exact loop
    jsr DetectHistoryLoop
    bcs .loopDetected

    ; No loop found. Commit this new generation to history
    jsr CommitHistory
    jsr SwapBoards
    jmp MainLoop

.loopDetected:
    ; Draw exact scores, indicate the loop period, draw the frozen state
    jsr DrawScores
    jsr DrawLoopHUD
    jsr SwapBoards      ; Move the duplicate state into PG_CUR
    jsr DrawBoard       ; Draw the board that caused the match

    ldx #0
    ldy #0
.delay:
    iny
    bne .delay
    inx
    bne .delay
    jmp NewRun

; =======================================================================
; History & Loop Detection Routines
; =======================================================================

InitHistory:
    lda #0
    sta HIST_HEAD
    sta HIST_COUNT
    sta LOOP_PERIOD
    rts

PackBoard:
    ; Packs exactly 1000 cells starting at Page (A) into 125 bytes at TEMP_PACK
    ; Also computes the 32-bit CUR_HASH.
    sta PACK_PTR+1
    lda #0
    sta PACK_PTR

    ; Seed the hash deterministically
    lda #$AA
    sta CUR_HASH
    lda #$55
    sta CUR_HASH+1
    lda #$CC
    sta CUR_HASH+2
    lda #$33
    sta CUR_HASH+3

    ldx #0              ; TEMP_PACK output index (0-124)
    stx BIT_COUNT       ; 0-7 bit accumulator
    stx PACK_BYTE
    ldy #0              ; Cell pointer (0-255)

.cellLoop:
    lda (PACK_PTR),y
    beq .zeroBit
    sec                 ; Inject 1
    bcs .shift
.zeroBit:
    clc                 ; Inject 0
.shift:
    rol PACK_BYTE
    inc BIT_COUNT
    lda BIT_COUNT
    cmp #8
    bne .nextCell

    ; Byte full -> Store it
    lda PACK_BYTE
    sta TEMP_PACK,x

    ; Fast Rolling Hash Mix
    clc
    adc CUR_HASH
    sta CUR_HASH
    rol CUR_HASH+1
    rol CUR_HASH+2
    lda CUR_HASH+3
    eor PACK_BYTE
    sta CUR_HASH+3

    ; CHECK TERMINATION CONDITION:
    ; Have we stored exactly 125 bytes? (representing exactly 1000 cells)
    inx
    cpx #125
    beq .packDone

    ; Not done yet. Reset byte packer
    lda #0
    sta BIT_COUNT
    sta PACK_BYTE

.nextCell:
    iny
    bne .cellLoop       ; If Y hasn't wrapped, continue on this page

    ; Y wrapped to 0. Advance high byte of source page.
    inc PACK_PTR+1
    jmp .cellLoop

.packDone:
    rts

DetectHistoryLoop:
    ; Checks up to HIST_COUNT previous generations from newest to oldest
    ; Returns Carry SET if loop found, CLEAR if no loop.
    lda HIST_COUNT
    bne +
    clc
    rts
+
    lda #1              ; Start checking Distance = 1
    sta HIST_DIST

.loop:
    ; Slot = (HIST_HEAD - HIST_DIST) AND 7
    lda HIST_HEAD
    sec
    sbc HIST_DIST
    and #7
    sta HIST_SLOT
    tax

    ; Compare Hash first (Slot * 4 offset)
    txa
    asl
    asl
    tay

    lda CUR_HASH
    cmp HASH_BASE,y
    bne .nextDist
    lda CUR_HASH+1
    cmp HASH_BASE+1,y
    bne .nextDist
    lda CUR_HASH+2
    cmp HASH_BASE+2,y
    bne .nextDist
    lda CUR_HASH+3
    cmp HASH_BASE+3,y
    bne .nextDist

    ; Hash Match! Perform Exact 125-byte Byte-for-Byte comparison
    lda HistSlotLo,x
    sta HIST_PTR
    lda HistSlotHi,x
    sta HIST_PTR+1

    ldy #0
.cmpLp:
    lda TEMP_PACK,y
    cmp (HIST_PTR),y
    bne .nextDist       ; Hash collision, keep searching
    iny
    cpy #125
    bne .cmpLp

    ; Exact match validated!
    lda HIST_DIST
    sta LOOP_PERIOD
    sec
    rts

.nextDist:
    inc HIST_DIST
    lda HIST_DIST
    cmp HIST_COUNT
    bcc .loop           ; if DIST < COUNT, loop
    beq .loop           ; if DIST == COUNT, loop

    clc                 ; No matches found
    rts

CommitHistory:
    ; Ensure HIST_COUNT maxes at 8
    lda HIST_COUNT
    cmp #8
    beq +
    inc HIST_COUNT
+
    ; Copy TEMP_PACK to history slot
    ldx HIST_HEAD
    lda HistSlotLo,x
    sta HIST_PTR
    lda HistSlotHi,x
    sta HIST_PTR+1

    ldy #0
.cpyLp:
    lda TEMP_PACK,y
    sta (HIST_PTR),y
    iny
    cpy #125
    bne .cpyLp

    ; Copy 32-bit Hash to HASH_BASE
    txa
    asl
    asl
    tay
    lda CUR_HASH
    sta HASH_BASE,y
    lda CUR_HASH+1
    sta HASH_BASE+1,y
    lda CUR_HASH+2
    sta HASH_BASE+2,y
    lda CUR_HASH+3
    sta HASH_BASE+3,y

    ; Advance Ring Buffer Pointer
    inx
    txa
    and #7
    sta HIST_HEAD
    rts

; --- Lookups for 125-byte boundaries ---
HistSlotLo:
    !byte <(HIST_BASE + 0*125), <(HIST_BASE + 1*125), <(HIST_BASE + 2*125), <(HIST_BASE + 3*125)
    !byte <(HIST_BASE + 4*125), <(HIST_BASE + 5*125), <(HIST_BASE + 6*125), <(HIST_BASE + 7*125)

HistSlotHi:
    !byte >(HIST_BASE + 0*125), >(HIST_BASE + 1*125), >(HIST_BASE + 2*125), >(HIST_BASE + 3*125)
    !byte >(HIST_BASE + 4*125), >(HIST_BASE + 5*125), >(HIST_BASE + 6*125), >(HIST_BASE + 7*125)

; =======================================================================
; Game Engine Subroutines
; =======================================================================

ClearAllBoards:
    ; Clears 17 Pages: $2000 - $30FF (Boards, History, Hash, Temp Pack)
    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
    sta $2C00,y
    sta $2D00,y
    sta $2E00,y
    sta $2F00,y
    sta $3000,y
    iny
    bne .clrLp
    rts

SeedBoard:
    lda #40
    sta DRAW_PTR
    lda PG_CUR          ; Seed directly into CUR
    sta DRAW_PTR+1
    
    ldx #23
.sRow:
    ldy #1
.sCol:
    lda SID_NOISE
    and #$03
    cmp #$01
    bcc .sAlive         ; ~25% density
    lda #0
    beq .sStore
.sAlive:
    lda #1
.sStore:
    sta (DRAW_PTR),y
    iny
    cpy #39
    bne .sCol
    
    lda DRAW_PTR
    clc
    adc #40
    sta DRAW_PTR
    bcc +
    inc DRAW_PTR+1
+   dex
    bne .sRow
    rts

ClearScreen:
    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:
    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:
    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
.rLoop:
    ldy #1
.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
    bne .cLoop

    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:
    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
+
    lda SCORE_H
    cmp HISCORE_H
    bcc .checkProx
    bne .isNewHi
    lda SCORE_L
    cmp HISCORE_L
    bcc .checkProx
    beq .checkProx

.isNewHi:
    lda SCORE_L
    sta HISCORE_L
    lda SCORE_H
    sta HISCORE_H
    inc BORDER
    jmp .doneScore

.checkProx:
    lda HISCORE_H
    cmp SCORE_H
    bne .cold
    lda #$07
    sta BORDER
    jmp .doneScore
.cold:
    lda #$00
    sta BORDER

.doneScore:
    jsr DrawScores
    rts

DrawScores:
    lda SCORE_H
    jsr PrintByte
    stx VIDRAM+0
    sty VIDRAM+1
    lda SCORE_L
    jsr PrintByte
    stx VIDRAM+2
    sty VIDRAM+3

    lda HISCORE_H
    jsr PrintByte
    stx VIDRAM+35
    sty VIDRAM+36
    lda HISCORE_L
    jsr PrintByte
    stx VIDRAM+37
    sty VIDRAM+38
    rts

DrawLoopHUD:
    lda #$10            ; "P" screen code
    sta VIDRAM+18
    lda LOOP_PERIOD
    ora #$30            ; Screen code offset for 1-8 ($31-$38)
    sta VIDRAM+19
    rts

PrintByte:
    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:
    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 #23
    sta ROW_CNT

.rowLoop:
    ldy #1

.colLoop:
    ; Top Row Neighbors
    dey
    lda (PTR_TOP),y
    clc
    adc (PTR_BOT),y
    iny
    clc
    adc (PTR_TOP),y
    adc (PTR_BOT),y
    iny
    clc
    adc (PTR_TOP),y
    adc (PTR_BOT),y
    dey
    
    ; Mid Row Neighbors
    dey
    clc
    adc (PTR_MID),y
    iny
    iny
    clc
    adc (PTR_MID),y
    dey

    sta NEIGH_CNT
    lda (PTR_MID),y
    tax
    lda NEIGH_CNT

    ; Conway Rules Check
    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
    
    iny
    cpy #39
    bne .colLoop

    jsr AdvPtrs
    dec ROW_CNT
    bne .rowLoop
    rts

AdvPtrs:
    ; Only increments the four required 16-bit sliding window pointers
    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
+
    rts

; =======================================================================
; Memory Protection Assertion
; =======================================================================
!if * > $2000 { !error "Fatal: Machine code size exceeds boundary and overlaps with working boards at $2000!" }
