Making of.

  • First, I've made a canvas, id #snake.
  • Now, let's add a script and get a hold of the canvas.
  • Fourth, I'm moving the rest of the making of bellow the canvas so I can always see the game.
  • Note for townies: mail me in town if you have feedback and I don't respond to IRC

WASD or arrow keys to move the snake.

Score: 0

Making of, continued.

  • So I've added a function called Snake that's going to be the bulk of the snake game.Outside of it, I've set the DOT_SIZE and WIDTH and HEIGHT of canvas, as absolutes (so I can easily play with it later.

    Within the constructor, I've set width and height in "dot" units (e.g. this.width = WIDTH / DOT_SIZE) and set locals (this.x and this.y) to half the width and height.

    Now I'll try to grab the canvas and draw a "dot" at [x, y].

  • Ok, dot drawn. Basically ctx = document.querySelector('#snake').getContext('2d');, ctx.beginPath(); ctx.arc(...); ctx.fill();

  • Now I'll try to grab keyboard events.

    Let's see: I've made a basic KeyboardListener prototype that pretends it's an EventEmitter and listens to arrows and WASD. Let's add it to the snake.

  • No, wait, I've added a repeat interval thingy to the KeyboardListener. Now if you press and hold a key, it'll wait for half a sec, then trigger the "move" event again and repeat every 200 milliseconds.

  • I've added a startTicksmethod to Snake. It will run a setIntervaevery second (actually every MOVE_INTERVAL millis) and try to move our dot in that direction. The moving part is done by a move method. Oh well.

    The same gor a GROW_INTERVAL. Now, the cool thing, is that it works - I can now move the snake, it'll get redrawn. I am still not clearing screens on ticks, I wanna get the sizing thing figured out first.

    The only problem is I've replaced up-down with left-right so let's remap thise this.x, this.y bits.

    Oh, I'm speeding it up for development purposes.

  • Oh, nice:

  • But I'm done for the night.
  • Except that the towners said nice things about this, so now I can't quit!
  • Now I need to make the snake not just leave the black trail, but rather grow in size. I'll try +1 for every tick. How to do that? I have two ideas. And it appears the townies suggest the easier one. So, the easy idea is to just keep a list of the tail coordinates in an array. As in, head would be [x, y] and the tail is [ [x, y - 1], [x, y - 2], [x + 1, y - 2] ] etc etc. It could grow a bit, but shouldn't be too hard. And it's going to be easier to get the matches (as in, did we crash into something).

    The other idea I've had is to keep tail as a bunch of pointers: ['down', 'left', 'left', 'up'...] but that would be a nightmare to calculate redraws on changing directions. selfsame says he went with the second version here.
    Kudos!

    So let's build the easy version.

  • So how it's supposed to work? E.g.

    • Head is at X, Y, say, [5, 7]. Going up.
    • Tail has size 3.E.g. [[5, 6], [5, 5], [4, 5]
    • We move head one more up. [5, 8].
    • We move the first tail-part to where the head was.
    • We move the next part to where the one before was.
    • Go on until we get them all moved.
    • If size of the snake is greater than current (we grew on last tick), add one more part to where the last tail part was.

    So, something like:

    
                tail.unshift(head);
                  if (tail.length > size - 1) {
                    tail.pop();
                  }
                
  • Woot! Woot!
    Ok, some glitches, but still!

    So, next: collision detection (so we don't drive over ourselves and remove that glitch when wall-crossing.

  • But before that, I've added freeze and unfreeze methods and bound to a button-click so we can stop and inspect the state when needed.
  • Thought I'd add that I'm trying to add all the properties of the Snake class in the Snake constructor. It probably doesn't matter,it's not like I'll have a huge number of these all at once anyway.
  • Now, about that collision detection. I need to be able to detect, on the "move" call, if the tile I'm stepping into occupied or not. Now, off the top of my head, I can think of two ways. One is to check if [x, y] is already within the snake. If yes, bummer, if no, all ok. The thing is, I might be adding other objects on the game board later.

    So the other idea is that the game board itself holds a list of the objects on it, and it exposes an API to check if a tile is ocupied or not.

    But since the "game board" right now is a canvas and it doesn't even have a state or anything, I'll just go with the first idea.

  • Wow! That was unexpectedly long. First, collision detection was easy. We now have a snake.isOccupied(x, y) which returns true or false. So our move() call will first check if the new position is occupied before moving the snake. If yes, we call a this.collide(); and bail.

    Now that collide function was interesting. I'm painting a red dot over the intended (occupied) spot, with the same this.dot but it doesn't work! So I find out you need to context.closePath() if you're painting with arc().

    But still no luck! Even when I manually draw another, red dot, wherever, it's still all gray. Only when I, after the game was over, manually grabbed that context and snake and drew a dot (snake.dot(x, y, color) did I get a dot. Why? Then I realized - in my ticker, I call this.move then this.redraw. So I was overwriting my shiny new red dot!

    Anyway, now we have collision detection.

    Next up, score!

  • (a while later...)

    So, score!

    First, some usability fixes - add a "reset/restart game" button. I think we can just forget the current snake and snake = new Snake(); . That should cleanly reset everything.

    Next, score. Every time a snake moves would be a bit too much. But maybe an ultra-silent, fast ticker, that gives one point every 10 or so milliseconds? Just to see the counter growing? Let's try. Also, every time to grow a tail part, add X to score. Now, how much? Let's say just surviving for a second would give you, I don't know, 5 points? And a tail part growing gives you 5 more?

    Those should be simple, one is a global (snake-global) interval, activated on first time move, and frozen when the game is frozen. The other is triggered on the "grow" interval.

    Also, display and refresh? Let's just add a simple div and put the number there and update on every update.

  • Ok, well. snake = new Snake(); works perfectly. Now I don't have to reload to restart the game.

    Displaying score every 10 miliseconds is not a problem. But it feels like the score is growing way too fast. So maybe only have 1 score per second. And 5 per tail growth.

  • So the new numbers work way better. We have a bug, we don't resume on unfreeze, but otherwise scoring is much better now. I want to abstract the UI display away though, so that that happens maybe with a redraw?

  • Also, I've noticed that every time there's a formula or something, first I write the number (e.g. this.score = this.score + 5 * some_calculation). But right after that, I delete it and put it a CAPS constant, e.g. SCORE_PER_SECOND in this case. Not sure why is that. I have a fear of magic numbers, I guess.
  • Bug fixed, it was a stray this.paused = true; .