Jump to content

Recommended Posts

Posted (edited)

Updates

 

  • Playfield is now 1/3 wider (24x16) and the maximum snake length is more than double at 200+ segments, all thanks to @Andrew Davie's very cleaver storage optimisation idea (I am now using just 2 bits for each segment by storing movement values instead of locations).

 

Known issues / bugs

 

  • Random number generator does not yet cover the extra screen area (it still works with a 16x16 grid).
  • The score will now wrap around if you exceed 99 🙂 

 

Hi, I'd like to share my first Atari project which is a little snakes game.  I'm sure this has been done before, but as I wrote one years ago for Windows it seemed like a good place to start.  The major challenges were finding enough memory to maintain a long (ish) snake and having to manipulate the playfield registers on every scanline whilst also drawing a couple of sprites.

 

I originally started using a linked list to maintain the snakes body (requiring two bytes per segment), but quickly started running out of RAM.  This is the most obvious algorithm for a computer, but clearly not for the 2600.  I now just use an ordered list (one byte per segment) which is reshuffled whenever the snake moves to maintain the order.  This is very inefficient, but the CPU has nothing else to do at the end of a frame so actually makes sense.  I'm starting to get into the mindset of programming this thing!

 

The playfield is limited to a 16x16 grid for obvious reasons (4-bits for X and 4-bits for Y allow any position to be represented by 1 byte).  The maximum size of the snake is around 75 segments long which makes things reasonably challenging I hope.

 

I've not added a proper "Game Over" or "You Win" sequence yet, but it will re-start if you die and stop moving if you win (by scoring 70 points).  Just push the joystick to start the game!

 

ROM file attached and also available on my github page with the source:

 

https://github.com/RobinSergeant/2600-Snakes

 

Comments and suggestions welcome.  It's been a fun learning exercise for me and I've really enjoyed doing some retro coding.

 

 

snakes.bin

Edited by CPC464Kid
Major game updates!
  • Like 5

Share this post


Link to post
Share on other sites
7 hours ago, CPC464Kid said:

The playfield is limited to a 16x16 grid for obvious reasons (4-bits for X and 4-bits for Y allow any position to be represented by 1 byte).  The maximum size of the snake is around 75 segments long which makes things reasonably challenging I hope.

 

 

In a snake game you could represent the snake by a tail position (x,y) and a head position(x,y) and the body simply as a series of "move directions". Moves are only up/down/left/right, so you only need two bits for these. Thus, 4 positions per byte.  You could probably spare about 100+ bytes for storing the body, so let's say 4 or 500 body length :). That is, if you wanted really long snakes, or perhaps a two player version with two snakes.
 

  • Like 3
  • Thanks 1

Share this post


Link to post
Share on other sites
5 hours ago, Andrew Davie said:

 

In a snake game you could represent the snake by a tail position (x,y) and a head position(x,y) and the body simply as a series of "move directions". Moves are only up/down/left/right, so you only need two bits for these. Thus, 4 positions per byte.  You could probably spare about 100+ bytes for storing the body, so let's say 4 or 500 body length :). That is, if you wanted really long snakes, or perhaps a two player version with two snakes.
 

Thanks @Andrew Davie that's a brilliant idea.  I never thought about it that way, but of course your right 🙂  I still need 1 bit of storage for every playfield square because the kernel needs to know what PF bits to set on each line (I'm currently using 32 bytes for this), but my 70 odd free bytes would still give a body length of 280.  Obviously I'll also need to make the playing area bigger but your idea gives a lot of scope for improving things.  I'll try using this technique.  A two player version would also be cool.

  • Like 1

Share this post


Link to post
Share on other sites
14 hours ago, ZeroPage Homebrew said:

Love the tiny face graphics of the snake and the small mouth animation when he eats!

 

- James

I echo James's comments regarding the face graphics and mouth animation.  Surround and Tapeworm are some of my favorite original games.  Anything resembling a Tron Light Cycle is cool in my book.  I look forward to the final version which I plan to put on my Harmony cart for some on demand Atari therapy when I need some downtime after a tough day at work or when my lawn mower won't start.

  • Like 1
  • Thanks 1

Share this post


Link to post
Share on other sites
Posted (edited)

Found a small bug in the game. There are certain directions you can press on the joystick that will result in instant death:

 

While Facing Right: Pressing Down/Up then Left IMMEDIATELY (this is more easily accomplished at the beginning before moving)

While Facing Left: Pressing Down/Up then Right IMMEDIATELY

 

- James

Edited by ZeroPage Homebrew
  • Thanks 1

Share this post


Link to post
Share on other sites
1 hour ago, ZeroPage Homebrew said:

Found a small bug in the game. There are certain directions you can press on the joystick that will result in instant death:

 

While Facing Right: Pressing Down/Up then Left IMMEDIATELY (this is more easily accomplished at the beginning before moving)

While Facing Left: Pressing Down/Up then Right IMMEDIATELY

 

- James

Thanks James, well spotted.  I knew about moving left before you start and that was just lazy programming, but the other cases I hadn't considered.  Just had a quick look and I think it's because I am checking the joystick and updating the direction every frame, but only moving every 10 frames.  So if you change direction very quickly it gets confused.  I will fix this!

  • Like 1

Share this post


Link to post
Share on other sites
5 hours ago, ZeroPage Homebrew said:

Found a small bug in the game. There are certain directions you can press on the joystick that will result in instant death:

 

While Facing Right: Pressing Down/Up then Left IMMEDIATELY (this is more easily accomplished at the beginning before moving)

While Facing Left: Pressing Down/Up then Right IMMEDIATELY

 

- James

Now fixed with replacement rom attached above!  Thanks again for spotting this James @ZeroPage Homebrew

  • Like 1

Share this post


Link to post
Share on other sites
6 hours ago, Armscar Coder said:

I echo James's comments regarding the face graphics and mouth animation.  Surround and Tapeworm are some of my favorite original games.  Anything resembling a Tron Light Cycle is cool in my book.  I look forward to the final version which I plan to put on my Harmony cart for some on demand Atari therapy when I need some downtime after a tough day at work or when my lawn mower won't start.

Thanks for the encouragement @Armscar Coder. When I get a bit further it would be useful to have somebody test on real hardware.  I've managed to resist buying a 2600 from eBay because I know it won't look great on my modern(ish) TV, and I need to stop filling my house with such things!

Share this post


Link to post
Share on other sites
Posted (edited)
12 minutes ago, CPC464Kid said:

Thanks for the encouragement @Armscar Coder. When I get a bit further it would be useful to have somebody test on real hardware.  I've managed to resist buying a 2600 from eBay because I know it won't look great on my modern(ish) TV, and I need to stop filling my house with such things!

Your wish is fulfilled, we've scheduled Snakes for this Friday on our stream! 🙂 Looking forward to playing it on ZPH!

 

- James

Edited by ZeroPage Homebrew
  • Like 2

Share this post


Link to post
Share on other sites
49 minutes ago, ZeroPage Homebrew said:

Your wish is fulfilled, we've scheduled Snakes for this Friday on our stream! 🙂 Looking forward to playing it on ZPH!

 

- James

cool, I look forward to seeing that 🙂 

  • Like 1

Share this post


Link to post
Share on other sites
20 minutes ago, CPC464Kid said:

cool, I look forward to seeing that 🙂 

Just checked it out on my Light Sixer using a Harmony Encore cart, no issues! Played up to 65 points, on point 59 the dot appeared on my tail so I had to wait until it was cleared to get it. Not sure if that's a bug or a feature but it works either way. 🙂

 

- James

Share this post


Link to post
Share on other sites
56 minutes ago, ZeroPage Homebrew said:

Just checked it out on my Light Sixer using a Harmony Encore cart, no issues! Played up to 65 points, on point 59 the dot appeared on my tail so I had to wait until it was cleared to get it. Not sure if that's a bug or a feature but it works either way. 🙂

 

- James

Cool, good to know thanks.  I think that is another bug as it's supposed to only put the dots on empty squares, but I'll look at that another day!

  • Like 1

Share this post


Link to post
Share on other sites
21 hours ago, CPC464Kid said:

Thanks @Andrew Davie that's a brilliant idea.  I never thought about it that way, but of course your right 🙂  I still need 1 bit of storage for every playfield square because the kernel needs to know what PF bits to set on each line (I'm currently using 32 bytes for this), but my 70 odd free bytes would still give a body length of 280.  Obviously I'll also need to make the playing area bigger but your idea gives a lot of scope for improving things.  I'll try using this technique.  A two player version would also be cool.

Although there are 6 bytes/line for PF - there are only 40 actually used bits.  That is, there are 8 bits "wasted" in the 6 bytes.

In particular, PF0 only uses 4 bits.  So you can save space (1 byte/line) by putting the left-side PF0 bits in the high nibble of a byte, and the right-side PF0 bits in the left nibble of the same byte. You just need to shift 4-times when doing the screen draw itself. This saves you, as noted, 1 byte per line (if you were using all 6).  I see you are probably using 4 bytes/line - this would give you full-screen width at the cost of just 1 more byte/line. Given the 2 bits/move + 5 bytes/line mods, I think you could get it full screen. Go on, I dare you :)


Good job so far, btw!

  • Like 3
  • Thanks 1

Share this post


Link to post
Share on other sites
Posted (edited)
7 hours ago, Andrew Davie said:

Although there are 6 bytes/line for PF - there are only 40 actually used bits.  That is, there are 8 bits "wasted" in the 6 bytes.

In particular, PF0 only uses 4 bits.  So you can save space (1 byte/line) by putting the left-side PF0 bits in the high nibble of a byte, and the right-side PF0 bits in the left nibble of the same byte. You just need to shift 4-times when doing the screen draw itself. This saves you, as noted, 1 byte per line (if you were using all 6).  I see you are probably using 4 bytes/line - this would give you full-screen width at the cost of just 1 more byte/line. Given the 2 bits/move + 5 bytes/line mods, I think you could get it full screen. Go on, I dare you :)


Good job so far, btw!

Thanks Andrew, that is true.  Although currently I'm actually only using two bytes per row (my rows are 8 scan lines deep), with a further 6 temporary variables used inside the kernel to keep track of what to load into the PF registers for the current and next row.  The values for the next row are built up during spare cycles in the current row from these two bytes etc.  Effectively I've got a 16x16 bit table that represents my playfield (excluding the side walls) and the actual PF data is temporary (changing every 8 scan lines).  I might need to think about storing the data in a format that fits the PF registers instead to get full screen width.  I guess it would be a trade off between RAM consumed for the playfield and RAM available for the snake to grow!

 

I'll see what I can do 🙂  Perhaps just extending it from 16 PF pixels wide to 24 PF pixels would be a good trade off.  Although making it full screen would be a good technical challenge!

Edited by CPC464Kid
typo!

Share this post


Link to post
Share on other sites
24 minutes ago, CPC464Kid said:

Thanks Andrew, that is true.  Although currently I'm actually only using two bytes per row (my rows are 8 scan lines deep), with a further 6 temporary variables used inside the kernel to keep track of what to load into the PF registers for the current and next row.  The values for the next row are built up during spare cycles in the current row from these two bytes etc.  Effectively I've got a 16x16 bit table that represents my playfield (excluding the side walls) and the actual PF data is temporary (changing every 8 scan lines).  I might need to think about storing the data in a format that fits the PF registers instead to get full screen width.  I guess it would be a trade off between RAM consumed for the playfield and RAM available for the snake to grow!

 

I'll see what I can do 🙂  Perhaps just extending it from 16 PF pixels wide to 24 PF pixels would be a good trade off.  Although making it full screen would be a good technical challenge!

Technically you don't need to store the snake at all, because it's effectively stored in the playfield. IF you have a playfield. You can keep a track of the snake tail "next position" by looking for the adjacent "on" pixel (i.e., up/down/left/right) in the screen "bitmap".  That is, I'm suggesting a 5-byte (for the PF0/PF1/PF2/PF0/PF1/PF2 as discussed above) x n lines RAM usage.  So, assuming 24 lines you would need 120 bytes :). 16 only requires 80 bytes.  Process would be to treat those bytes as a "bitmapped" display (even though the pixel positions are weird-as) and thus the snake head pointer writes a pixel in, and the tail pointer erases itself and "finds" the new tail position by looking for the adjacent set-pixel.

Just throwing alternate ideas at you, as this is what '2600 programming is all about.

  • Like 1
  • Thanks 1

Share this post


Link to post
Share on other sites
15 minutes ago, Andrew Davie said:

Technically you don't need to store the snake at all, because it's effectively stored in the playfield. IF you have a playfield. You can keep a track of the snake tail "next position" by looking for the adjacent "on" pixel (i.e., up/down/left/right) in the screen "bitmap".  That is, I'm suggesting a 5-byte (for the PF0/PF1/PF2/PF0/PF1/PF2 as discussed above) x n lines RAM usage.  So, assuming 24 lines you would need 120 bytes :). 16 only requires 80 bytes.  Process would be to treat those bytes as a "bitmapped" display (even though the pixel positions are weird-as) and thus the snake head pointer writes a pixel in, and the tail pointer erases itself and "finds" the new tail position by looking for the adjacent set-pixel.

Just throwing alternate ideas at you, as this is what '2600 programming is all about.

Interesting idea, but I still think I will need to store the "move directions" (2 bits per segment) that you suggested before.  I don't think it would be possible to find the adjacent set-pixel without storing all the movements.  Or at least I don't think my brain could cope with trying to find the correct one!  That's what I meant with the trade off as I'm now planning to store movements rather than the snake itself, but if the PF gets too big I won't be able to store enough movements.

 

I will think about this properly when I've got more free time and start refactoring the code.  Thanks for all the ideas, you definitely need to think outside the box with the 2600.  I'm more used to C and C++ applications where nobody cares about memory anymore!

Share this post


Link to post
Share on other sites
2 minutes ago, CPC464Kid said:

Interesting idea, but I still think I will need to store the "move directions" (2 bits per segment) that you suggested before.  I don't think it would be possible to find the adjacent set-pixel without storing all the movements.  Or at least I don't think my brain could cope with trying to find the correct one!  That's what I meant with the trade off as I'm now planning to store movements rather than the snake itself, but if the PF gets too big I won't be able to store enough movements.

 

I will think about this properly when I've got more free time and start refactoring the code.  Thanks for all the ideas, you definitely need to think outside the box with the 2600.  I'm more used to C and C++ applications where nobody cares about memory anymore!

 

Muddy thinking on my part. I was thinking there was only one of the UDLR squares from the tail position occupied, but this is not the case.

So, carry on - nothing to see here.

 

 

  • Haha 1

Share this post


Link to post
Share on other sites

Perhaps there's a better way.

MOST of the movements will be exactly the same direction as a previous movement, right?

In other words, changes of direction are (relatively) infrequent. Perhaps an encoding scheme taking account of this would work.

For example, assume "0" means "no change".  Then for most of the movements you need a single bit.

Then you could have "1" mean "there's a new direction" and use the next two bits to indicate which direction it is.

 

Or, another alternative...

only store the (x,y) at each direction change. Nothing inbetween.

You can calculate which direction to move by looking at which of (x) or (y) are unchanged from the previous position.

 

 

 

 

 

 

  • Thanks 1

Share this post


Link to post
Share on other sites
12 minutes ago, Andrew Davie said:

Perhaps there's a better way.

MOST of the movements will be exactly the same direction as a previous movement, right?

In other words, changes of direction are (relatively) infrequent. Perhaps an encoding scheme taking account of this would work.

For example, assume "0" means "no change".  Then for most of the movements you need a single bit.

Then you could have "1" mean "there's a new direction" and use the next two bits to indicate which direction it is.

 

Or, another alternative...

only store the (x,y) at each direction change. Nothing inbetween.

You can calculate which direction to move by looking at which of (x) or (y) are unchanged from the previous position.

 

 

 

 

 

 

True, most of time you won't be making many turns.  I guess the issue is also being able to support the extreme cases where somebody keeps twisting and turning.  The edge cases are always the problem 🙂

I like the ideas though. I think I'll start by converting what I've got to use movement values and see how that goes.  I'm finding that small incremental changes are the way to got with 6502 because otherwise it's hard to figure out where you've broken something!!!

Share this post


Link to post
Share on other sites
Posted (edited)

We'll be checking out Snakes on tomorrow's (Friday July 3) ZeroPage Homebrew stream LIVE on Twitch at 6PM PT | 9PM ET | 1AM GMT! Hope everyone can watch!

 

Games:

 

  (SET VIDEO TO 1080P60 FOR FULL QUALITY)

 

 

 

Edited by ZeroPage Homebrew
  • Like 2

Share this post


Link to post
Share on other sites

Just an update on more efficient encoding...
Use "0" bit to indicate no change.

Then, "1" bit indicates a change - there are actually only two possibilities, as you can't go backwards, and you're already ruled out forwards.
So, you only need another bit 0 or 1 to indicate direction left/right.
That would be pretty efficient.

But you could do even better, I think.

Use "0" bit to indicate no change, and follow that by (say) 4 bits indicating a counter.  So, "no change for 10 moves" for example.

In that case, 5 bits for 10 moves total, compared to 10 bits for the earlier example.
In games, there are fairly long stretches of "no change"; it's uncommon for a human to move regularly/quickly. Humans tend to focus on a target and just let it go. Your mileage may vary.

But in any case, this would probably... let's say... double the length you could store compared to the two-bit per move method.

 

  • Thanks 1

Share this post


Link to post
Share on other sites
On 7/5/2020 at 5:19 PM, Andrew Davie said:

Just an update on more efficient encoding...
Use "0" bit to indicate no change.

Then, "1" bit indicates a change - there are actually only two possibilities, as you can't go backwards, and you're already ruled out forwards.
So, you only need another bit 0 or 1 to indicate direction left/right.
That would be pretty efficient.

But you could do even better, I think.

Use "0" bit to indicate no change, and follow that by (say) 4 bits indicating a counter.  So, "no change for 10 moves" for example.

In that case, 5 bits for 10 moves total, compared to 10 bits for the earlier example.
In games, there are fairly long stretches of "no change"; it's uncommon for a human to move regularly/quickly. Humans tend to focus on a target and just let it go. Your mileage may vary.

But in any case, this would probably... let's say... double the length you could store compared to the two-bit per move method.

 

Makes sense, thanks Andrew.  I'd just need to be careful to keep the maximum length sensible to allow for more turns after the snake grows, and I guess the player will also make more turns when the snake is longer as there would then be less free screen space.  Otherwise I could run out of memory when the player tries to change direction!

Share this post


Link to post
Share on other sites

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.

Guest
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

Loading...

  • Recently Browsing   0 members

    No registered users viewing this page.

×
×
  • Create New...