heckmeck!

Nerd content and
cringe since 1999

Alexander Grupe
Losso/ATW

VCCC 2025: Snowflake

This is my entry for this year’s Vintage Computing Christmas Challenge (VC³) by Logiker. The challenge: Recreate this snowflake made of asterisks on the platform of your choice, make your code as small as possible, and have as much fun as possible doing it. :)

Previous VC³ fun challenges I participated in: A present box in 2024, and a diamond pattern in 2023.

Prototyping

After the first shock (“This shape is way more complex than last year!”), I quickly got sidetracked by all the symmetries and built a JavaScript prototype around that. The idea: Mirror and transform all the points and only include so many explicit coordinate pairs.

For the Amiga version, I glanced at the BCPL global vector for a bit, looking for some output function we can call without much overhead. Maybe we can use writef, feed it the coordinates, and print everything with row/column-based escape codes?

// Print a "*" at row y, column x
// Possible ANSI escape sequences:
// <CSI>y;xH
// <CSI>y;xf

writef("\x9b%d;%dH*", y, x)

Turns out working with this particular BCPL function is horrible and I quickly lost my patience. The string constant needs to be longword-aligned, you have to pass it as a BCPL pointer (i. e. shifted to the right by 2), it isn’t null-terminated, but has its length in the first byte, and then I only had text output under Kickstart 2+. No fun!

So I ditched writef and friends and went for an alert requester again (like last year). Here, we can pass the coordinates as bytes by building a special alert text structure:

 dc.w 320       ; x coordinate (2 bytes)
 dc.b 128       ; y coordinate (1 byte)
 dc.b '*',0     ; text, null-terminated
 dc.b 1         ; continuation byte not 0 = more text chunks to come

 dc.w 328       ; next star at 328, 136
 dc.b 136
 dc.b '*',0
 dc.b 1

 ...

 dc.w 384       ; final star at 384, 192
 dc.b 192
 dc.b '*',0
 dc.b 0         ; continuation byte is 0 = end of alert

My entry

After some evenings of staring at the code, preparing everything for a release, and then finding another way to save two bytes, I ended up with a solution that takes up 84 bytes of code and data:

        move.l  368(a2),a6      ; intuition base
        clr.l   -(a7)           ; alert text end marker
        lea     .coords(pc),a1
        moveq   #8*8,d4         ; diagonal loop initialization
.loop   subq.b  #8,d4           ; loop until d4 < 0
        move.b  d4,d0           ; and set y := x
        bge.b   .star
        move.b  -(a1),d4        ; otherwise, read x,y from coords
        move.b  -(a1),d0
        blt.b   .done           ; negative value marks the end
.star   move.w  d0,d6           ; x working copy
        move.w  d4,d7           ; y working copy
        add.l   (a2),d6         ; more-or-less center x, (a2) = 150
        move.l  #$802a0001,-(a7); y center = 0x80, star = 0x2a,
                                ; string terminator, continuation = 1
        add.b   d7,(a7)         ; add y.b to 0x80
        move.w  d6,-(a7)        ; write x.w
        neg.w   d0              ; mirror: x := -x
        blt.b   .star
        neg.w   d4              ; mirror: y := -y
        blt.b   .star
        exg     d0,d4           ; mirror: swap x, y
        neg.w   d1              ; use d1 negation to loop twice
        blt.b   .star
        clr.w   d0              ; transform: x := 0
        neg.w   d5              ; use d5 negation to loop twice
        blt.b   .star
        bra.b   .loop
.done   move.l  d0,d1           ; d1 = height (d0 is 255 here)
        move.l  a7,a0           ; a0 = stack = alert message
        jmp     -90(a6)         ; DisplayAlert
                                ; jmp -90(a6) is 0xffa6 in binary,
                                ; and 0xff is used as end marker
        dc.b    9*8,0*8
        dc.b    4*8,1*8
        dc.b    5*8,2*8
        dc.b    1*8,7*8
        dc.b    2*8,8*8
        dc.b    6*8,5*8
        dc.b    7*8,5*8
.coords

The code takes two nasty shortcuts:

  • The alert message is constructed on the stack (a7), but the stack pointer isn’t restored on exit. In other words, if you click to dismiss the alert, you will get a real guru meditation…
  • The last bytes of the alert message need to be five zero bytes to cleanly terminate the list of message chunks:
     ; last message chunk generated by .star
     dc.w 320
     dc.b 128,'*',0,1
    
     ; final message chunk
     dc.w 0     ; dummy x
     dc.b 0,0,0 ; dummy y, empty string, continuation = 0
    But clr.l -(a7) only clears four bytes – we quietly assume the top of the stack contains a memory address like 0x00ff492e when we get executed, where the top byte is already zero.

Other strategies

At 84 bytes, my entry lands on a 55th (-ish) place overall, if I counted correctly. The shortest entry is a ZX Spectrum program with only 33 bytes. And, much to my delight, there were four other Amiga entries this year!

  • A text-based 94-byte snowflake by Saturnus the Invincible of SCA (using writeoutput from the BCPL global vector)
  • A text-based 114-byte snowflake by DrSnuggles (using wrch from the BCPL vector for output; see also my 2023 entry)
  • A cool bootblock with 290 bytes also by DrSnuggles, working with a custom copperlist and graphics library functions to render the stars (it seems fast ram should be disabled for this one)
  • A text-based snowflake by Nicalejo written in E, with the source code taking up 209 bytes.

Many of the other entries don’t bother with coordinates and encode a quadrant of the snowflake as a bit pattern instead:

 ; snowflake data from STI’s entry

 dc.b %00000000 ; ********
 dc.b %01101101 ; *  *  * 
 dc.b %10110110 ;  *  *  *
 dc.b %11011111 ;   *     
 dc.b %01101111 ; *  *    
 dc.b %10110001 ;  *  *** 
 dc.b %11110011 ;     **  
 dc.b %01110101 ; *   * * 
 dc.b %10111111 ;  *      

Interesting! The code around those solutions looks more elegant, too – maybe I was too focused on mirror coordinates early on?

Improvements

Unused register

When writing this post, I noticed a very obvious optimization in the code: I needlessly put the y coordinate in an extra register. We can rewrite this to:

        ...
.star   move.w  d0,d6
        move.w  d4,d7
        add.l   (a2),d6
        move.l  #$802a0001,-(a7)
        add.b   d7,(a7)
        add.b   d4,(a7)
        ...

This brings down the code+data size to 82 bytes. If there is a post challenge this year, this might be a version I can upload – unless there’s something else I’m missing. The fun never ends! :)

Keep the stack

In STI’s snowflake code, this line caught my attention:

; see: sca-flake.asm in STI’s entry

_main:  move.l  d2,a0   ; there's 380 free bytes for us here

So, d2 is initially pointing to an area of free space? Interesting! As a little experiment, I changed the target buffer from a7 to a0:

        move.l  d2,a0	; d2 points to an empty-ish area
        clr.l   -(a7)   ; no end-marker bytes needed
        ...
.star
        ...
        move.l  #$802a0001,-(a7)-(a0)
        add.b   d4,(a7)(a0)
        move.w  d6,-(a7)-(a0)
        ...
        move.l  a7,a0   ; a0 already set
	jmp     -90(a6) ; DisplayAlert

This is still a bit hacky – I take d2 and fill the memory region backwards, while the usable memory is after the address in d2. This will usually overwrite the mousepointer, but it will keep the stack intact (no crash when exiting) – and the total code size is down to 80 bytes!

Or we could use this information to make a better-behaving program: We fill the buffer starting from d2 upwards, not downwards, keep the overall code size, but cleanly exit to the system. That is, as long as we only write 380 bytes or less, I’d have to measure that…

Downloads

previous next close