Skip to content
HN On Hacker News ↗

Pong Wars on the Commodore 64

▲ 14 points 10 comments by Two9A 1mo ago HN discussion ↗

Pangram verdict · v3.3

We believe that this document is fully human-written

0 %

AI likelihood · overall

Human
100% human-written 0% AI-generated
SEGMENTS · HUMAN 7 of 7
SEGMENTS · AI 0 of 7
WORD COUNT 1,847
PEAK AI % 3% · §7
Analyzed
Jul 15
backend: pangram/v3.3
Segments scanned
7 windows
avg 264 words each
Distribution
100 / 0%
human / AI fraction
Verdict
Human
Pangram v3.3

Article text · 1,847 words · 7 segments analyzed

Human AI-generated
§1 Human · 0%

Back to Articles 15th Jul 2026 This post was first promised in early 2024[0]Pongwars on the C64, @Two9A, Feb 22nd 2024, but it got so long, I added a table of contents. That might be a first... Introduction Before we get started Digression: Building a C64 disk image The game loop: Movement calculations Digression the second: Representing negative numbers After movement comes positioning An element of chance The playing field Keeping score Digression the third: Binary long division Rendering the score Future considerations Footnotes and citations One day early in 2024, I was idly flipping through Mastodon when I came across this toot by Koen van Gilst[1]Pongwars recreated in JavaScript, @vnglst, Jan 2024: Figure 1: The toot by @vnglst that sparked the descent into madness currently under discussion This is a depiction of an eternal battle between the "day" ball and the "night" ball, to reverse the colour of the opposite ball's field. Many were transfixed, judging by the thousands of faves; I was one of them, and the thought arose unbidden: If van Gilst saw this online and had to recreate it in JavaScript, and now I've seen it online, I have to recreate it on the Commodore 64, right? Is such a thing even possible? "Yes it is!" Well, it seems like it should be. This is a relatively simple animation, with a random element thrown in at the point the balls contact a surface that makes them bounce; defining "a surface that makes them bounce" seems like the tricky part. So we need to do some planning. And as the first step, there's some defining: We have the ball (two of them) which moves smoothly in straight lines; There's a blockfield: the 20x20 block area over which the balls travel; The blockfield has a boundary outside of which the balls can't travel, and they bounce off the boundary; And there are two sides whose edges also cause the ball to bounce, but the block that gets hit reverses sides.

§2 Human · 0%

This is all complicated by the fact that, despite a lifelong interest in hacking at the C64, I'd never to this point written anything substantial in assembly for the machine. Looks like we'll be learning as we go. On most machines of the era video was output to a CRT, as explained in my megathread on the 1541 disk drive from a couple of years back[2]"Why Was the Commodore 64 Disk Drive So Slow?", Imran Nazar, Dec 2024. After each frame of video output comes a short vertical blanking period, during which the video chip is not making any demands on memory; we'll be looking to try to limit any calculations needed for this animation, so they're completed within the blanking period and we're not unduly affecting other things that may be happening on the machine during the frame rendering period. On the Commodore 64, the BASIC interpreter is the main thing that might be running during that period, and it provides some helpful things for this effect. One of those is a hook that gets triggered at the bottom of every frame[3]"Raster interrupt", C64 Wiki, updated Mar 2024, allowing custom code to be called every frame while keeping BASIC running. So we can plan out something like this for the code: Figure 2: Flow chart for the main loop Before we get started So let's look at the initialisation portion: we'll be keeping an internal representation of the state of the blockfield, but there's also the on-screen blockfield to set up. The C64 has a 40x25 area of characters in its default text mode (each character being an 8x8 pixel square), and there are two areas of memory of interest here: The screen RAM sits at $0400 by default, and is a thousand-byte area where the character values are stored; The color RAM at $D800 is a thousand nybbles[A]A nybble is four bits, as opposed to an 8-bit byte. where each location can represent one of the C64's 16 colours. Immediately after screen RAM sit the pointers to sprite data, but we're not setting up any sprites (yet), so a little shortcut is to write 1024 bytes to screen.

§3 Human · 0%

This writes past the end of the screen, but makes the code much simpler: SCRRAM = $0400 BLOCKPTR = $03 ; A block of two bytes not used by BASIC init: subroutine ; Set up a pointer to screen RAM, starting at the top left ; The 65xx CPUs are little-endian, so low byte first lda #<SCRRAM sta BLOCKPTR lda #>SCRRAM sta BLOCKPTR + 1 ; We'll be writing SPACE (character 32), not char 0 which is @ lda #32 ; Four lots of 256, for 1024 writes ldx #4 ldy #0 .blank: ; The inner loop: runs from Y=0 through 255, 254, ...0 again sta (BLOCKPTR), y dey bne .blank ; The outer loop: punt the pointer along by 256 each go-around inc BLOCKPTR + 1 dex bne .blank (As an aside, BLOCKPTR is set to a value which is "not used by BASIC", but how do we know that? It turns out there's a detailed map of memory used by the C64, by Joe Forster of the STA demo group[4]Commodore 64 memory map, STA, last updated probably a while ago as the page is HTML 4.01, which states locations that the BASIC and Kernal ROMs don't touch in their standard operation, and which are free for programs to use.) With the screen a uniformly blank Commodore blue, we can draw out the blockfield as a 20x20 area. The display of the C64 allows each character to have a foreground colour in text mode, but the background colour is shared across the whole screen; if we want to use colour RAM to indicate which blocks are white and which are black (and we do), we'll want a character that's all foreground colour with no holes.

§4 Human · 0%

Figure 3: C64 Character set 1, in two sets of 128Source: Jodigi, admin of the C64 Wiki[5]"Character set", C64 Wiki, updated Aug 2025 As the second half of the character set contains reverse-video copies of the first half, we can use the reverse-video version of SPACE (character 160) as our fully-foreground block. Our loop is a little more complicated: to write rows of 20 characters on a screen that's 40 wide, we need to write 20 and skip 20; additionally, we'll want to put this block centrally on the screen, so we need to start one row down and 10 characters in. Our resultant pointer is actually one row and nine characters after the start of screen RAM, because we can achieve the effect of writing 20 bytes in a row either by starting at 0 and going up to 19 (aborting at 20), or starting at 20 and coming down to 1 (aborting at 0). The latter saves us a compare, so is both quicker and smaller in code size. lda #<(SCRRAM + 40 + 9) sta BLOCKPTR lda #>(SCRRAM + 40 + 9) sta BLOCKPTR + 1 ; This time we're writing 20 rows of 20, char 160 ; We'd normally think of this as X being along the line, and Y down ; but the 65xx only offers indirect addressing with Y as the index ldx #20 .field: lda #160 ldy #20 .row: sta (BLOCKPTR), y dey bne .row ; Add 40 to the BLOCKPTR (16-bit add) ; Clobbers A, will need reloading at the top of the .field loop lda BLOCKPTR adc #40 sta BLOCKPTR lda BLOCKPTR + 1 adc #0 sta BLOCKPTR + 1 dex bne .field With the screen RAM fully set up, next comes the colour RAM.

§5 Human · 0%

As it turns out (peeking ahead) we'll be using a double-buffering technique to simplify the bounce calculations: that means an internal buffer of 400 contiguous black-or-white values for the blockfield, which is rendered to colour RAM each frame. So our initialisation becomes setup of the internal blockfield buffer: BLOCKBUF = $CE00 ; 512 bytes unused RAM, which is ample lda #<BLOCKBUF sta BLOCKPTR lda #>BLOCKBUF sta BLOCKPTR + 1 ; The initial blockfield is half black (colour 0), half white (1) ; In other words, 20 rows of 10 black and 10 white ; In other other words, 40 areas of alternating black and white lda #0 ldx #40 .fill: ; The inner loop, writing the area of 10 (either black or white) ; This time, write from 9 to 0 inclusive, and stop at -1 ldy #9 .fill_row: sta (BLOCKPTR), y dey bpl .fill_row ; Add 10 to BLOCKPTR (16-bit add) ; Clobbers A, but this time we can't just write a fixed value afresh ; So push A to the stack pha lda BLOCKPTR adc #10 sta BLOCKPTR lda BLOCKPTR + 1 adc #0 sta BLOCKPTR + 1 ; Pull A back off the stack, and XOR 1 to swap between 0 and 1 pla eor #1 dex bne .fill And we're almost done with the initialisation steps. There are two things left to do: set up the sprites for our smoothly-moving balls, and connect the hook for our main routine.

§6 Human · 1%

So we'll need to design an 8x8-pixel representation of a ball; it turns out that the Minecraft scene has a plethora of pixelated shape generators online, so we can use a circle generator[6]"Pixel Circle Generator", Donat Studios, updated Jul 2025 set to 8x8: Figure 4: Pixel Circle Generator set to filled circles of width 8 Just like the text characters on a C64, sprites can have a foreground colour for any pixels that are set in the sprite, and a background that shines through for pixels that aren't set. That means sprites can be defined in binary; the text characters are 8x8 pixels, but sprites are 24x21, and the video chip expects binary data corresponding to that size when setting up the sprite. Our ball itself is only 8x8 pixels, so the rest will need to be padded out with 0's. 24x21 bits of data is 63 contiguous bytes, so the C64's video chip expects a pointer to the sprite data to be aligned to a 64-byte boundary; as a result, it actually expects the pointer divided by 64 as a sort of "data block index" during setup of the sprite. As we mentioned earlier, the pointers to the sprite are, by default, immediately after the screen text in RAM: SPRPTR = SCRRAM + (40 * 25) lda #(ballmap / 64) sta SPRPTR ; Sprite 0 sta SPRPTR + 1 ; Sprite 1 ; This will end up at the bottom of our source code, ; in a "data section". For now it's presented alongside align 64 ballmap: byte %00111100, %00000000, %00000000 byte %01111110, %00000000,

§7 Human · 3%

%00000000 byte %11111111, %00000000, %00000000 byte %11111111, %00000000, %00000000 byte %11111111, %00000000, %00000000 byte %11111111, %00000000, %00000000 byte %01111110, %00000000, %00000000 byte %00111100, %00000000, %00000000 byte %00000000, %00000000, %00000000 byte %00000000, %00000000, %00000000 byte %00000000, %00000000, %00000000 byte %00000000, %00000000, %00000000 byte %00000000, %00000000, %00000000 byte %00000000, %00000000, %00000000 byte %00000000, %00000000, %00000000 byte %00000000, %00000000, %00000000 byte %00000000, %00000000, %00000000 byte %00000000, %00000000, %00000000 byte %00000000,