A downloadable project

; ===========================================================================

;  BALL IN CUP

;  An original game for the Atari 2600 Video Computer System (VCS)

; ---------------------------------------------------------------------------

;  Written by:  

;

;  Dedicated, with all my love, to my son:

;

;        R A M D I   F A L L U J A H   K H A N D H A R

;

;  Ramdi -- if you are reading this, then the string finally pulled the ball

;  home. I built this the old way: by hand, register by register, the same

;  way we shaped games on the 2600 back when every byte was a decision and

;  every scanline was a held breath. I hoped that one day you would open this

;  source and feel what I felt at the machine: that a small, honest program

;  can be beautiful -- revelation after revelation -- and that a father can

;  hide a whole heart inside four kilobytes.

;

;  Swing the ball. Find the cup. Begin.

;

;  -- Dad

; ===========================================================================

;

;  TARGET   : Atari 2600 (NTSC), 4K ROM, standard joystick in LEFT port.

;  ASSEMBLE : dasm src/ball_in_cup.asm -f3 -Isrc -obin/ball_in_cup.bin

;

;  HOW TO PLAY

;    * A white ball swings on its tether above a green cup.

;    * Push the joystick LEFT / RIGHT to pump the swing, like the real toy.

;    * Press the BUTTON to let go -- the ball drops with the momentum it has.

;    * Land it in the cup to score. Miss, and it simply swings again.

;      There is no losing here, Ramdi. Only trying. That is how you learn

;      anything worth learning.

; ===========================================================================

    processor 6502

    include "vcs.h"

    include "macro.h"

; ---------------------------------------------------------------------------

;  COLOUR CONSTANTS (NTSC)

; ---------------------------------------------------------------------------

COL_BG      = $00       ; black night sky

COL_BALL    = $0E       ; white ball

COL_SCORE   = $1E       ; bright yellow score

COL_CUP     = $C8       ; warm green cup

COL_FLASH   = $4A       ; orange flash shown briefly on a catch

; ---------------------------------------------------------------------------

;  GAMEPLAY CONSTANTS

; ---------------------------------------------------------------------------

CENTER_X    = 76        ; resting X of the ball (cup mouth centre)

START_Y     = 92        ; resting scanline of the ball while tethered

SKY_TOP     = 32        ; first scanline of the open-sky region

RIM_Y       = 150       ; scanline of the cup rim (the catch line)

SWING_MAX   = 5         ; clamp on swing speed

PUMP        = 1         ; swing energy added per joystick push

GRAV_MASK   = 3         ; apply gravity once every (mask+1) frames

FALL_MAX    = 5         ; terminal fall speed

CATCH_LO    = 64        ; left edge of the forgiving catch window

CATCH_HI    = 88        ; right edge of the forgiving catch window

EDGE_LO     = 16        ; how far left the ball may swing

EDGE_HI     = 136       ; how far right the ball may swing

SCORE_P0_X  = 68        ; screen X of the tens digit

SCORE_P1_X  = 76        ; screen X of the ones digit

; ---------------------------------------------------------------------------

;  RAM (zero page $80..$FF)

; ---------------------------------------------------------------------------

    SEG.U variables

    ORG $80

BallX       ds 1        ; ball X (screen pixel of left edge)

BallY       ds 1        ; ball Y (absolute scanline)

BallVX      ds 1        ; horizontal velocity (signed)

BallVY      ds 1        ; vertical velocity (downward, free flight)

State       ds 1        ; 0 = tethered/swinging, 1 = free flight

ScoreBCD    ds 1        ; score, packed BCD 00..99

BallYSky    ds 1        ; BallY - SKY_TOP, precomputed for the kernel

FrameCtr    ds 1        ; free-running frame counter

FireLatch   ds 1        ; button edge-detect latch

SoundTimer  ds 1        ; frames remaining for the catch beep

FlashTimer  ds 1        ; frames remaining for the catch colour flash

TensPtr     ds 2        ; pointer to tens-digit graphic

OnesPtr     ds 2        ; pointer to ones-digit graphic

; ===========================================================================

;  ROM

; ===========================================================================

    SEG code

    ORG $F000

; ---------------------------------------------------------------------------

;  RESET -- cold start

; ---------------------------------------------------------------------------

Reset

    CLEAN_START                 ; clear RAM + TIA, set the stack (macro.h)

    lda #CENTER_X-26            ; start the ball off-centre so it swings at boot

    sta BallX

    lda #START_Y

    sta BallY

    lda #SWING_MAX

    sta BallVX                  ; an initial push to the right

    lda #0

    sta BallVY

    sta State

    sta ScoreBCD

    sta FireLatch

    sta SoundTimer

    sta FlashTimer

; ---------------------------------------------------------------------------

;  MAIN LOOP -- one pass per frame (NTSC: 3 + 37 + 192 + 30 = 262 lines)

; ---------------------------------------------------------------------------

MainLoop

    ; ----- VERTICAL SYNC (3 lines) -----

    lda #2

    sta VSYNC

    sta WSYNC

    sta WSYNC

    sta WSYNC

    lda #0

    sta VSYNC

    ; ----- VERTICAL BLANK: arm timer, run logic, place objects -----

    lda #2

    sta VBLANK                  ; keep the beam blanked

    lda #44

    sta TIM64T                  ; ~37 lines of vblank

    jsr GameLogic               ; physics, input, scoring (no WSYNCs)

    jsr PositionScore           ; place P0/P1 for the score (uses WSYNCs)

.waitVBlank

    lda INTIM

    bne .waitVBlank

    sta WSYNC

    lda #0

    sta VBLANK                  ; beam on -- picture begins now

    jsr KernelScore             ; lines 0..15   : the score

    jsr KernelTransition        ; lines 16..31  : reposition P0 as the ball

    jsr KernelSky               ; lines 32..151 : sky + swinging ball

    jsr KernelCup               ; lines 152..191: the cup

    ; ----- OVERSCAN (30 lines) -----

    lda #2

    sta VBLANK

    lda #36

    sta TIM64T

.waitOver

    lda INTIM

    bne .waitOver

    sta WSYNC

    jmp MainLoop

; ===========================================================================

;  GAME LOGIC  (runs during vertical blank -- no cycle-critical WSYNCs)

; ===========================================================================

GameLogic

    inc FrameCtr

    ; --- fade out the catch beep / flash when their timers run down ---

    lda SoundTimer

    beq .soundDone

    dec SoundTimer

    bne .soundDone

    lda #0

    sta AUDV0                   ; silence

.soundDone

    lda FlashTimer

    beq .flashDone

    dec FlashTimer

.flashDone

    lda State

    bne FreeFlight

; ----- TETHERED: the ball swings; the player pumps it ----------------------

Tethered

    ; Joystick LEFT (SWCHA bit6 = 0 when pushed)

    lda SWCHA

    and #%01000000

    bne .notLeft

    lda BallVX

    sec

    sbc #PUMP

    sta BallVX

.notLeft

    ; Joystick RIGHT (SWCHA bit7 = 0 when pushed)

    lda SWCHA

    and #%10000000

    bne .notRight

    lda BallVX

    clc

    adc #PUMP

    sta BallVX

.notRight

    ; A gentle restoring pull toward centre, applied every 4th frame, gives

    ; the tether its lazy pendulum sway without ever standing still.

    lda FrameCtr

    and #3

    bne .noPull

    lda BallX

    cmp #CENTER_X

    beq .noPull

    bcs .rightOfCentre

    inc BallVX                  ; left of centre -> pull right

    jmp .noPull

.rightOfCentre

    dec BallVX                  ; right of centre -> pull left

.noPull

    jsr ClampSwing

    jsr MoveX                   ; BallX += BallVX, bounce at the edges

    ; BUTTON = release the ball into free flight.

    lda INPT4

    bmi .buttonUp               ; bit7 = 1 -> not pressed

    lda FireLatch

    bne LogicDone               ; ignore a held button (edge only)

    lda #1

    sta FireLatch

    sta State                   ; -> free flight

    lda #1

    sta BallVY                  ; start gently falling

    jmp LogicDone

.buttonUp

    lda #0

    sta FireLatch

    jmp LogicDone

; ----- FREE FLIGHT: gravity takes over; did we find the cup? ----------------

FreeFlight

    ; gravity every (GRAV_MASK+1) frames

    lda FrameCtr

    and #GRAV_MASK

    bne .noGravity

    lda BallVY

    cmp #FALL_MAX

    bcs .noGravity

    inc BallVY

.noGravity

    lda BallY

    clc

    adc BallVY

    sta BallY

    jsr MoveX                   ; keep drifting horizontally as it falls

    lda BallY

    cmp #RIM_Y

    bcc LogicDone               ; still above the rim -> keep flying

    ; Reached the rim. Are we inside the forgiving catch window?

    lda BallX

    cmp #CATCH_LO

    bcc .missed

    cmp #CATCH_HI+1

    bcs .missed

    ; ----- CAUGHT! -----

    sed

    lda ScoreBCD

    clc

    adc #1

    sta ScoreBCD

    cld

    lda #8                      ; a soft, pure tone

    sta AUDC0

    lda #16

    sta AUDF0

    lda #12

    sta AUDV0

    lda #18

    sta SoundTimer

    lda #20

    sta FlashTimer

.missed

    jsr ResetToy

LogicDone

    ; precompute the ball's row within the sky for the kernel

    lda BallY

    sec

    sbc #SKY_TOP

    sta BallYSky

    rts

; ---- return the ball to its resting, tethered swing ----

ResetToy

    lda #CENTER_X-26

    sta BallX

    lda #START_Y

    sta BallY

    lda #0

    sta BallVY

    sta State

    lda #SWING_MAX

    sta BallVX

    rts

; ---- clamp BallVX to +/- SWING_MAX ----

ClampSwing

    lda BallVX

    bmi .neg

    cmp #SWING_MAX+1

    bcc .ok

    lda #SWING_MAX

    sta BallVX

    rts

.neg

    cmp #256-SWING_MAX

    bcs .ok

    lda #256-SWING_MAX

    sta BallVX

.ok

    rts

; ---- BallX += BallVX, with a soft bounce at the swing limits ----

MoveX

    lda BallX

    clc

    adc BallVX                  ; signed add (two's complement low byte)

    sta BallX

    cmp #EDGE_LO

    bcs .mvLeft

    lda #EDGE_LO

    sta BallX

    jsr FlipVX

    rts

.mvLeft

    cmp #EDGE_HI+1

    bcc .mvRight

    lda #EDGE_HI

    sta BallX

    jsr FlipVX

.mvRight

    rts

; ---- BallVX = -BallVX ----

FlipVX

    lda #0

    sec

    sbc BallVX

    sta BallVX

    rts

; ===========================================================================

;  POSITION THE SCORE PLAYERS (during vblank)

; ===========================================================================

PositionScore

    lda #SCORE_P0_X

    ldx #0

    jsr PosObject               ; player 0 = tens

    lda #SCORE_P1_X

    ldx #1

    jsr PosObject               ; player 1 = ones

    sta WSYNC

    sta HMOVE

    rts

; ---- Classic divide-by-15 horizontal positioning ----

;  A = target X (0..159), X = object (0 = P0, 1 = P1)

PosObject

    sec

    sta WSYNC

.div15

    sbc #15

    bcs .div15

    eor #7

    asl

    asl

    asl

    asl

    sta HMP0,x

    sta RESP0,x

    rts

; ===========================================================================

;  KERNEL: SCORE  (lines 0..15)  -- two BCD digits via players 0 and 1

; ===========================================================================

KernelScore

    ; build pointers to the two digit graphics from the BCD score

    lda ScoreBCD

    and #$F0

    lsr

    lsr

    lsr

    lsr                         ; A = tens (0..9)

    asl

    asl

    asl                         ; tens * 8

    clc

    adc #<DigitGfx

    sta TensPtr

    lda #>DigitGfx

    adc #0

    sta TensPtr+1

    lda ScoreBCD

    and #$0F

    asl

    asl

    asl                         ; ones * 8

    clc

    adc #<DigitGfx

    sta OnesPtr

    lda #>DigitGfx

    adc #0

    sta OnesPtr+1

    lda #COL_SCORE

    sta COLUP0

    sta COLUP1

    ldx #0                      ; line within the 16-line score band

.scoreLoop

    txa

    lsr                         ; X/2 -> font row 0..7 (each row 2 lines tall)

    tay

    lda (TensPtr),y

    sta WSYNC

    sta GRP0

    lda (OnesPtr),y

    sta GRP1

    inx

    cpx #16

    bne .scoreLoop

    lda #0

    sta GRP0

    sta GRP1

    rts

; ===========================================================================

;  KERNEL: TRANSITION (lines 16..31) -- reposition P0 to become the ball

; ===========================================================================

KernelTransition

    lda #0

    sta GRP0

    sta GRP1

    lda BallX

    ldx #0

    jsr PosObject               ; 1 WSYNC, sets P0's coarse + fine position

    sta WSYNC

    sta HMOVE

    ; the ball's colour: normally white, briefly orange right after a catch

    ldx #COL_BALL

    lda FlashTimer

    beq .noFlash

    ldx #COL_FLASH

.noFlash

    stx COLUP0

    ldx #13                     ; pad out the rest of the transition band

.pad

    sta WSYNC

    dex

    bne .pad

    rts

; ===========================================================================

;  KERNEL: SKY (lines 32..151) -- the swinging ball (player 0)

; ===========================================================================

KernelSky

    ldy #0                      ; Y = sky line 0..119

.skyLoop

    tya

    sec

    sbc BallYSky                ; A = line - ball top

    cmp #8

    bcs .blank                  ; outside the 8-pixel ball

    tax

    lda BallGfx,x

    jmp .draw

.blank

    lda #0

.draw

    sta WSYNC

    sta GRP0

    iny

    cpy #120

    bne .skyLoop

    lda #0

    sta GRP0

    rts

; ===========================================================================

;  KERNEL: CUP (lines 152..191) -- a reflected-playfield goblet

; ===========================================================================

KernelCup

    lda #COL_CUP

    sta COLUPF

    lda #%00000001              ; reflect the playfield -> a symmetric cup

    sta CTRLPF

    lda #0

    sta PF0

    sta PF1

    lda #$18                    ; the two upright cup walls

    sta PF2

    ldx #31

.walls

    sta WSYNC

    dex

    bne .walls

    lda #$F8                    ; the cradle / floor of the cup

    sta PF2

    ldx #9

.floor

    sta WSYNC

    dex

    bne .floor

    lda #0                      ; tidy up so nothing bleeds into overscan

    sta PF2

    sta COLUPF

    rts

; ===========================================================================

;  GRAPHICS DATA

; ===========================================================================

; ---- the ball: a tidy 8x8 sphere ----

BallGfx

    .byte %00111100

    .byte %01111110

    .byte %11111111

    .byte %11111111

    .byte %11111111

    .byte %11111111

    .byte %01111110

    .byte %00111100

; ---- digit font 0..9, eight rows each (bit7 = leftmost pixel) ----

DigitGfx

    .byte %00111100             ; 0

    .byte %01100110

    .byte %01101110

    .byte %01110110

    .byte %01100110

    .byte %01100110

    .byte %00111100

    .byte %00000000

    .byte %00011000             ; 1

    .byte %00111000

    .byte %00011000

    .byte %00011000

    .byte %00011000

    .byte %00011000

    .byte %01111110

    .byte %00000000

    .byte %00111100             ; 2

    .byte %01100110

    .byte %00000110

    .byte %00001100

    .byte %00110000

    .byte %01100000

    .byte %01111110

    .byte %00000000

    .byte %00111100             ; 3

    .byte %01100110

    .byte %00000110

    .byte %00011100

    .byte %00000110

    .byte %01100110

    .byte %00111100

    .byte %00000000

    .byte %00001100             ; 4

    .byte %00011100

    .byte %00111100

    .byte %01101100

    .byte %01111110

    .byte %00001100

    .byte %00001100

    .byte %00000000

    .byte %01111110             ; 5

    .byte %01100000

    .byte %01111100

    .byte %00000110

    .byte %00000110

    .byte %01100110

    .byte %00111100

    .byte %00000000

    .byte %00011100             ; 6

    .byte %00110000

    .byte %01100000

    .byte %01111100

    .byte %01100110

    .byte %01100110

    .byte %00111100

    .byte %00000000

    .byte %01111110             ; 7

    .byte %00000110

    .byte %00001100

    .byte %00011000

    .byte %00110000

    .byte %00110000

    .byte %00110000

    .byte %00000000

    .byte %00111100             ; 8

    .byte %01100110

    .byte %01100110

    .byte %00111100

    .byte %01100110

    .byte %01100110

    .byte %00111100

    .byte %00000000

    .byte %00111100             ; 9

    .byte %01100110

    .byte %01100110

    .byte %00111110

    .byte %00000110

    .byte %00001100

    .byte %00111000

    .byte %00000000

; ===========================================================================

;  HIDDEN MESSAGE -- never shown on screen, but written into the cartridge so

;  that one day, reading the raw bytes, you will find it. Hi, Ramdi. (ASCII)

; ===========================================================================

Dedication

    .byte "FOR RAMDI FALLUJAH KHANDHAR. ", 0

    .byte "FROM YOUR FATHER, . ", 0

    .byte "EVERY BYTE HERE WAS PLACED BY HAND, AND EACH ONE MEANS I LOVE YOU. ", 0

    .byte "SWING THE BALL. FIND THE CUP. BEGIN.", 0

; ===========================================================================

;  6502 VECTORS (top of the 4K bank)

; ===========================================================================

    ORG $FFFA

    .word Reset                 ; NMI

    .word Reset                 ; RESET

    .word Reset                 ; IRQ

    END

Published 18 days ago
StatusOn hold
CategoryOther
Authorwozitdev