commit 9206086b8a65e0d25da8509e84e23bb3efcb7911 Author: John Lorentzson Date: Wed Mar 26 10:28:23 2025 +0100 Import from tarball created on 2024-03-14 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..02a6980 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +*.o +*.prg +*.lst diff --git a/build.sh b/build.sh new file mode 100755 index 0000000..a3b2050 --- /dev/null +++ b/build.sh @@ -0,0 +1,3 @@ +#!/bin/sh + +cl65 -o pong.prg -l pong.lst -t c64 -C c64-asm.cfg -u__EXEHDR__ main.asm diff --git a/c64.inc b/c64.inc new file mode 100644 index 0000000..b0e6b0e --- /dev/null +++ b/c64.inc @@ -0,0 +1,84 @@ +;;; -*- Mode: asm; indent-tabs-mode: t; tab-width: 8 -*- + + PROGSTART = $080D ; just after BASIC header + + ;; Misc + CINV = $0314 ; Hardware IRQ vector + + ;; CIA registers + PRA = $DC00 + PRB = $DC01 + DDRA = $DC02 + DDRB = $DC03 + CIA_ICR = $DC0D + + PRA2 = $DD00 + + ;; VIC-II registers + VICBASE = $D000 + SPRITE_0X = VICBASE+0 + SPRITE_0Y = VICBASE+1 + SPRITE_1X = VICBASE+2 + SPRITE_1Y = VICBASE+3 + SPRITE_2X = VICBASE+4 + SPRITE_2Y = VICBASE+5 + SPRITE_3X = VICBASE+6 + SPRITE_3Y = VICBASE+7 + SPRITE_4X = VICBASE+8 + SPRITE_4Y = VICBASE+9 + SPRITE_5X = VICBASE+10 + SPRITE_5Y = VICBASE+11 + SPRITE_6X = VICBASE+12 + SPRITE_6Y = VICBASE+13 + SPRITE_7X = VICBASE+14 + SPRITE_7Y = VICBASE+15 + SPRITE_X_MSB = VICBASE+16 + YSCROLL_MODE = VICBASE+17 + RASTER = VICBASE+18 + ;; ... + SPRITE_ENABLE = VICBASE+21 + XSCROLL_MODE = VICBASE+22 + SPRITE_XPAND_Y = VICBASE+23 + ;; ... + MEMPTR = VICBASE+24 + VICINT = VICBASE+25 + VICINTMASK = VICBASE+26 + SPRITE_BG_PRIO = VICBASE+27 + ;; ... + SPRITE_XPAND_X = VICBASE+29 + SSCOL = VICBASE+30 + ;; ... + BORDER = VICBASE+32 + BGCOL0 = VICBASE+33 + BGCOL1 = VICBASE+34 + BGCOL2 = VICBASE+35 + BGCOL3 = VICBASE+36 + ;; ... + SPRITE_0C = VICBASE+39 + SPRITE_1C = VICBASE+40 + SPRITE_2C = VICBASE+41 + SPRITE_3C = VICBASE+42 + SPRITE_4C = VICBASE+43 + SPRITE_5C = VICBASE+44 + SPRITE_6C = VICBASE+45 + SPRITE_7C = VICBASE+46 + + COLORMEM = $D800 + + ;; SID registers + SIDBASE = $D400 + SIDF1H = SIDBASE+1 + SIDF1L = SIDBASE+2 + SIDCR1 = SIDBASE+4 + SIDAD1 = SIDBASE+5 + SIDSR1 = SIDBASE+6 + + SIDFMV = SIDBASE+24 + + ;; Macros + + .macro padto addr + .repeat (addr - *) + .byte 0 + .endrep + .endmacro diff --git a/enum.asm b/enum.asm new file mode 100644 index 0000000..97c2a87 --- /dev/null +++ b/enum.asm @@ -0,0 +1,8 @@ +;;; -*- Mode: asm; indent-tabs-mode: t; tab-width: 8 -*- + + .enum mode + StartScreen = 0 + MainGame + ScoreHighlight + Win + .endenum diff --git a/gameloop.asm b/gameloop.asm new file mode 100644 index 0000000..bc63503 --- /dev/null +++ b/gameloop.asm @@ -0,0 +1,183 @@ +;;; -*- Mode: asm; indent-tabs-mode: t; tab-width: 8 -*- + +;;; Main gameloop routine. +game: + lda p1score + ldx #4 + jsr printbyte + + lda p2score + ldx #(40 - 6) + jsr printbyte + + lda col + beq @nocol + jsr handle_collision +@nocol: + ;; Player input: + ;; F1 (restart) + ldx #%11111110 + ldy #%00010000 + jsr keydown + bne @norestart + jmp PROGSTART +@norestart: + + ;; Q (player 1 up) + ldx #%01111111 + ldy #%01000000 + jsr keydown + bne @skip1 + dec p1y + dec p1y + + ;; A (player 1 down) +@skip1: + ldx #%11111101 + ldy #%00000100 + jsr keydown + bne @skip2 + inc p1y + inc p1y +@skip2: + + ;; O (player 2 up) + ldx #%11101111 + ldy #%01000000 + jsr keydown + bne @skip3 + dec p2y + dec p2y +@skip3: + + ;; L (player 2 down) + ldx #%11011111 + ldy #%00000100 + jsr keydown + bne @skip4 + inc p2y + inc p2y +@skip4: + + ;; Clamp paddles to stay on-screen. + lda #$32 ;player 1 top + cmp p1y + bcc @dontclamp1 + sta p1y +@dontclamp1: + lda #($fa - 42) ;player 1 bottom + cmp p1y + bcs @dontclamp2 + sta p1y +@dontclamp2: + lda #$32 ;player 2 top + cmp p2y + bcc @dontclamp3 + sta p2y +@dontclamp3: + lda #($fa - 42) ;player 2 bottom + cmp p2y + bcs @dontclamp4 + sta p2y +@dontclamp4: + +update_paddles: + lda p1y + sta SPRITE_0Y + lda p2y + sta SPRITE_1Y + +ball_physics: + lda #BALLVERT + and balldata + beq @down ; 0 = down, 1 = up + txa + clc + dec ball_y + lda #BALLVSPEED + bit balldata + beq @slowballup + dec ball_y ;else we have a fast down ball +@slowballup: + ;; Top bounce check + lda ball_y + cmp #$32 + beq @topbouncestill + bcs @horizontal +@topbouncestill: + ;; If it's <= 32 then we bounce off the top + jsr ball_bounce_vert + jmp @horizontal + +@down: + inc ball_y + lda #BALLVSPEED + bit balldata + beq @slowballdown + inc ball_y +@slowballdown: + ;; Bottom bounce check + lda ball_y + cmp #($fa - 8) ;$fa is bottom of the screen, - 8 for ball height + bcc @horizontal + jsr ball_bounce_vert + ;; fallthrough to horizontal + +@horizontal: + lda #BALLHORI + and balldata + beq @left ; 0 = left, 1 = right + ;; Move ball right + jsr ball_x_inc ;first increment + lda #BALLHSPEED + bit balldata + beq @done + ;; if bit BALLHSPEED is set, we have a horizontally fast ball + jsr ball_x_inc ;so we increment again + jmp @done +@left: + ;; Move ball left + jsr ball_x_dec ;first decrement + lda #BALLHSPEED + bit balldata + beq @done + ;; if bit BALLHSPEED is set, we have a horizontally fast ball + jsr ball_x_dec ;so we dec again +@done: + +check_ball_goal: + ;; Check to see if it's time to change score and reset the ball + lda #%00000100 + bit SPRITE_X_MSB ;check if it's on the left or right side + bne @right + + lda ball_x + cmp #$10 + bcs @nope + inc p2score + lda p2score + jsr clampscore + sta p2score + jsr reset_ball + jsr highlightscore + jmp @nope +@right: + lda ball_x + cmp #$50 + bcc @nope + inc p1score + lda p1score + jsr clampscore + sta p1score + jsr reset_ball + jsr highlightscore +@nope: + +ball_sprite: + lda ball_x + sta SPRITE_2X + lda ball_y + sta SPRITE_2Y + + ;; MAIN GAME ROUTINE ENDS HERE, PUT SUBROUTINES AFTER + rts diff --git a/graphics.asm b/graphics.asm new file mode 100644 index 0000000..236a260 --- /dev/null +++ b/graphics.asm @@ -0,0 +1,73 @@ +;;; -*- Mode: asm; indent-tabs-mode: t; tab-width: 8 -*- + + SPRITEBASE = $3400 + +sprite_data: + .repeat 21 + .byte $ff, 0, 0 ;defines 8x21 paddle sprite + .endrep + .byte 0 + + .repeat 8 + .byte $ff, 0, 0 + .endrep + .repeat 13 + .byte 0, 0, 0 + .endrep + .byte 0 + +sprite_data_end: + +scrlinelo: + .byte $00 + .byte $28 + .byte $50 + .byte $78 + .byte $A0 + .byte $C8 + .byte $F0 + .byte $18 + .byte $40 + .byte $68 + .byte $90 + .byte $B8 + .byte $E0 + .byte $08 + .byte $30 + .byte $58 + .byte $80 + .byte $A8 + .byte $D0 + .byte $F8 + .byte $20 + .byte $48 + .byte $70 + .byte $98 + .byte $C0 + +scrlinehi: + .byte $04 + .byte $04 + .byte $04 + .byte $04 + .byte $04 + .byte $04 + .byte $04 + .byte $05 + .byte $05 + .byte $05 + .byte $05 + .byte $05 + .byte $05 + .byte $06 + .byte $06 + .byte $06 + .byte $06 + .byte $06 + .byte $06 + .byte $06 + .byte $07 + .byte $07 + .byte $07 + .byte $07 + .byte $07 diff --git a/init.asm b/init.asm new file mode 100644 index 0000000..0c2fcab --- /dev/null +++ b/init.asm @@ -0,0 +1,215 @@ +;;; -*- Mode: asm; indent-tabs-mode: t; tab-width: 8 -*- + +init: + ;; Set our custom NMI vector so that pressing restore quits + lda #nmi + sta $0319 + + ;; Reset gamemode + lda #mode::StartScreen + sta gamemode + + ;; Reset variables + lda #0 + sta p1score + sta p2score + sta p1y + sta p2y + sta exit + lda #64 + sta ball_x + sta ball_y + + + sei ;turn off IRQ while setting up + + lda #$7f + sta CIA_ICR ;disable CIA IRQ + bit CIA_ICR ;acknowledge it + + ;; Set up our graphics + ldx #$00 + stx BGCOL0 ;black background, appropriate for pong + stx BORDER + + jsr loadtitlescreen + + lda #%00000111 + sta SPRITE_ENABLE ;enable sprite 0,1,2 + sta SSCOL + sta SPRITE_BG_PRIO + lda #%00000011 + sta SPRITE_XPAND_Y + lda #$01 + sta SPRITE_0C + sta SPRITE_1C + sta SPRITE_2C + + lda #%00000010 + sta SPRITE_X_MSB + lda #$18 + sta SPRITE_0X + lda #$50 + sta SPRITE_1X + + lda #$32 + sta SPRITE_0Y + sta SPRITE_1Y + + lda #$64 + sta SPRITE_2X + sta SPRITE_2Y + + ;; Load sprites + ;; Note: breaks if we have more than 255 bytes of sprite gfx + ldx #(sprite_data_end - sprite_data) ;DANGER + ldy #$00 +@loadloop: + lda sprite_data,y + sta SPRITEBASE,y + iny + dex + bne @loadloop + ;; done when X is zero + + ;; lda #scorestr + ;; sta z:zscratch1 + ;; ldx #4 + ;; jsr printstr + + ;; Set up our own IRQ routine + lda #irq + sta CINV+1 + + ldx #$0 + stx RASTER ;trigger IRQ on scanline 0 (vblank) + ldx #%00000101 ;raster & sprite-sprite col IRQs + stx VICINTMASK ;enable VIC IRQ + + cli ;turn IRQ back on, we're ready +idle: + jmp idle ;do nothing forever, game is driven by IRQ + +nmi: + brk + +clearscreen: + ;; Let's clear the background + lda #VMBASE + sta z:zscratch1 + lda #COLORMEM + sta z:zscratch3 + ldx #$04 +@loopouter: + ldy #$00 +@loopinner: + lda #$20 + sta (zscratch0),y + lda #$01 ;write the color white + sta (zscratch2),y ;to every part of color RAM + iny + bne @loopinner + inc z:zscratch1 ;increment page + inc z:zscratch3 + dex + bne @loopouter + + ;; Set up sprites + ;; sprite ptr $d0 = memory location $3400 + ldx #$d0 + stx VMBASE+($400 - 8) + stx VMBASE+($400 - 7) + inx + stx VMBASE+($400 - 6) + + rts + +loadtitlescreen: + jsr clearscreen + + lda #title + sta z:zscratch1 + + ldx #$0a + ldy #$0b + jsr printstr + + lda #<credits + sta z:zscratch0 + lda #>credits + sta z:zscratch1 + + ldx #$0c + ldy #$0e + jsr printstr + + lda #<tutorial2 + sta z:zscratch0 + lda #>tutorial2 + sta z:zscratch1 + + ldx #$09 + ldy #$15 + jsr printstr + + lda #<tutorial1 + sta z:zscratch0 + lda #>tutorial1 + sta z:zscratch1 + + ldx #$0a + ldy #$17 + jsr printstr + + rts + +title: + .byte $10, $0F, $0E, $07, $2C, $20, $0D, $0F, $12, $05, $20, $0F, $12, $20, $0C, $05, $13, $13, 0 + +credits: + .byte $0D, $01, $04, $05, $20, $02, $19, $20, $04, $15, $15, $11, $0E, $04, 0 + +tutorial1: + .byte $03, $0F, $0E, $14, $12, $0F, $0C, $20, $17, $09, $14, $08, $20, $11, $01, $20, $0F, $0C, 0 + +tutorial2: + .byte $10, $12, $05, $13, $13, $20, $13, $10, $01, $03, $05, $20, $14, $0F, $20, $13, $14, $01, $12, $14, 0 + +loadgamefield: + jsr clearscreen + + ;; now we'll draw the center column + ldx #$25 +@lineloop: + lda scrlinelo,x + sta z:zscratch0 + lda scrlinehi,x + sta z:zscratch1 + txa + and #%00000001 + bne @skipdot + lda #103 + ldy #20 + sta (zscratch0),y + jmp @next +@skipdot: + lda #116 + ldy #19 + sta (zscratch0),y +@next: + dex + bpl @lineloop + + rts diff --git a/main.asm b/main.asm new file mode 100644 index 0000000..80000ec --- /dev/null +++ b/main.asm @@ -0,0 +1,246 @@ +;;; -*- Mode: asm; indent-tabs-mode: t; tab-width: 8 -*- + .include "c64.inc" + .include "enum.asm" + + VMBASE = $0400 + + .org PROGSTART + + .include "init.asm" ;must come first + .include "utilities.asm" + + ;; Start screen, waits for space key to be pressed. +titlescreen: + ;; More visually interesting sprite positioning for + ;; the title screen. Overrides default from init. + lda #$82 + sta SPRITE_0Y + + ldx #%01111111 + ldy #%00010000 + jsr keydown + bne @dontstart + ;; Start the game here. + jsr loadgamefield ;set the background + lda #mode::MainGame ;don't forget that # + sta gamemode ;set the game mode to MainGame +@dontstart: + rts + + + ;; Wait and highlight the score change for half a second. + ;; Happens every time a player scores or whatever it's called in tennis +scorewait: + ;; While waiting we play a buzz + lda #$0f + sta SIDFMV + lda #$08 + sta SIDAD1 + lda #$68 + sta SIDSR1 + + lda #$45 + sta SIDF1L + lda #$1d + sta SIDF1H + + lda #$11 + sta SIDCR1 + + dec timer + bne @dontresume + ;; Reset gamemode + lda #mode::MainGame + sta gamemode + ;; Reset background color + lda #0 + sta BGCOL0 + ;; Stop sound + lda #$10 + sta SIDCR1 +@dontresume: + rts + + + ;; Call after a player scores to highlight the win +highlightscore: + lda #mode::ScoreHighlight + sta gamemode + lda #25 + sta timer + lda #2 + sta BGCOL0 + rts + + .include "gameloop.asm" + + + ;; "Victory" mode when 100 points has been reached. +win: + dec timer + bne @skip + dec timer2 + bne @continue + ;; After five seconds (10 half-seconds) we reset the game + jmp PROGSTART +@continue: + lda #25 + sta timer + lda BGCOL0 + eor #8 + sta BGCOL0 +@skip: + rts + +winstop: + lda #25 + sta timer + lda #10 + sta timer2 + lda #$0d + sta BGCOL0 + lda #mode::Win ;DO NOT FORGET THE # + sta gamemode + rts + +ball_x_inc: + inc ball_x + bne @done + ;; If ball_x incremented to zero we have either looped around + ;; the screen (shouldn't happen) or crossed the bit 8 boundry. + ;; So we set the MSB to one, since we're going right. + lda #%00000100 + ora SPRITE_X_MSB + sta SPRITE_X_MSB +@done: + rts + +ball_x_dec: + dec ball_x + lda ball_x + cmp #$ff + bne @done + ;; If ball_x incremented to zero we have either looped around + ;; the screen (shouldn't happen) or crossed the bit 8 boundry. + ;; So we set the MSB to one, since we're going right. + lda #%11111011 + and SPRITE_X_MSB + sta SPRITE_X_MSB +@done: + rts + +ball_bounce_vert: + jsr color_change + lda #%10000000 + eor balldata + sta balldata + rts + +ball_bounce_hori: + jsr color_change + lda #%01000000 + eor balldata + sta balldata + rts + +reset_ball: + jsr ball_bounce_hori + lda #$90 + sta ball_y + lda #$c0 + sta ball_x + lda #%11111011 + and SPRITE_X_MSB + sta SPRITE_X_MSB + rts + +color_change: + inc bordercol + lda bordercol + and #$0f + cmp #2 + bcs @fine + lda #2 + sta bordercol +@fine: + lda bordercol + sta BORDER + rts + +handle_collision: + jsr ball_bounce_hori ;obvious, flip the ball's horizontal direction + lda #$00 + sta col ;clear the collision flag + rts + + ;; Cheap trick to make a hexprinted number look decimal. + ;; Score comes in the A register, clamped score returned in A +clampscore: + tax + and #%00001111 + cmp #$0a + bcc @noclamplower + ;; yes clamp, inc upper nybble, ex. $19 -> $20 + txa + and #%11110000 + clc + adc #$10 + tax +@noclamplower: + txa + and #%11110000 + cmp #$a0 + bcc @noclamp + ;; over 99? just stop at that point, we'll flash some colors then reset + jsr winstop + pla + pla +@noclamp: + txa + rts + +modedispatch: + jsr jumpwithtable + .addr titlescreen + .addr game ;normal gameplay + .addr scorewait + .addr win + + + ;; Main IRQ subroutine. CPU gets sent here by the interrupts from the + ;; VIC-II's raster and collision interrupts, and is responsible for + ;; calling everything else. Since the game is interrupt-driven, you + ;; could add a RTS to the init code and have the game run together + ;; with BASIC. Would be pretty stupid but you *can* do it. That does + ;; give me an idea though... +irq: + lda SSCOL ;need to do this for collision checking to work + + lda #%00000100 ;check if irq is for collision or not + and VICINT + beq @nocol + ;; Collision + sta col ;set the collision flag + lda #$7f + and VICINT + sta VICINT + jmp @skipgame +@nocol: + lda gamemode + jsr modedispatch +@skipgame: + lda #$7f + and VICINT + sta VICINT + + jmp $ea31 + + .include "variables.asm" + + BALLVERT = %10000000 + BALLHORI = %01000000 + BALLVSPEED = %00000010 + BALLHSPEED = %00000001 + + .include "graphics.asm" + .include "zeropage.asm" diff --git a/utilities.asm b/utilities.asm new file mode 100644 index 0000000..2057309 --- /dev/null +++ b/utilities.asm @@ -0,0 +1,88 @@ +;;; -*- Mode: asm; indent-tabs-mode: t; tab-width: 8 -*- + + ;; Jumps to a routine via a table. Index goes in A register. +jumpwithtable: + tay + + ;; Set up pointer to jump table + pla + sta z:zscratch0 + pla + sta z:zscratch1 + + tya + asl + tay + iny + + lda (zscratch0),y + sta z:zscratch2 + iny + lda (zscratch0),y + sta z:zscratch3 + + jmp (zscratch2) + +printbyte: + inx + jsr printnyb + .repeat 4 + lsr + .endrep + dex + jsr printnyb + rts + +printnyb: + pha + and #$0f + cmp #$0a + bcc @number ;if nybble<$A then print a number, else letter + sec + sbc #$09 ;letters start at $01, so sub away 9 + jmp @done +@number: + clc + adc #$30 ;print a number, starting at $30 +@done: + sta VMBASE,x + pla + rts + + ;; Put string pointer into zscratch1+2 + ;; X position into X, y pos into Y +printstr: + lda scrlinelo,y + sta z:zscratch2 + lda scrlinehi,y + sta z:zscratch3 + + txa + clc + adc z:zscratch2 + sta z:zscratch2 + lda z:zscratch3 + adc #0 + sta z:zscratch3 + + ldy #$00 +@loop: + lda (zscratch0),y + beq @done + sta (zscratch2),y + iny + inx + jmp @loop +@done: + rts + +keydown: + lda #$ff + sta DDRA + lda #$00 + sta DDRB + stx PRA + tya + and PRB + rts + diff --git a/variables.asm b/variables.asm new file mode 100644 index 0000000..bca1b8c --- /dev/null +++ b/variables.asm @@ -0,0 +1,14 @@ +;;; -*- Mode: asm; indent-tabs-mode: t; tab-width: 8 -*- +gamemode: .res 1, mode::StartScreen +p1y: .res 1, 0 +p1score: .res 1, $90 +p2score: .res 1, 0 +p2y: .res 1, 0 +col: .res 1, 0 +balldata: .res 1, %01000011 ;ball starts going right, fast vert & hori +ball_x: .res 1, 64 +ball_y: .res 1, 64 +bordercol: .res 1, 0 +timer: .res 1, 0 +timer2: .res 1, 0 +exit: .res 1, 0 diff --git a/zeropage.asm b/zeropage.asm new file mode 100644 index 0000000..fc3a786 --- /dev/null +++ b/zeropage.asm @@ -0,0 +1,7 @@ +;;; -*- Mode: asm; indent-tabs-mode: t; tab-width: 8 -*- + .zeropage + .org $50 +zscratch0: .res 1 +zscratch1: .res 1 +zscratch2: .res 1 +zscratch3: .res 1