Thursday 27 February 2020

C64 quickie - autorun assembly code


One thing my previously mentioned Snake 1K game didn't do was automatically launch. Typing in SYS 4096 every time is not a huge task, but it nonetheless gets old fast.

It's actually a very simple issue to solve, though - so I thought I'd hammer out a very quick explanation of how to do this for any assembly code program.

On the C64, BASIC starts at memory location 2048 (or $0800 in Hex). All we need to do to create a launcher for our code is to "assemble" in some data at that location that forms a BASIC program, that in turn runs our code.

This doesn't take that much effort to do:

You can easily change the ASCII values for the SYS address to any address you wish to place your code at. Each value is simply represented as Hex code $3x, where 'x' is the value between 0 and 9, forming (in this case) 2062.

If you assemble a program built with this at 2048, the .prg (or a.out) file produced will automatically create the BASIC code when loaded. If you drag the file into VICE's window, you won't even need to type RUN as VICE does this automatically in this situation.


Anyway, I hope that is useful! Until next time :)

[Copy-Paste-able version of code now follows...]

   processor 6502

    org 2048 ; start of BASIC program area in memory

    ; BASIC launcher

    .byte $00                ; first byte has to be zero for BASIC program to work
    .byte $0c, $08           ; pointer to next BASIC line in memory, i.e. $080C
    .byte $0a, $00, $9e, $20 ; the code for: 10 SYS (includes trailing space)
    .byte $32, $30, $36, $32 ; ASCII version of SYS address, i.e. 2062
    .byte $00                ; line terminator
    .byte $00, $00           ; the pointer above points to this place in memory
                             ; as we have no other line, we leave both bytes zero
                             ; end of program

    ; memory location 2062 comes next, which our SYS command above calls

    ; (i.e. 2048 + 14 bytes for our BASIC program above)

start:
    ; your C64 assembly program goes here
    ; to demonstrate, this simple program just changes the screen color

    jsr $e544 ; clear the screen
    lda #$00
    sta $d020 ; border to black
    lda #$05
    sta $d021 ; background to green
    lda #$00  ; Black text
    sta $0286

    rts ; back to BASIC!


Saturday 22 February 2020

The Dark Heart of Uukrul [RPG | DOS]


I've recently been playing this hugely underrated gem, and thought it worth a mention here.

The Dark Heart of Uukrul is a very old-skool RPG from a bygone era (way back when in the dark magical mists of 1989), that these days runs most effectively via DOSBox. No config is required, other than mounting the folder you unpack the files to as the C drive. It just works. (Pro Tip: the game files can very easily be found with a simple Google search.)

The game has no sound of any kind whatsoever, which on the plus side does at least mean no fiddling with IRQ and DMA settings to persuade it to run. Pick a solid track listing from Spotify and off you go!

After a short and lightly comical character creation sequence, you are dropped straight into the corridors of the ancient city under the mountain, in which your gaming session takes place.


The exploration view is reminiscent of Bards Tale era games, with some Dungeon Master-ness thrown in for good measure. Cursor (or ASDW) keys move you around the landscape, where you will encounter colorful descriptions of the rooms passed through, puzzles to stump even a veteran of RPGs of its - or any other - era; and monsters most foul - all comprising the elements of your quest to defeat the evil Uukrul of the title.

On the subject of monsters, whenever conflict arises, the game switches to an Ultima-style top-down 2D view for the duration of the battle, where turns are taken between your characters and the enemies.


Movement is a simple matter of choosing one of the eight available directions for each character, and weapon-based combat is also straightforward: just select which adjacent enemy to fight, and you're good to go (hit or miss).

Magic requires actual knowledge of the mystical words of power, so having a physical copy of the manual to hand as you play is a definite plus here (I chose to have the PDF version open in another window, and switch to it from DOSBox as required). It also requires your character to possess the appropriate magic rings of the correct element (e.g. Iron, Copper, etc) to trigger the spell or prayer.

Normal sorcery is just a matter of typing the name of your spell, and choosing a target (if applicable), and if you have the spell points, then that's it.

Priest spells on the other hand require the favour of the appropriate god to work, and more often than not, that favour is not forthcoming - meaning a missed turn (or even worse, a punishment from said deity for the irritation you have caused them).

I'm still fathoming all the details of this system, so I guarantee this will have more than enough surprises to entertain you as you journey into ever more perilous encounters.

In fact, I'm not all that far into the game as yet, but the premise and first rate design of the game is compelling me ever further into it. I'd quite like to get back to it right now, in fact...

If you've never heard of this game, and would like to probe the depths of the history of gaming (and RPGs), then you can certainly do a lot worse than this title. Apparently, it sold very poorly on its release, which in my opinion is a criminal injustice for such an extremely interesting and individual title.

Just one more thing...

It also occurred to me that my recent forays into the world of the C64 could perhaps benefit from consideration here. A game of this sort wouldn't be too difficult to craft on that machine (and I'm already thinking of algorithms for the two views, and how to store the data, dialogue and descriptions to fit in 64K).

I am not promising anything just yet, but I may well create a C64 homage to this title with a plot and world of my own devising... (Perhaps) watch this space?

UPDATE:

Had a very quick go on PetDraw (written by the 8-Bit Guy), and produced this:


This is on the C64 (in VICE) using just character mode and with no redefined characters (I would need some for the real thing, but this is just a proof of concept at this stage). With either careful use of a custom character set, and/or some sprites over the top, this could work very well I think!

Sunday 16 February 2020

C64 Snake Game in under 1K


Here is very simple game of Snake, written in 100% 6502 assembly for the C64.

In total, it comes in well under 1K of memory in size (628 bytes to be exact). The only other memory it uses when running are the two 16-bit general purpose vectors in zero page memory, some of the SID voice 3 vectors (for a random number generator) and the video memory of the C64 itself.

In fact, if you discount the snake body pieces buffer (which once the code is loaded could in theory be placed anywhere in the C64's RAM), the code in fact comes at 458 bytes in size, and so could quite happily sit on the 512 byte boot block of an old-style floppy disk with room to spare.

It's certainly not the best version of Snake in the world - it lacks a score, any music and joystick support. (Keyboard controls are P - up, L - down, Z - left and X - right). But it works, uses the entire character area of the screen and has a certain degree of challenge.

Mainly, it was an exercise in properly learning the 65xx processor instruction set, while trying to come in under 1024 bytes in size as an extra personal challenge.

Anyway, copy-paste the code into a new text file renamed to 'snake1k.asm', and assemble it using:

    dasm snake1k.asm

then drag the resulting a.out file into VICE to try it out using:

    SYS 4096

from BASIC. I've also tested it on theC64 (or Maxi as some like to call it), where it also works just fine.

Short video of it running here:

https://www.youtube.com/watch?v=vOrLySWMXFY

Enjoy!

processor 6502
org $1000

NUMBER_OF_SNAKE_PARTS_TO_ADD = 4
FOOD_OUT_OF_RANGE_POS        = $FF

SNAKE_DIRECTION_LEFT = 0
SNAKE_DIRECTION_RIGHT = 1
SNAKE_DIRECTION_UP = 2
SNAKE_DIRECTION_DOWN = 3

_start:
; RND technique using SID from:
; https://www.atarimagazines.com/compute/issue72/random_numbers.php
LDA #$FF  ; maximum frequency value
STA $D40E ; voice 3 frequency low byte
STA $D40F ; voice 3 frequency high byte
LDA #$80  ; noise waveform, gate bit off
STA $D412 ; voice 3 control register

jsr $e544 ; clear the screen

; Initialize everything
ldy #0
init:
lda default_values,y
sta snake_head_x,y
iny
cpy #[body_directions - snake_head_x + 1]
            ; +1 to include first body byte
bne init

draw_snake_head:
ldx snake_head_x
ldy snake_head_y
jsr calculate_snake_screen_address
            ; exits with y == 0

lda #87
sta ($FD),y ; y should be zero by here

; save head address for use when erasing later
lda $FD
sta $FB
lda $FE
sta $FC

draw_snake_tail:
ldx snake_tail_x
ldy snake_tail_y
jsr calculate_snake_screen_address

lda #81
sta ($FD),y ; y should be zero by here

; delay wait to make each update playable :)
ldx #$20
wait:
ldy #$FF
wait_inner_loop:
dey
bne wait_inner_loop
dex
bne wait

; erase head
lda #81
sta ($FB),y ; y should be zero by here, ptr == head position

; erase tail
lda #$20
sta ($FD),y ; y should be zero by here, ptr == tail position

; move_head
lda body_directions
and #%11000000 ; keep only head direction
bne check_head_right
dec snake_head_x
check_head_right:
cmp #$40
bne check_head_up
inc snake_head_x
check_head_up:
cmp #$80
bne check_head_down
dec snake_head_y
check_head_down:
cmp #$C0
bne move_head_done
inc snake_head_y

move_head_done:
; Add a single snake part (if required) for this game loop
lda snake_parts_to_add
beq move_tail
inc snake_length
dec snake_parts_to_add
jmp skip_tail_move

move_tail:
; Get snake tail direction, move tail
ldx snake_length

; we now need to divide by 4 to get the byte position
txa
clc
ror
clc
ror
tay ; y is now buffer bytes offset

txa
and #$03
tax
        ; x == number of 2-bit shifts in byte to get tail direction

lda body_directions,y
; a == direction value for four possible body parts

direction_shift_loop:
dex
bmi tail_direction_found
rol
rol
jmp direction_shift_loop

tail_direction_found:
and #$C0 ; get just that one direction in top two bits

; a == tail direction in two high bits
and #%11000000 ; keep only tail direction
bne check_tail_right
dec snake_tail_x
check_tail_right:
cmp #$40
bne check_tail_up
inc snake_tail_x
check_tail_up:
cmp #$80
bne check_tail_down
dec snake_tail_y
check_tail_down:
cmp #$C0
bne skip_tail_move
inc snake_tail_y

skip_tail_move:
; Update all body directions as snake moves
lda body_directions
and #$C0
tay
jsr update_body_directions
jsr update_body_directions
lda #$3F
and body_directions
sta body_directions
tya
ora body_directions
sta body_directions

; check for all collisions, with edges and snake parts
ldx snake_head_x
cpx #$FF ; gone past zero, so hit screen edge
beq game_over

cpx #40
beq game_over

ldx snake_head_y
cpx #$FF
beq game_over

cpx #25
beq game_over

ldx snake_head_x
ldy snake_head_y
jsr calculate_snake_screen_address
lda ($FD),y ; y should be zero here
tax

        ; increase snake length, clear food
cpx #83 ; food?
bne check_for_obstacle
lda #NUMBER_OF_SNAKE_PARTS_TO_ADD
sta snake_parts_to_add
lda #FOOD_OUT_OF_RANGE_POS
sta food_x
sta food_y
bne collision_checks_done

check_for_obstacle:
cpx #$20
bne game_over

collision_checks_done:
jsr read_keys

; food never appears at FOOD_OUT_OF_RANGE_POS
lda #FOOD_OUT_OF_RANGE_POS
cmp food_x
bne draw_food
cmp food_y
bne draw_food

jsr get_rnd_pos
stx food_x
sty food_y

draw_food:
ldx food_x
ldy food_y
jsr calculate_snake_screen_address
lda #83
sta ($FD),y ; y should be zero after calculation jsr

jmp draw_snake_head

; Game Over - restart the game
game_over:
ldx #0
stx 198 ; clear key buffer
jmp _start

; Entry: none
; Exit: x == xcoord, y == ycoord, a corrupt
get_rnd_pos:
ldx $D41B ; get RND from SID
get_rnd_x_loop:
cpx #40
bcc get_rnd_y
txa
clc
sbc #40
tax
bne get_rnd_x_loop

get_rnd_y:
ldy $D41B ; get RND from SID
get_rnd_y_loop:
cpy #25
bcc get_rnd_done
tya
clc
sbc #25
tay
bne get_rnd_x_loop

get_rnd_done:
rts

update_body_directions:
ldx #0
update_body_loop:
rol ; use carry from A
            ; (first time through will be garbage but that's okay)
ror body_directions,x
ror ; temp store carry in A for next time round the loop (and next byte)
inx
cpx #170
bne update_body_loop
rts

; Read Keys and modify head direction accordingly
; Exit: a,x corrupt
read_keys:
clv
ldx 197 ; read key buffer
cpx #41 ; P
bne check_down_key
ldx #[SNAKE_DIRECTION_UP << 6]
bvc key_direction_update

check_down_key:
cpx #42 ; L
bne check_left_key
ldx #[SNAKE_DIRECTION_DOWN << 6]
bvc key_direction_update

check_left_key:
cpx #12 ; Z
bne check_right_key
ldx #[SNAKE_DIRECTION_LEFT << 6]
bvc key_direction_update

check_right_key:
cpx #23 ; X
bne check_keys_done
ldx #[SNAKE_DIRECTION_RIGHT << 6]

key_direction_update:
lda body_directions
and #$3F ; Keep all bits except top two (head direction)
sta body_directions
txa ; get new direction for head
ora body_directions
sta body_directions

check_keys_done:
rts

; Calculate Snake Screen Address
; Entry: x, y - screen coordinates
; Exit: y == 0, x,a corrupt
calculate_snake_screen_address:
; initialize character screen memory pointer
lda #0
sta $FD
lda #$04
sta $FE

; calculate actual address
row_address_calc:
cpy #0
clc
beq row_address_calc_done
lda #40
adc $FD
sta $FD
lda #0
adc $FE
sta $FE
dey
bvc row_address_calc ; 2nd adc never overflows, so safe
row_address_calc_done:
txa
adc $FD
sta $FD
tya ; y guaranteed to be zero here, saves a byte over lda #0
adc $FE
sta $FE
rts

default_values:
dc.b 19,12, 20,12, 1, 4, FOOD_OUT_OF_RANGE_POS, FOOD_OUT_OF_RANGE_POS, 0

snake_head_x:
dc.b 0
snake_head_y:
dc.b 0
snake_tail_x:
dc.b 0
snake_tail_y:
dc.b 0
snake_length:
dc.b 0
snake_parts_to_add:
dc.b 0
food_x:
dc.b FOOD_OUT_OF_RANGE_POS
food_y:
dc.b FOOD_OUT_OF_RANGE_POS
body_directions:
ds.b 170, 0 ; each byte has four body part directions in it,
    ; each direction two bits,
    ; = left, %01 = right,
    ; %10 = up,   %11 = down

_end:
; label only here to measure length of program