Skip to content

Instantly share code, notes, and snippets.

@danstowell
Created January 13, 2025 19:49
Show Gist options
  • Save danstowell/661b5f8d871a6b5c4d2a27db9b6b53dd to your computer and use it in GitHub Desktop.
Save danstowell/661b5f8d871a6b5c4d2a27db9b6b53dd to your computer and use it in GitHub Desktop.
UXN code for "linf_land" demo by me 2024: https://www.youtube.com/watch?v=5dqfhiMWZdk
( linf_land_1.tal
~/dev/uxn/uxnasm linf_land_1.tal linf_land_1.rom && ~/dev/uxn/uxncli linf_land_1.rom
~/dev/uxn/uxnasm linf_land_1.tal linf_land_1.rom && ~/dev/uxn/uxnemu -3x linf_land_1.rom
)
( devices )
|00 @System [ &vector $2 &pad $6 &r $2 &g $2 &b $2 ]
|20 @Screen [ &vector $2 &width $2 &height $2 &pad $2 &x $2 &y $2 &addr $2 &pixel $1 &sprite ]
|c0 @DateTime [ &year $2 &month $1 &day $1 &hour $1 &minute $1 &second $1 &dotw $1 &doty $2 &isdst $1 ]
( zero page )
|0000
@framecounter $2
( ------------------------------------------------------------------------------------------------------ )
( Some simple macros )
%NL { #0a #18 DEO } ( -- )
( these printdigit methods are BAD, DON'T USE THEM! They don't work for "abcdef" values. )
%PRINTDIGIT { #30 ADD #18 DEO } ( number -- )
%PRINTDIGITS { DUP #04 SFT PRINTDIGIT #0f AND PRINTDIGIT } ( number -- )
( minimum of two values )
%MIN { LTHk JMP SWP POP } ( b c -- retval )
( maximum of two values )
%MAX { GTHk JMP SWP POP } ( b c -- retval )
( modular distance 1D )
%moddist1d {
SUBk
SWP ROT
SUB
MIN
} ( b c -- dist )
( ------------------------------------------------------------------------------------------------------ )
( main program )
|0100
( set system colors )
#29ec .System/r DEO2
#01c0 .System/g DEO2
#2ce5 .System/b DEO2
( outputting some basics, and building up to outputting the results of our distance functions )
#08 #0f MIN PRINTDIGITS NL ( 08 )
#10 #23 moddist1d PRINTDIGITS NL ( 13 )
#23 #10 moddist1d PRINTDIGITS NL ( 13 )
#03 #fe moddist1d PRINTDIGITS NL ( 05 )
;val1 ;val2 #02 ;distLinf JSR2 PRINTDIGITS NL ( 05 )
;valtmp ;val4 #02 ;distLinf JSR2 PRINTDIGITS NL ( 7< )
( ------------------------------------------------------------------------------------------------- )
( now some work with "nnquerytable" -- initialising it and testing that it works )
( First we load data INTO the querytable )
;nnquerytable
;valtmp OVR2 STA2
#0002 ADD2 ;val1 OVR2 STA2
#0002 ADD2 ;val2 OVR2 STA2
#0002 ADD2 ;val3 OVR2 STA2
#0002 ADD2 ;val4 OVR2 STA2
NL
( and at last -- invoke our nearest-neighbour subroutine, and print its outcomes )
#02 #04 ;nnquerytable ;nearestneighbour JSR2 ( ndims ncentroids tbladdrHILO -- bestindex bestdistance )
PRINTDIGITS NL PRINTDIGITS NL ( bestdistance is printed first, then bestindex: 44, 03 )
( ------------------------------------------------------------------------------------------------- )
( NOW LET'S DRAW FUN THINGS )
#00 .framecounter STZ
;on-frame .Screen/vector DEO2
BRK
( ------------------------------------------------------------------------------------------------------ )
@on-frame ( -> )
.framecounter LDZ INC #01 AND DUP .framecounter STZ ( increment and store )
#00 EQU JMP BRK
;erasecentroids JSR2
;erasegrid JSR2
;makeamove JSR2
;drawcentroids JSR2
;drawvoronoigrid JSR2
BRK
( ------------------------------------------------------------------------------------------------------ )
( data )
@val1 07 04
@val2 09 09
@val3 dc 76
@val4 82 f2
@valtmp 98 76 ( the current position to be used for queries will be loaded into this "variable" )
( the "query table". the ADDRESS of valtmp will be loaded into the first entry, the others will be for the reference positions )
@nnquerytable $10 ( reserve space: for each pointer one short, thus 2 * (1 + numcentroids) )
( sprite )
@square fe82 8282 8282 fe00
@dotter 1010 10fe 1010 1000
@eraser ffff ffff ffff ffff
( ------------------------------------------------------------------------------------------------------ )
@drawcentroids
;square .Screen/addr DEO2
#00 ;val1 LDA2 #00 SWP .Screen/y DEO2 .Screen/x DEO2
#41 .Screen/sprite DEO
#00 ;val2 LDA2 #00 SWP .Screen/y DEO2 .Screen/x DEO2
#42 .Screen/sprite DEO
#00 ;val3 LDA2 #00 SWP .Screen/y DEO2 .Screen/x DEO2
#43 .Screen/sprite DEO
#00 ;val4 LDA2 #00 SWP .Screen/y DEO2 .Screen/x DEO2
#44 .Screen/sprite DEO
JMP2r
@erasecentroids
;eraser .Screen/addr DEO2
#00 ;val1 LDA2 #00 SWP .Screen/y DEO2 .Screen/x DEO2
#40 .Screen/sprite DEO
#00 ;val2 LDA2 #00 SWP .Screen/y DEO2 .Screen/x DEO2
#40 .Screen/sprite DEO
#00 ;val3 LDA2 #00 SWP .Screen/y DEO2 .Screen/x DEO2
#40 .Screen/sprite DEO
#00 ;val4 LDA2 #00 SWP .Screen/y DEO2 .Screen/x DEO2
#40 .Screen/sprite DEO
JMP2r
@drawvoronoigrid
;dotter .Screen/addr DEO2
#f0 #f0 ;valtmp STA2 ( initialise our iteration counters )
&whilegrid1
&whilegrid2
#00 ;valtmp LDA2 #00 SWP .Screen/y DEO2 .Screen/x DEO2 ( set draw location )
( find the NN index, and use index+1 as the colour byte )
( a neat trick! our "grid-point" value is already being pushed into "valtmp" and thus into the query table )
#02 #04 ;nnquerytable ;nearestneighbour JSR2 ( ndims ncentroids tbladdrHILO -- bestindex bestdistance )
POP .Screen/sprite DEO ( pop discards the distance value, and - another neat trick! - the index becomes the colour val consumed by sprite )
( if the position has hit zero, we want to skip out )
;valtmp LDA DUP #00 EQU ,&endwhilegrid2 JCN
( else let's decrement )
#10 SUB ;valtmp STA
,&whilegrid2 JMP
&endwhilegrid2
POP
( if the position has hit zero, we want to skip out )
;valtmp #01 ADD LDA DUP #00 EQU ,&endwhilegrid1 JCN
( else let's decrement )
#10 SUB ;valtmp #01 ADD STA
#f0 ;valtmp STA ( put the x counter back to its high starting point )
,&whilegrid1 JMP
&endwhilegrid1
POP
JMP2r
( I repeat the above code for the eraser. Not sure how best to do it with less duplication. )
@erasegrid
;eraser .Screen/addr DEO2
#f0 #f0 ;valtmp STA2 ( initialise our iteration counters )
&whilegrid1
&whilegrid2
#00 ;valtmp LDA2 #00 SWP .Screen/y DEO2 .Screen/x DEO2 ( set draw location )
#00 .Screen/sprite DEO ( pop discards the distance value, and - another neat trick! - the index becomes the colour val consumed by sprite )
( if the position has hit zero, we want to skip out )
;valtmp LDA DUP #00 EQU ,&endwhilegrid2 JCN
( else let's decrement )
#10 SUB ;valtmp STA
,&whilegrid2 JMP
&endwhilegrid2
POP
( if the position has hit zero, we want to skip out )
;valtmp #01 ADD LDA DUP #00 EQU ,&endwhilegrid1 JCN
( else let's decrement )
#10 SUB ;valtmp #01 ADD STA
#f0 ;valtmp STA ( put the x counter back to its high starting point )
,&whilegrid1 JMP
&endwhilegrid1
POP
JMP2r
( ------------------------------------------------------------------------------------------------------ )
@makeamove
.DateTime/second DEI ( we use the clockseconds as a source of simple arbitrariness and change )
DUP #03 AND #01 ADD ( which centroid to move (seconds & 03 + 1) )
OVR #01 AND ( which axis (seconds & 1) )
ROT #03 AND #00 EQU #10 SFT #01 SUB #10 SFT ( and whether it's +1 or -1 (something consistent, unchanging in each onesecond bout, maybe (seconds_mod_4==0)*2-1) )
( now we should have [ whichc whichaxis adder ] ... )
ROT #10 SFT ;nnquerytable ROT #00 SWP ADD2 LDA2 ( now whichaxis adder vectoraddress2 )
OVR2 POP #00 SWP ADD2 ( now adder addhere2 )
ROT #00 OVR2 LDA NIP ADD ( now addhere2 newval )
ROT ROT STA
JMP2r
( ------------------------------------------------------------------------------------------------------ )
( L-inf distance calculated modulo, APPLIED TO VECTORLISTS IN MAIN MEMORY, written as a subroutine which we then put after the main )
@distLinf ( point1addr point2addr dim -- thedist )
( HOW THIS WORKS: we progressively increment the pointers and decrement "dim" until it hits zero, accumulating the distance as we go. )
( push a zero at stackpos2 - this is the distance value, already finished if dim equals zero! )
#00 SWP ( Stack: point1HILO point2HILO thedist dim )
&loopsbacktohere
DUP #00 EQU ,&exithere JCN ( if the dim (at top of stack) is zero, jump to ,&exithere, we've done it all )
( decrement dim, and push the dim DOWN BELOW THE DIST because we'll not need it for the rest of this iteration )
#01 SUB SWP
( here we use short-rotting to get the pointers to the top of the stack,
retrieve their values onto the stack, and increment the pointers. )
( there are some dummy "00" values used for padding in here. I wonder if it can be achieved without them. )
ROT2 ROT2
LDAk #00
ROT2
LDAk #00 SWP2
INC2 ( point1 has been incremented )
ROT2 ROT2 POP SWP POP
ROT2 INC2 ( point2 has been incremented )
SWP2 ( Stack now: dim thedist point1HILO point2HILO val1 val2 )
( here we calculate the next 1Ddistance, then rearrange so we can combine it (max-it) with the numbers so far )
moddist1d
( the tricky part: we want to rearrange to get thedist and newdist together, which requires some shortsmashing............... )
ROT2 ROT ROT
MAX ( max is how we combine dimensions, because it's L-inf distance )
ROT2 ROT ROT ( Stack: point2HILO point1HILO thedist dim )
( We're now BACK YAYYYY in the arrangement we entered the loop with! )
,&loopsbacktohere JMP
&exithere
( We now want to REDUCE the stack leaving only "thedist" but we've got point2HILO point1HILO thedist dim )
ROT2 ROT2 POP2 POP2 POP
JMP2r
( ------------------------------------------------------------------------------------------------------ )
( Find which of the given centroids is the NN. )
( IMPORTANT NOTES about the arguments and returns:
* It uses 1-based indexing: the returned value is in the range [1, ncentroids]
* The "tbladdr" should be a pointer to a table, which itself contains ncentroids+1 addrs. The first is the addr of the query point, the rest are the centroids.
)
@nearestneighbour ( ndims ncentroids tbladdrHILO -- bestindex bestdistance )
(
Implementation note: instead of keeping "ncentroids" living in here, I first create a pointer
to where the LASTcentroid is stored i.e. currentcentroidaddr = tbladdr + 2 * ncentroids
and then to handle the iteration I simply do currentcentroidaddr -= 2 and terminate if it MATCHES tbladdr.
)
( create a pointer initialised to where the LASTcentroid is stored i.e. currentcentroidaddr = tbladdr + 2 * ncentroids )
ROT #00 SWP
#10 SFT2 ADD2k NIP2 ( stack: ndims tbladdrHILO currentcentroidaddr )
( instantiate a "bestindex" value at zero and a "bestdist" value at ff -- these will live on the RETURNstack )
LIT2r 00ff ( Stack: ndims tbladdrHILO currentaddrHILO | besti bestd )
&while
( rotate and duplicate, to get the "dist" routine-call arguments ready.
duplication because the routine will consume them, but we also need to dereference the pointers. )
ROT2k ROT2 LDA2 ROT2 LDA2 ROT2 NIP ( stack: ndims tbladdrHILO currentaddrHILO tbladdrHILOgot currentaddrHILOgot ndims )
;distLinf JSR2 ( point1addr point2addr dim -> newdist )
( test whether the new distance is better. if not then we simply forget it and skip onwards. if so then we update our besties. )
DUP STHkr GTH ?&afterupdatebesties ( i believe this leaves the stack unchanged )
( now update besties )
POP2r
STH ( the dist goes to return stack, note it should end up on top though ... )
SWP2k SUB2 #01 SFT2 STH POP
#00 SWPr
&afterupdatebesties
POP ( stack: ndims tbladdrHILO currentaddrHILO | besti bestd )
( decrement currentcentroidaddr -= 2, and if it's reached tbladdr then we boing out of the loop, ready to finish up. )
#0002 SUB2 NEQ2k ?&while
( set the return values nicely. clear the stack values and then put those 2 items from the return stack in their place. )
POP2 POP2 POP STH2r
JMP2r
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment