Skip to main content

On Sale: GamesAssetsToolsTabletopComics
Indie game storeFree gamesFun gamesHorror games
Game developmentAssetsComics
SalesBundles
Jobs
TagsGame Engines
(+1)

Here's yet another program - which uses both the drawing routine and the overlay loader that I have posted about earlier, and the preprocessor I've mentioned.

This time, the program is an implementation of the classic Snake game.


Classic WASD control scheme, plus R for reset/restart after a crash (hitting the wall or yourself). (Only lowercase, though. I'd need one more instruction to also handle uppercase, and I don't have room for that, plus it'd be slower.)

This is, yet again, for the default mode of Senbir, though it only barely fits. It requires that the overlay loader is in ROM (instead of including it on disk), and still fills the disk entirely (all 256 words). A couple of the overlays use all the available RAM (with the median being 19 words). And every register - all 16 of them - is used for something.

It's awkwardly slow during gameplay (though I haven't really measured it, I think it's about 5 or 6 seconds per step), but given enough patience, it's playable.

And that's after I tried pretty hard to optimize it...

I believe most of that slowness is because the main loop of the game is 3 overlays long, with another overlay for when we need to add a new food, and a couple more to handle a crash and restart. (Plus a couple for initialization.) I'm pretty sure that the repeated overlay loading takes a lot more time than running the game code itself does. So, no surprise really, the main limiting factor for speed is the RAM size.

Which is only part of the reason the main runtime scratch space of the game is actually on the disk - another reason is that with the TC-06 ISA it's simply easier to read/write from/to the disk than from/to dynamic areas of the RAM.

While I've fixed several bugs already, it's not quite bug-free yet, though the only known bug (besides the slowness) is pretty unlikely, as it requires you to fill the entire board with your snake first. (If you do that, then crash into something and restart, you're left with no food in the new game... Wouldn't be hard to fix if I had room for more code, but I don't, so... yeah.)

I've actually been considering replacing that restart code with simply re-initializing the game as if you rebooted. It would save some code space, and fix that bug. But it wouldn't look as nice, nor (probably) be as fast. Oh well. We'll see.

Anyway, the source code is on GitHub, along with the preprocessor it uses.

I've included an already-preprocessed copy of the code below, in case anyone wants to just run it without going through the trouble of getting the source and using the preprocessor.

In this copy, I've removed all the lines that only contained a comment, to shorten it down (preprocessor output was 335 lines). If you want to read the code, not just use it, I'd recommend looking at the actual source code.

DATAC 00000000000000000000000000010010 // End address for overlay
MOVI 6 16 // R6 = ram[..] : load address of data to be drawn
MATH 7 7 1 // R7 = 0 : initialization for updating only parts of it
MATH 3 3 1 // R3 = 0 : initialization for updating only parts of it
MATH 2 2 1 // R2 = 0 : initialization for updating only parts of it
GETDATA 1 3 6 // R1 = read(disk, R6) : load next step
MATH 15 6 0 // R6++ : update address
PMOV 1 2 0 8 0 0 // R2[0:8] = R1[ 0: 8] : set start pixel
PMOV 1 3 9 17 9 0 // R3[0:8] = R1[ 9:17] : set end pixel
IFJMP 1 14 0 // jump to done if R2 == R3
PMOV 1 7 18 26 18 0 // R7[0:8] = R1[18:26] : set position increment
SETDATA 0 3 2 // Draw a pixel from R2
MATH 7 2 0 // R2 += R7 : increment position
IFJMP 1 10 1 // jump to loop if R2 != R3
JMP 1 4 // jump to next otherwise
MOVI 2 17
JMP 3 9
DATAC 00000000000000000000000000010011
DATAC 00000000000000000000000000011001
DATAC 10000100110111011100000000100000 // Main area fill, TL-to-BR
DATAC 01111011100111111111111100000000 // Bottom row, R-to-L
DATAC 01000011000111111111111111100000 // Left column, B-to-T
DATAC 01000100010000000000000100000000 // Top row, L-to-R
DATAC 01111100110000000000000000100000 // Right column, T-to-B
NIL // End of list
DATAC 00000000000000000000000000101100 // End address for overlay
MOVI 0 18 // R0 = 6
MOVI 4 13 // R4 = start position (and color)
MOVI 5 14 // R5 = direction
MOVI 8 17 // R8 = 14
MOVI 9 15 // R9 = address of history array
MOVI 10 16 // R10 = max history size
MATH 15 11 5 // R11 = 1 : add two segments to start (1+food)
MATH 9 12 5 // R12 = R9 : tail is at first element of array
MATH 9 13 5 // R13 = R9 : head is at first element of array
SETDATA 1 3 9 4 // Save head into array
SETDATA 0 3 4 // Draw current head pos
SET 2 3 00101101
JMP 3 9
DATAC 00011110000000000000000000000000
DATAC 00000000000000000000000011111110
DATAC 00000000000000000000000010101000
DATAC 00000000000000000000000001010100
DATAC 00000000000000000000000000001110
DATAC 00000000000000000000000000000110
DATAC 00000000000000000000000001000011 // End address for overlay
MATH 13 2 5 // R2 = R13 : head of list
MATH 15 2 0 // R2++ : add one
MATH 10 2 4 // R2 %= R10 : mod size of list
MATH 12 3 5 // R3 = R12 : tail of list
MATH 10 3 4 // R3 %= R10 : mod size of list
IFJMP 1 19 0 // if head is one behind tail, then screen is full
MATH 15 11 0 // R11++ : add a segment to the snake tail
MATH 8 2 6 // R2 = random number within 0-13 : X pos - 1 of food
MATH 0 3 6 // R3 = random number within 0-5 : Y pos -1 of food
MATH 15 2 0 // R2++ : X pos of food
MATH 15 3 0 // R3++ : Y pos of food
PMOV 3 3 0 31 25 0 // R3 = .. : move Y pos into place
PMOV 2 3 28 31 28 0 // R3 = .. : move X pos into place
SET 3 3 00000010 // R3 = .. : set expected color
GETDATA 0 3 3 // R1 = current pixel in that position
PMOV 1 2 0 31 2 0 // R2 = R1(shifted) : current pixel for comparison
IFJMP 1 7 1 // if not the same, try again
PMOV 14 1 2 3 2 0 // R1 = .. : move color into place
SETDATA 0 3 1 // Draw food pixel
MOVI 2 21 // high bytes are set, so can't use SET here.
JMP 3 9
DATAC 00000000000000000000000010010100
DATAC 00000000000000000000000001010111 // End address for overlay
GETDATA 1 3 5 // R1 = read(disk, R5) : load direction value
MATH 1 4 0 // R4 += R1 : update position by adding direction value to it
PMOV 4 6 2 31 2 0 // R6[0:29] = R4[2:31] : set R6 to screen address of new pos
MATH 2 2 1 // R2 = 0
MATH 11 3 5 // R3 = R11
IFJMP 1 8 0 // if R11 == 0 then goto erase_segment
MATH 15 11 1 // R11-- : decrement number of segments to add
JMP 1 14
GETDATA 1 3 12 // R1 = read(disk, R12) : load tail position from history
MATH 15 12 0 // R12++ : increment history position of last tail segment
MATH 10 12 4 // R12 %= R10 : wrap around size of history
MATH 9 12 0 // R12 += R9 : re-add address of array to its index
PMOV 14 1 3 4 3 0 // R1[0:1] = R14[3:4] : set color for old pos to bg-color
SETDATA 0 3 1 // erase old position
GETDATA 0 3 6 // R1 = old pixel from the new position
SETDATA 0 3 4 // draw new position
MATH 1 6 5 // R6 = R1 : keep old pixel for later
SET 2 3 01011000
JMP 3 9
DATAC 00000000000000000000000001101000 // End address for overlay
MATH 15 13 0 // R13++ : increment history position of head segment
MATH 10 13 4 // R13 %= R10 : wrap around size of history
MATH 9 13 0 // R13 += R9 : re-add address of array to its index
SETDATA 1 3 13 4 // write current position to head of history array
PMOV 15 6 0 29 2 1 // R6[2:31] = 0 : clear all but color
PMOV 6 3 0 31 2 0 // R3 = R6(shifted) : copy color for comparison
SET 2 3 00000010 // R2 = 0b10 : the background color for comparison
IFJMP 1 14 0 // if old color was bg-color, goto done
SET 2 3 00000011
IFJMP 1 12 0 // if old color was food color, goto food
SET 2 3 01101001
JMP 3 9
SET 2 3 00101101
JMP 3 9
SET 2 3 10010100
JMP 3 9
DATAC 00000000000000000000000001111100 // End address for overlay
SETDATA 0 0 0000000000000000000000 // set status pixel to black to show crashed
SET 2 3 01110010 // R2 = 'r'
GETDATA 2 0 0000000000000000000000 // R1 = keyboard input
MATH 1 3 5 // R3 = R1 : for comparison
IFJMP 1 2 1 // if R2 != R3 then it was not pressed yet, so loop
MATH 13 3 5 // R3 = R13 : snake head
MATH 12 2 5 // R2 = R12 : snake tail
GETDATA 1 3 2 // R1 = read(disk, R2) : load tail position from history
PMOV 14 1 3 4 3 0 // R1[0:1] = R14[3:4] : set color for old pos to bg-color
SETDATA 0 3 1 // erase old position
IFJMP 1 15 0 // if tail == head then we're done
MATH 15 2 0 // R2++ : increment history position of last tail segment
MATH 10 2 4 // R2 %= R10 : wrap around size of history
MATH 9 2 0 // R2 += R9 : re-add address of array to its index
JMP 1 7 // loop back up for next tail segment
MATH 13 12 5 // R12 = R13 : reset tail position address to that of the head
MATH 1 6 5 // R6 = R1 : save head pixel so we don't need to re-load it
SET 2 3 01111101
JMP 3 9
DATAC 00000000000000000000000010010011 // End address for overlay
MATH 2 2 1 // R2 = 0
MATH 3 3 1 // R3 = 0
PMOV 6 3 6 8 9 0 // R3[29:31] = R6[6:8] : copy Y position
MOVI 7 20 // R7 = 7
MATH 7 3 4 // R3 %= R7 : mod-7 so that both 0 and 7 become 0
IFJMP 1 10 0 // if head-X is 0 or 15, goto make_white
PMOV 6 3 2 5 6 0 // R3[28:31] = R6[2:5] : copy X position
SET 7 3 00001111 // R7 = 15
MATH 7 3 4 // R3 %= R7 : mod-15 so that both 0 and 15 become 0
IFJMP 1 12 1 // if head-X is not 0 or 15, goto not_white
PMOV 15 6 30 31 2 1 // R6[0:1] = R15[30:31] : set color to 0b01
SETDATA 0 3 6 // draw pixel as white (to fix the border)
MOVI 4 21 // R4 = start position (and color)
SET 11 3 00000010 // R11 = 2 : add two segments immediately
PMOV 11 5 30 31 0 0 // R5[offset] = 0b10 : set starting direction
SETDATA 0 0 0100000000000000000000 // reset status pixel to white
SETDATA 1 3 13 4 // save head position to history
SETDATA 0 3 4 // draw head
SET 2 3 10010100
JMP 3 9
DATAC 00000000000000000000000000000111
DATAC 00011110000000000000000000000000
DATAC 00000000000000000000000010100111 // End address for overlay
MATH 2 2 1 // R2 = 0
GETDATA 2 0 0000000000000000000000 // R1 = keyboard input
MATH 1 6 5 // R6 = R1 : keep this key for later
GETDATA 2 0 0000000000000000000000 // R1 = keyboard input
MATH 1 3 5 // R3 = R1 : for comparison
IFJMP 1 2 1 // if R2 != R3 then another key was pressed, so loop
MATH 6 3 5 // R3 = R6 : for comparison
SET 2 3 01110111 // R2 = 'w' (up)
IFJMP 1 16 0 // if this key pressed, goto set_direction
SET 2 3 01100001 // R2 = 'a' (left)
IFJMP 1 16 0 // if this key pressed, goto set_direction
SET 2 3 01110011 // R2 = 's' (down)
IFJMP 1 16 0 // if this key pressed, goto set_direction
SET 2 3 01100100 // R2 = 'd' (right)
IFJMP 1 16 0 // if this key pressed, goto set_direction
JMP 1 17
PMOV 6 5 29 30 1 1 // R5[offset] = new direction (taken from key)
SET 2 3 01000100
JMP 3 9
NILLIST 84
DATAC 11111100000000000000000000000000 // 0b00 : a : left
DATAC 00000000100000000000000000000000 // 0b01 : s : down
DATAC 00000100000000000000000000000000 // 0b10 : d : right
DATAC 11111111100000000000000000000000 // 0b11 : w : up

This is incredible - the first full-featured game made for not just the TC-06 architecture, but the default mode version that has 128 bytes of RAM and a 1kB drive?  Mind blowingly cool.  Played in a Custom mode preset with some different colors, and a 1kHz clockspeed to have more than 0.15 frames per second?  It's a proper Snake game, complete with the real tension of needing to make decisions on the fly.  I do not have words for the levels of cool.  Also, naturally, I'm absolutely terrible at it.  >.<

I found what I initially thought was a bug while trying to do some fancy maneuvering around a food item, but I think it might just be that I ran into myself.  I don't think there's anything to stop you from turning in the direction you came from - e.g, going left to right, trying to change direction to be right to left functions just fine, and in a buttonmash panic, it's very easy to do this and get a game over.

Snake?  SNAAAAAAKE!

1kHz clock, custom colorpalette specially for Snake, otherwise just the Snake program in Default Mode. SUPER COOL OMG

I think you officially qualify as being better with the TC-06 than I am.  I haven't done anything nearly as cool as this, my main specialty seems to be planning out grand ideas that I'll never actually get around to implementing.  >_<

(+1)

Well, it's not like Snake is really an easy game. It may seem like it at first glance, but it's harder than it looks. (At least at a decent speed like that.) You have to keep track of quite a few things, while managing your action speed (not too fast, nor too slow). I tend to prefer games where I can take my time to think, but this was a game I thought I could actually implement.

One thing I'll note about this implementation is that it's possible to change your mind on which direction you're going, as long as you do it before the game is done checking the keyboard for that step - it takes whichever key was the last one you pressed. Obviously, that's harder to take advantage of at the 1kHz speed than at 60Hz, but still possible (at least in theory).

This is different from some other implementations I've seen, where pressing a key will actually make it immediately move in that direction (allowing you to go faster than the timer if you want to), or queue up inputs so you can press e.g. up-right-down and then sit back for a few frames. (Mine did the queue at first, but I'm finding it tricky to hit the game objects at first try (and there's no real visual indication of having done it), so I'd often end up starting with a few Fs in the queue...)


Yeah, switching direction back the way you came is pretty much an instant game over, since that counts as colliding with yourself (it actually does that backwards move and sees the collision as with any other tail hit). That's part of the reason you start the game with 2 tail segments, so that it's consistent that way from the start.

I think I've seen that be a game over in other implementations as well (as opposed to not allowing you to make that turn, which I'm less sure I've seen), but it's been a while, so I can't be sure. In your case (at least in the gif) it might not even have been that, you might have just switched direction towards the right too soon.

It would have been easier to tell if I had made the head a different color, like I wanted to, because then we could see where it ended up. But I both ran out of code space and wanted to minimize the run time since it was so slow, so... yeah.


That custom mode looks pretty good, and with that speed it does seem a lot more playable (or at least enjoyable).


Well, thank you? I'd guess it's probably mostly that I've been doing fairly small and self-contained projects that are graphical in some way, which makes them appear more interesting and lets them be ready sooner. (I've also been spending rather more time on Senbir stuff lately than I probably should...) (Though I won't deny that having experience playing around with related things probably helps too.)

(+1)

I wasn't sure whether to make this a separate top-level reply, but I guess it's mostly about the same thing, really, so, sub-reply it is, I suppose. Even though some of it is rather different.

To make a long story short, I made a new variant of the Snake game, which is not based on overlays but on another idea I had for enabling use of more code than you have memory. This variant can be started with the built-in bootloader, instead of needing a custom ROM.

This variant actually runs rather faster than the first one, taking "only" almost 2 seconds per step (though about double that when you eat food or press a key). It also uses a different color scheme that I think I like better than the old one (which I suspect used the background it did primarily to show off how that was drawn... *eyeroll*). I suspect that on your 1kHz machine it might be too fast to easily play.


For this variant I did away with the fancy resetting, instead simply restarting the game (well, not quite completely, but nearly so) - mainly to get enough space for the rest of the code.

I also have a version that shows where the snake's head is during gameplay, but it is slower - it tipped to the other side of the 2 second mark. So I decided not to include that in the final version, instead just blinking the head after a crash to show where you ended up - but if you want to try it, I kept that version of it in a branch. Here's how that looked:


As for the code, how this was done, well, to be honest the idea it's based on sounds a bit preposterous on the face of it, but as you can see, it turned out to work fairly well for this particular use case.

The idea was to not load sections of code into memory and then run them, but instead load and execute the instructions one at a time directly from the disk.

This means that for every instruction of the main program, the runtime actually runs several instructions. (5 in my runtime, so it should take 5 times as long to run a program, right? Well, both yes and no...)

In return, this enables the main code to be written in a more sequential manner, instead of dividing it up into blocks that you switch between, which makes things easier because you don't have to consider the tight memory space constraints all the time while writing the code.

Of course, this works best for code that is primarily sequential instead of having tight loops that need to be fast, but that's exactly what I saw in Snake - I didn't have many loops, or really time-sensitive things, mostly just long sections of code to run through mostly sequentially (with some jumps).

There were a couple exceptions to this, the line drawing inner loop and the snake position update, so I put those into the memory space left over from the main runtime, as separate functions I could just call from the rest of the code.

Getting back to the speed, this runtime really does make most of the instructions take 5 times as long as running them directly. However, what we're really comparing against is the overlay-based variant, and since the main loop there took 3 overlays for a single run through, it was effectively loading each instruction from disk for every time it was run anyway - and the overlay loader needs more instructions for doing that than the one-at-a-time runtime does.

(Of course, this does strongly indicate that having enough RAM to keep all of the code loaded at the same time, with a game variant written accordingly, would still be quite a bit faster - in fact, by a factor of ~5.)

The runtime also imposes some limitations on the code, such as JMP/IFJMP not really working the way they usually do, and R1 not being persistent from one instruction to the next (which affects any use of GETDATA). So the code needs to be written for this runtime, most nontrivial pre-existing code won't work without changes.

While it would be possible to make those things work, to make the runtime more transparent, that would significantly slow it down, as it would need to look at what each instruction is and handle some of them specially, instead of just blindly executing them like it does now.


Here's the source code of the game, and of just the base disk-runner runtime (which is included in the game code). The preprocessor isn't ideal for this kind of self-loading program, but it still made things significantly easier. (And I did end up using the per-line disk address comments for debugging, so good call on that being useful.)

Here's an already-processed and comment-stripped version of the game (and runtime):

DATAC 00000000000000000000000000010111
JMP 1 19
DATAC 00000000000000000000000000000001
MATH 13 14 5
JMP 1 5
JMP 1 19
GETDATA 1 3 14
MATH 15 14 0
MOVO 1 8
NIL
MOVO 1 1
JMP 1 5
GETDATA 1 3 2
MOVO 1 19
MATH 15 2 0
MOVI 1 12
MATH 15 1 0
MOVO 1 12
IFJMP 1 11 1
JMP 1 5
MOVI 15 1
MOVI 14 22
JMP 1 5
DATAC 00000000000000000000000000011000
MATH 2 2 1
MATH 3 3 1
SET 2 3 10011010
SET 3 3 10100101
SETDATA 0 0 1000000000000000000000
JMP 1 11
MATH 12 12 1
SET 12 3 10010101
MATH 2 2 1
MATH 3 3 1
MATH 4 4 1
SETDATA 0 0 1100000000000000000000
MATH 14 13 5
SET 14 3 10001100
SET 14 3 10001100
SET 14 3 10001100
SET 14 3 10001100
SET 14 3 10001100
PMOV 15 10 0 31 6 1
MATH 9 9 1
SET 9 0 11011110
MATH 4 4 1
MATH 6 6 1
MATH 7 7 1
MATH 8 8 1
SET 4 3 10101000
SET 6 3 00000110
SET 7 3 00001110
SET 8 3 01010100
MATH 4 11 5
MATH 4 12 5
MATH 15 5 5
SETDATA 1 3 11 9
SET 13 3 00111100
SETDATA 0 0 0100000000000000000000
SETDATA 0 3 9
MATH 11 2 5
MATH 15 2 0
MATH 8 2 4
MATH 12 3 5
MATH 8 3 4
SET 13 3 01010001
IFJMP 1 2 0
MATH 14 13 5
MATH 7 2 6
MATH 6 3 6
MATH 15 2 0
MATH 15 3 0
PMOV 3 3 0 31 25 0
PMOV 2 3 28 31 28 0
GETDATA 0 3 3
MOVI 2 1
PMOV 3 3 0 31 2 1
IFJMP 1 2 1
PMOV 1 3 0 1 0 0
SETDATA 0 3 3
MATH 15 5 0
MATH 2 2 1
GETDATA 2 0 0000000000000000000000
MOVI 3 1
SET 13 3 01111000
IFJMP 1 2 1
MATH 10 9 0
PMOV 9 0 2 31 2 0
SET 13 3 01100000
MATH 5 3 5
IFJMP 1 2 1
JMP 1 23
MATH 15 12 0
MATH 8 12 4
MATH 4 12 0
SET 14 3 01100011
JMP 1 26
MATH 15 5 1
MATH 3 3 1
MATH 15 11 0
MATH 8 11 4
MATH 4 11 0
SETDATA 1 3 11 9
PMOV 0 3 0 1 2 0
SET 13 3 01010001
IFJMP 1 2 0
SET 2 3 00000010
SET 13 3 00111100
IFJMP 1 2 0
SETDATA 0 0 0000000000000000000000
SETDATA 2 0 0000000000000000000000
SET 2 3 01110010
PMOV 15 0 0 31 1 1
MATH 14 13 5
MATH 0 9 0
SETDATA 0 3 9
GETDATA 2 0 0000000000000000000000
MOVI 3 1
IFJMP 1 2 1
SET 14 3 00011110
MATH 3 0 5
GETDATA 2 0 0000000000000000000000
MOVI 3 1
IFJMP 1 2 1
MATH 0 3 5
SET 13 3 10000111
SET 2 3 01110111
IFJMP 1 2 0
SET 2 3 01100001
IFJMP 1 2 0
SET 2 3 01110011
IFJMP 1 2 0
SET 2 3 01100100
IFJMP 1 2 0
SET 14 3 01010001
SET 2 3 11111100
PMOV 0 2 29 30 1 1
GETDATA 1 3 2
MOVI 10 1
SET 14 3 01010001
GETDATA 1 3 12
MOVI 0 1
MATH 15 12 0
PMOV 0 2 0 8 0 0
PMOV 0 3 9 17 9 0
PMOV 0 4 18 26 18 0
JMP 1 19
MATH 15 13 0
MATH 13 14 5
DATAC 00000100100111011100000000100000
DATAC 01000100010000000000000100000000
DATAC 01111100110000000000000000100000
DATAC 01111011100111111111111100000000
DATAC 01000011001000000011111111100000
SETDATA 0 3 2
MATH 4 2 0
IFJMP 2 2 1
JMP 1 5
GETDATA 1 3 12
PMOV 15 1 0 1 0 0
SETDATA 0 3 1
GETDATA 0 3 0
SETDATA 0 3 9
MATH 1 0 5
JMP 1 5
NILLIST 3
NILLIST 84
DATAC 11111100000000000000000000000000
DATAC 00000000100000000000000000000000
DATAC 00000100000000000000000000000000
DATAC 11111111100000000000000000000000