Programming my Commodore 64

Advent of Code 2024 Day 1 & 2 - Ultimate 64 - Vision Basic

Below are the final versions of day 1 and 2 of AOC 2024. These were written in Vision Basic 1.1 as a way to learn more about that language and programming environment. I coded these on an Ultimate 64 (Commodore 64 in FPGA) and used its 48MHz mode to speed up the process. As such, and because of the nature of AOC, the code is not optimized for speed but for accuracy in the final result.

Getting the Data

At first I tried converting the input data into a sequence (SEQ) file and reading that byte-for-byte. While that worked it was slow, as the 48MHz speed of the U64 didn't do much to speed up disk access when reading in bytes in that way.

Eventually I settled on taking the input data into Visual Studio Code, using the command to add a cursor to every line, and then prepending DATA to the beginning of every line. Then a quick find-replace to put a comma where there was a space between numbers. I used the petcat command which comes with the VICE C64 emulator to convert that text file into a PRG file.

petcat -f -w2 -o day1data.prg -- day1data.txt

Loading day1data.prg in Vision Basic converts the Commodore BASIC file into a Vision Basic .vis file.

LOAD"DATA1DATA.PRG"

And then I renumbered it using the RENUM command to start on line 3000 so there were plenty of lines available before the DATA statements to write my solution.

RENUM 0-2000,3000,1

Reading and Sorting and Adding

Reading in data is fast.

Sorting not so much. I used an insertion sort, which seemed appropriate since I was sorting each number as it was read and inserted. There were two lists of 1000 numbers each. In Vision Basic I had to use a DECIMAL type for these numbers since the max INTEGER value is 65535 and the inputs could go up to 99999. Again I used a DECIMAL to store the sum of distances, which thankfully did not exceed the ~16 million limit. Integers would have been faster.

Things started slowing down after about 700 inserts. In all, the process took about 60 seconds to finish. That's at 48MHz and reading from DATA statements with zero disk access. I didn't attempt a full run at the regular ~1MHz speed. At the 60 second mark with no turbo it had inserted 150 numbers into each array. Subsequent inserts would be increasingly slower, and I'm guessing total time would have been 30+ minutes.

It should be noted that Vision Basic compiles to 6502 machine code. It also allows you to inline assembly. Even without assembly it provides alternative--and often faster--ways of implementing the same thing. It's very likely that speed increases would be seen with more time spent optimizing.

Day 1 Part 2 and Larger Sums

The second half of the challenge has us calculating similarity scores and summing them. This sum was too great for the DECIMAL type, but it would fit within 2 of them, so once the sum got too big I printed it out, zeroed the sum, and kept on adding. That resulted in two large numbers, which I used a calculator to sum and get the final answer. Yep, I did.

Vision Basic has a concept of modules and program banks, so in the future there could be a "large numbers" module that could be imported to provide procedures for working with number types outside the range of the DECIMAL type.

This part took about 10 seconds to run.

Vision Basic

Most of my time was spent learning Vision Basic and its quirks. Several times I was sure I found a bug, but on a more careful reading of the manual and trying out lots of code, I found I was wrong. Vision Basic goes by the beat of its own drum, and that's fine by me. I'm in this for the journey. And we're lucky to have new software written for 40-year-old machines!

Its dialect of BASIC isn't quite as flexible as BASIC but it makes up for it by offering a lot of commands for graphics and sound and sprites that you don't get in BASIC (kind of like Simons Basic). Additionally, it gives you more than enough Assembly language to make things fast or shoot yourself in the foot trying. To me, it's a nice balance of flexibility and speed.

It uses the Ultimate 64's turbo mode (or SuperCPU or Turbo Chameleon, maybe others), so compiles happen in a second or two, and it adds some nice developer tools to the experience as well. I wish there were more examples in the manual--or anywhere--and I wish I didn't have to read and re-read it as much as I did. (It's wordy and sometimes the information you need is spread out in different sections.) And I kind of wish the editor made line numbers optional. Overall, though, I recommend it for those wanting a little more modern take on native C64 development.

Printouts

For brevity I haven't included the full input data sets, but here are parts 1 and 2 of day one's solutions. I printed these as RAW files using the Ultimate 64's virtual printer and Vision Basic's very handy PLIST command, which sends your entire program listing to the printer.

Day 2

I took everything I learned from Day 1 (which took more days than just one...) into day 2, and went straight into converting the input data into DATA statements. This made for large files in Vision Basic, and despite them taking maybe 10 seconds to load they compiled in about 1 second.

By part 2 I started to embrace creating user-defined procedures. I did a lot of experimenting with program structure and really wanted to use the LOCAL keyword to contain the scope of my procedures. However, by doing so I could find no way to either 1) get access to global variables within a locally-scoped procedure; or 2) pass into my procedure a reference/pointer to an array. It seems like this should be possible but I couldn't figure it out without dissecting the array structure in assembly. Maybe there is no way to de-reference again?

So instead I embraced procedures and global variables. It felt pretty good and properly vintage, but you do have to keep track of which variables you're using in which procedures, or risk introducing some hard-to-spot bugs. I tried to avoid having large procedures call other procedures, which was usually the cause of this problem.

Day 1 Part 1

0 CLR:CLS
10 POKE 53296,1:; U64 TURBO
30 DIM DECIMAL ROW1(1000),DECIMAL ROW2(1000)
40 DEF DECIMAL N1,N2,SUM,DIS
50 DEF INT N: PL=1000:PLL=PL-1
60 DECIMAL T: T=0
70 LET N$="",A$=""
80 LET I=0,J=0,ROW=0
90 :
110 *PREADLINE
120 READ ROW1(ROW): PRINT ROW1(ROW);: P@SORT1
140 READ ROW2(ROW): PRINT ROW2(ROW);: P@SORT2
160 PRINT "ROW"ROW
170 INC ROW: IF ROW<PL THEN GOTO PREADLINE
185 PRINT"DONE READING AND SORTING": KEYPRESS
190 PRINT: FOR I=0 TO PLL: PRINT ROW1(I),ROW2(I): NEXT I
210 KEYPRESS:PRINT
220 FOR I=0 TO PLL
230 DIS=ROW1(I)-ROW2(I): DIS=ABS(DIS)
240 SUM=SUM+DIS
250 PRINT DIS,SUM
260 NEXT I
270 PRINT"THE ANSWER IS"SUM: KEYPRESS
290 END:END
500 ;-------------------
502 ;
504 ; SORT1
506 ;
508 PROC P@SORT1
510 IF ROW=0 THEN RETURN
512 I=ROW
514 *CHECK
516 J=I-1
518 IF I=0 OR ROW1(I) >= ROW1(J) THEN RETURN
520 T=ROW1(J):ROW1(J)=ROW1(I):ROW1(I)=T
522 I=I-1: GOTO CHECK
524 RETURN
600 ;-------------------
602 ;
604 ; SORT2
606 ;
608 PROC P@SORT2
610 IF ROW=0 THEN RETURN
612 I=ROW
614 *CHECK
616 J=I-1
618 IF I=0 OR ROW2(I) >= ROW2(J) THEN RETURN
620 T=ROW2(J):ROW2(J)=ROW2(I):ROW2(I)=T
622 I=I-1: GOTO CHECK
624 RETURN
2990 ;
2991 ; DATA
2992 ;
2999 END
3000 DATA 56208,95668
3002 DATA 52621,74203
3004 DATA 95252,33335
3006 DATA 79799,26047
3008 DATA 88005,37435
3010 DATA 61887,93836
3012 DATA 48454,95821
3014 DATA 62543,40154

Day 1 Part 2

0 CLR:CLS
10 POKE 53296,1
30 DIM DECIMAL ROW1(1000),DECIMAL ROW2(1000)
40 DEF DECIMAL SUM
50 PL=1000:PLL=PL-1
60 DECIMAL T: T=0
80 LET I=0,J=,ROW=,N=
90 :
91 LET PN=3
95 PRINT"READING DATA..."
110 *PREADLINE
120 READ ROW1(ROW),ROW2(ROW)
160 LOC 15,0 ROW
170 INC ROW: DO PREADLINE,PL
190 :
195 GOSUB T@PRINT
200 *ILOOP: ;FOR I=0 TO PLL
210 T=ROW1(I)
220 *JLOOP: ;FOR J=0 TO PLL
230 IF T <> ROW2(J) THEN 250
231 SUM=SUM+T
232 IF SUM < 15000000 THEN 250
233 GOSUB T@PRINT: SUM=0
237 INC N: INC PN
250 INC J: DO JLOOP,PL: J=0
252 GOSUB T@PRINT
254 INC I: DO ILOOP,PL
260 PRINT "SIMILARITY SCORE...SUM IT": KEYPRESS
270 END
301 *T@PRINT: LOC 0,PN I,J,SUM
302 RETURN
999 :
2990 ;
2991 ; DATA
2992 ;
2999 END
3000 DATA 56208,95668
3002 DATA 52621,74203
3004 DATA 95252,33335
3006 DATA 79799,26047
3008 DATA 88005,37435
3010 DATA 61887,93836
3012 DATA 48454,95821
3014 DATA 62543,40154

Day 2 Part 1

Kind of a brute force effort here -- ended up making it easy to walk through each result to figure out what was going on. I improved this a lot (I think) in part 2.

0 CLR:CLS
10 POKE 53296,1
30 LET A=0,ROW=,UP=,LAST=,SAFE=,SC=,DIS=,N=
33 DD=100
35 DEF TAG T@NEXTROW,T@FINDEND,T@CHECKDIS
90 :
100 *T@BEGIN: PRINT ROW;:N=0:LAST=0:UP=0
110 *T@READ
120 LAST=A: READ A: PRINT A;:KEYPRESS
130 IF N=0 THEN INC N: GOTO T@READ
140 IF A=DD AND SAFE THEN INC SC: PRINT"ENDS";: GOTO T@NEXTROW
141 IF A=DD THEN PRINT"END";: GOTO T@NEXTROW
143 IF LAST=A THEN PRINT"SAME";: GOTO T@FINDEND
150 IF UP=0 AND LAST<A THEN UP=2: GOTO T@CHECKDIS
170 IF UP=0 AND LAST>A THEN UP=1: GOTO T@CHECKDIS
190 IF UP=2 AND LAST>A THEN SAFE=0: GOTO T@FINDEND
200 IF UP=1 AND LAST<A THEN SAFE=0: GOTO T@FINDEND
210 *T@CHECKDIS
211 IF UP=2 THEN DIS=A-LAST
212 IF UP=1 THEN DIS=LAST-A
220 IF DIS<1 OR DIS>3 THEN SAFE=0: PRINT"DIS"LAST;A;DIS;: GOTO T@FINDEND
230 SAFE=1: GOTO T@READ
240 *T@NEXTROW: PRINT: INC ROW: GOTO T@BEGIN
250 *T@FINDEND: READ A: IF A<>DD THEN GOTO T@FINDEND: GOTO T@NEXTROW
260 PRINT"SAFE REPORTS:"SC
270 END
999 :
3000 DATA 51,52,55,58,60,61,62,61,100
3001 DATA 64,65,67,70,72,74,77,77,100
3002 DATA 2,4,6,9,11,14,18,100
3003 DATA 79,81,82,84,86,88,91,97,100
3004 DATA 81,83,84,81,83,100
3005 DATA 4,6,9,10,7,8,9,6,100
3006 DATA 65,68,66,69,69,100
3007 DATA 80,82,79,81,84,85,88,92,100
3008 DATA 42,43,41,42,49,100
3009 DATA 2,4,7,8,11,14,14,15,100
3010 DATA 46,48,49,49,47,100
3011 DATA 28,30,31,34,34,37,37,100
3012 DATA 26,29,31,34,37,39,39,43,100
3013 DATA 62,63,66,66,72,100
3014 DATA 82,84,88,91,92,93,95,97,100

Day 2 Part 2

I re-wrote this one a few times, each time breaking things out more into procedures to try and eliminate bugs. I felt like I was starting to hit my stride.

0 CLR:CLS
10 POKE 53296,1: ;U64 TURBO
30 LET A=0,X=,Y=,N=,I=,J=,E=,COUNT=
31 LET A1=0,X1=,Y1=
34 DIM R(10),O(10)
36 DEF TAG TEND,TNEXT
90 :
100 *MAIN
101 PREADROW
102 CLS
105 IF A=999 THEN GOTO TEND
110 PCHECKER.N
140 IF E=0 THEN PRINT"SAFE": INC COUNT
141 ELSE PRINT"UNSAFE"
145 ;KEYPRESS" "
150 GOTO MAIN
198 :
199 :
200 PROC PREADROW
201 : N=0
202 : *TNEXT: READ A
204 : IF A=999 THEN RETURN
206 : O(N)=A
208 : IF A=100 THEN RETURN
210 : INC N: GOTO TNEXT
298 :
299 :
300 PROC PCHECK
305 : A=0:Y=0:N=0:X=1:E=0
310 : *TCHECK
315 : IF R(X)=100 THEN RETURN
317 : IF R(N)=R(X) THEN E=1: RETURN
320 : IF R(N)<R(X) THEN A=R(X)-R(N)
325 : IF R(N)>R(X) THEN A=R(N)-R(X)
332 : IF A>3 THEN E=2: RETURN
340 : IF N>=1 AND R(Y)<R(N) AND R(N)>R(X) THEN E=3: RETURN
342 : IF N>=1 AND R(Y)>R(N) AND R(N)<R(X) THEN E=4: RETURN
370 : INC N: INC X
372 : IF N>0 THEN Y=N-1
375 : GOTO TCHECK
398 :
399 :
400 PROC PCHECKER.X1
401 : PRINT"LENGTH"X1
402 : A1=-1
404 : *PCN
406 : PCOPYO2R.A1: PPRINT
408 : PCHECK: PRINT"SKIP"A1;: PDISPLAY
410 : IF E<>0 AND A1<>X1 THEN INC A1: GOTO PCN
440 : RETURN
498 :
499 :
500 PROC PCOPYO2R.SKIP
502 : A=0
504 : FOR I=0 TO 9
506 : IF I<>SKIP THEN R(A)=O(I): INC A
508 : NEXT I
510 : RETURN
518 :
519 :
520 PROC PPRINT
522 : FOR I=0 TO 9: PRINT R(I);: NEXT I
523 : PRINT
530 : RETURN
558 :
559 :
560 PROC PDISPLAY
561 : IF E=0 THEN RETURN
562 : PRINT"ERROR ";
564 : IF E=1 THEN PRINT"S"
566 : IF E=2 THEN PRINT"D"
568 : IF E=3 THEN PRINT">"
570 : IF E=4 THEN PRINT"<"
572 RETURN
898 :
899 :
900 *TEND
901 PRINT"COUNT";COUNT
910 END
999 :
3000 DATA 51,52,55,58,60,61,62,61,100
3001 DATA 64,65,67,70,72,74,77,77,100
3002 DATA 2,4,6,9,11,14,18,100
3003 DATA 79,81,82,84,86,88,91,97,100
3004 DATA 81,83,84,81,83,100
3005 DATA 4,6,9,10,7,8,9,6,100
3006 DATA 65,68,66,69,69,100
3007 DATA 80,82,79,81,84,85,88,92,100
3008 DATA 42,43,41,42,49,100
3009 DATA 2,4,7,8,11,14,14,15,100
3010 DATA 46,48,49,49,47,100
3011 DATA 28,30,31,34,34,37,37,100
3012 DATA 26,29,31,34,37,39,39,43,100
3013 DATA 62,63,66,66,72,100
3014 DATA 82,84,88,91,92,93,95,97,100

Conclusion

And that's it. No day 3 for me, I think. It took 11 days to do 2 days. And day 3 requires even more manipulation of the input data in order to massage it into a SEQ file or a bunch of strings within DATA statements. But who knows, maybe a day of rest will refresh me enough to take a stab at it.

There is a lot more to explore with Vision Basic, so I'll have to come up with something to help exercise its other parts.

#aoc2024 #ultimate64 #vision basic