While working on psychedelia syndrome I finally disassembled the Psychedelia sub-game in Batalyx. Eventually I'll fill out a chapter in the book on how it evolved from the original Psychedelia release, but toying with the sub-game, which is really just a glorified pause-mode, gave me an idea of how Psychedelia might be turned into an actual game.
There isn't a whole lot of point in going to the trouble of disassembling C64 binaries if you can't have a bit of fun while you're at it. The Batalyx sub-game is an ideal little base for building something new on. It contains a slightly modified version of the core from the 1984 original with nearly all of the knobs and dials stripped out, so developing a game around it involves a lot more adding my crap than deleting Jeff Minter's precious code.
My modest little notion is to give the player a simple objective: reach every part of the screen
with the assigned pattern. They receive points for every tile they fill and lose points every
time the fill the same tile more than once. If you click on the frame below, you should be able
to give it a quick go, using CTRL
to fire and the arrow keys for moving around. If
that doesn't work for you, you
can try playing it in a new tab.
Look at it in all its glory..
The game is essentially restful: there is no dying or losing. You never get bumped back to Level 1 again - the only way is forward, as slowly or as quickly as you please. You can lose points sure, but you really would have to zone out completely to find yourself reduced back to zero. Perhaps that's a different challenge: get as far as you can with as few points as possible!
If you're interested in what the source code for such a game looks like, you can take a look at it here. Here are a couple of things in there that are kind of interesting.
You may have noticed that the unfilled background scrolls away beneath the filled tiles. This is a nice little effect stolen from Gridrunner. This is what the routine looks like:
;------------------------------------------------------------------------- ; PerformRollingGridAnimation ;------------------------------------------------------------------------- PerformRollingGridAnimation INC frameControlCounter LDA frameControlCounter AND #$01 BEQ ScrollGrid RTS ScrollGrid LDA unpaintedGrid + $0007 STA rollingGridPreviousChar LDX #$07 _Loop LDA unpaintedGrid - $0001,X STA unpaintedGrid,X DEX BNE _Loop LDA rollingGridPreviousChar STA unpaintedGrid RTS
unpaintedGrid
refers to this chap, our 8-byte definition of the character used to
display the background:
unpaintedGrid .BYTE $10,$00,$00,$82,$00,$00,$10,$00 ;.BYTE $10,$00,$10,$BA,$10,$00,$10,$00 ; CHARACTER $83 ; 00010000 * ; 00000000 ; 00010000 ; 10111010 * * ; 00010000 ; 00000000 ; 00010000 * ; 00000000
As you can see the trick involves rotating the bytes around one byte at a time. We call this little routine once every time just before the screen is painted (the screen is painted 60 times a second, or 50 times a second if you're in Europe). We manage this by setting a single interrupt in the Raster Interrupt feature in the C64 and doing both this bit of business and checking for joystick input while we're at it.
There are three little routines below. The first one, SetUpInterrupts
sets up our interrupt.
It tells the C64 to call TitleScreenInterruptHandler
whenever it is about to paint a line
or lines on the screen that we specify. We specify these lines in UpdateRasterPosition
.
In here we specify the 256th line (LDA $FF
) by storing $FF
in register
$D012
.
;--------------------------------------------------------------- ; SetUpInterrupts ;--------------------------------------------------------------- SetUpInterrupts LDA #$7F STA $DC0D ;CIA1: CIA Interrupt Control Register LDA #<TitleScreenInterruptHandler STA $0314 ;IRQ LDA #>TitleScreenInterruptHandler STA $0315 ;IRQ JSR UpdateRasterPosition LDA #$01 STA $D01A ;VIC Interrupt Mask Register (IMR) RTS ;--------------------------------------------------------------- ; UpdateRasterPosition ;--------------------------------------------------------------- UpdateRasterPosition LDA $D011 ;VIC Control Register 1 AND #$7F STA $D011 ;VIC Control Register 1 ; Set the position of the next interrupt LDA #$FF STA $D012 ;Raster Position ; Acknowledge the interrupt LDA #$01 STA $D019 ;VIC Interrupt Request Register (IRR) RTS
Finally we have our little routine which is called every time line 256 on the screen is about to be painted
roughly 50 times every second. This is where we call PerformRollingGridAnimation
:
;--------------------------------------------------------------- ; TitleScreenInterruptHandler ;--------------------------------------------------------------- TitleScreenInterruptHandler LDA $D019 ;VIC Interrupt Request Register (IRR) AND #$01 BNE RasterPositionMatchesRequestedInterrupt JMP $EA31 ; Returns RasterPositionMatchesRequestedInterrupt JSR CheckJoystickAndUpdateCursor JSR PerformRollingGridAnimation JSR $FF9F ;$FF9F - scan keyboard JSR UpdateRasterPosition ; Sounds are turned off for now ;JSR PlaySoundEffects ReturnFromInterrupt PLA TAY PLA TAX PLA RTI
I've included all the patterns from the original game, but really there's no point in a game being too easy all of the time. For this reason I've sprinkled a few harder-to-fill patterns throughout the levels where it takes a little more deliberation by the player to fill the screen without burning a lot of hard-earned points. Because I'm lazy these are randomly generated by a little Python script.
Psychedlia Syndrome goes into a bit of depth on how the pattern data structure works (I see I've only mentioned my book-in-progress twice already so remedying that here) but the principle is very simple. This is what the pattern data structure for the pattern used in the Batalyx subgame looks like:
; The pattern array data. patternXPosArray .BYTE $FF,$01,$55 ; 6 .BYTE $FE,$02,$55 ; 5 .BYTE $FD,$03,$55 ; 4 .BYTE $FC,$04,$55 ; 3 .BYTE $FB,$05,$55 ; 2 .BYTE $FA,$06,$55 ; 1 .BYTE $55,$55 ; patternYPosArray ; 1 .BYTE $01,$FF,$55 ; 2 .BYTE $FE,$02,$55 ; 3 .BYTE $03,$FD,$55 ; 4 .BYTE $FC,$04,$55 ; 5 .BYTE $05,$FB,$55 ; 6 .BYTE $FA,$06,$55 .BYTE $55,$55
If you're reading this far you're probably the sort that can guess what's going on here without too
much assistance. What we have is a pair of arrays, one of which gives the X positions of the pattern's
pixels and the other which gives the Y positions. The $55
is a separator or sentinel value,
which we've made plainer here by separating each segment of each array onto its own line. Finally,
the values are given relative to an origin. So $01
in the first value in patternYPosArray
tells us that the pixel is 'plus one' from the origin, i.e. one row down. Meanwhile $FF
given as the
first value in patternXPosArray
might be a little more puzzling. It is in fact 'minus one'. This is because
if you subtract 1 from 0 in binary, we rotate around to 255, or $FF
. Likewise -2 is given as $FE
.
The way Psychedelia works is by processing one 'line' from each of the arrays at a time. The littl ascii diagram to the right of the data structure is my attempt to illustrate this. The '1's in that diagram are painted by the first lines ine each array, the '2's by the second line in ear array, and so on.
If this has whetted your appetite for even more gory detail, can you guess what I recommend? That's right, my omnium gatherum of all things Psychedelia, 'psychedelia syndrome'. It may not be finished but it is already packed with excessive detail.