Tutorial, Part 5: Code the Game


< Previous Tutorial (Part 4)


With the basics setup and the ability to see the game is looking fine, we can work on the core elements of making this game work.

For this tutorial, we will be working exclusively with the scripts.js page.

First, update the ACTION() near the bottom to include the pTimer variable:

function ACTION(){
    //
    // -- SKIP SOME CODE
    //
    TEXT(0,0,"LIVES:"+pLives);                          //Top line details
    TEXT(0,8,"ROUND:"+(pRound+1));
    let p=pScore.toString(); while(p.length<5){p="0"+p;}
    TEXT(60,0,"SCORE:"+p);
    TEXT(60,8,"SNACKS:"+pSnack);
    ENDGFX();                                           //Draw the graphics.
>   //Misc.
>   pTimer=(pTimer+1)%20;                               //Timer for when the player can moved is advanced a frame.
    requestAnimationFrame(ACTION);
}

And add the following underneath the ACTION() function.

//
// -- Pause Game
function PAUSEGAME(){
}
//
// -- Crash
function CRASH(){
}
//
// -- Continue to the next round
function CONTINUE(){
}
//
// -- Check Score
function CHECKSCORE(a){
}

Player Movement

We are going to setup a system every 1/3 of a second (20 frames) the worm will move a tile over in a certain direction.

How this will work is:

  • Initially, the worm does not move at all, until a directional key is pressed. This happens because we set both pDir (Player's direction) and tempDir (Temporary direction) to values beyond the limits of the arrow keys (0-3). So long as these values are at 4, the worm does not move.
  • An active check looks to see if an arrow key is pressed. Once that happens, it will go through the process of checking the direction in question.
  • Since we do not want the player to accidentally reverse course and bump into the body of the worm, it will see if the pDir matches the opposite direction you want to go (For instance, 0 is right and 2 is left. The player's current direction is 2, so if you try to press 0, it will do nothing). Once the check is passed, tempDir will be set to that value.
  • After 20 frames, pDir will become the value of tempDir and the player X and Y value will shift, depending on their direction (it calls on dirX and dirY to shift 1 tile over).
  • We will modify the key pressing code, first, at the top of ACTION():
function ACTION(){
    let x, y, bL;  //Useful in-function variables: X&Y positions | body Length
    //Keyboard controls
>   if(KEYBOARD(0)){if(pDir!=2){tempDir=0;}}      //Left
>   else if(KEYBOARD(1)){if(pDir!=3){tempDir=1;}} //Up
>   else if(KEYBOARD(2)){if(pDir!=0){tempDir=2;}} //Right
>   else if(KEYBOARD(3)){if(pDir!=1){tempDir=3;}} //Down
    if(KEYBOARD(4)){}    //Pause Game?

Underneath this, we will start on the game action code and sneak in a line to update pTimer, since it is the variable that is tracking when 20 frames will occur. Place this right under the keyboard controls (the code above):

    //Player action
    if(pTimer==0 && tempDir!=4){                        //The player can move around and is given a direction to move.
           pDir=COPY(tempDir);
           pBody[0][0]+=dirX[pDir];
           pBody[0][1]+=dirY[pDir];
    }

Worm Size

We have a worm that can move, but it is not much of a worm, more like a random orange block that does not do a lot, so let's give it a body.

We will make use of some variables. The variable pBody (Player's body) in an array setup to display the X and Y of all the segments. It seems like it is setup excessively for a single value, but it was never intended to hold more than just one set of coordinates. It will hold as many X and Y bundles as we want it to.

  • The game treats the first value in the array as the head and the last one as the tail.
  • The game starts us off with 1 segment in pBody. As the player starts to move around, it adds more segments. They are added to the beginning of the array, so the head has a new position as the worm moves. When the array capacity reaches the value of pSegMax (Initially, this is 5), the behaviour of the game changes; it will continue to add segments, but each time it adds one beyond the capacity, it will kick a value off the end of the array, giving the appearance of movement on the board.
  • When the player picks up a snack, it increases the pSegMax value, allowing the worm to grow in size some more.

Replace the //Player action subsection with the following:

   //Player action
    if(pTimer==0 && tempDir!=4){                        //The player can move around and is given a direction to move.
           pDir=COPY(tempDir), x=pBody[0][0], y=pBody[0][1];
           pBody.unshift([x+dirX[pDir],y+dirY[pDir]]); //Add a body segment to the start of the worm. notably the next tile the player is supposed to end up on.
           if(pBody.length>pSegMax){pBody.pop();}      //If the worm size exceeds the limit, remove the segment at the end.
    }

Collision Detection

Our worm can move, but it has too much free reign, allowing it to move out of bounds, into the text box, etc. We need to fix that. To do that, we will run two separate checks and end the free reign of the worm if it collides with something.
First, update the variable list at the top of ACTION() to include the variable kill to the in-function list:

    let x, y, bL, kill=0;  //Useful in-function variables: X&Y positions | body Length | Player crashed?

Second part adds the collision checkers. It pulls the X & Y of the worm's head, runs that through the map tiles and checks to see if it returns a 1 (wall value). If so, player has hit a wall and is KIA. Next it will check the whole length of the player body and see if the head's X and Y matches the X and Y of the body. If there is a match, the player has hit a body segment and is KIA.

   //Player action
    if(pTimer==0 && tempDir!=4){                        //The player can move around and is given a direction to move.
           pDir=COPY(tempDir), x=pBody[0][0], y=pBody[0][1];
           pBody.unshift([x+dirX[pDir],y+dirY[pDir]]); //Add a body segment to the start of the worm. notably the next tile the player is supposed to end up on.
           if(pBody.length>pSegMax){pBody.pop();}      //If the worm size exceeds the limit, remove the segment at the end.
>          x=pBody[0][0], y=pBody[0][1];               //Current Position
>          if(mapX[y*16+x]==1){                        //Collision with wall. If yes, player gets killed.
>              kill=1;
>          }
>          bL=pBody.length;                            //Collision with body segment. If yes, player gets killed
>          for(let i=1; i<bL; i++){
>              if(pBody[i][0]==pBody[0][0] && pBody[i][1]==pBody[0][1]){kill=1;}
>          }
    }

Third part removes the requestAnimationFrame(ACTION) at the bottom and moves it into an IF/ELSE check. It will first check to see if the player died. If so, ACTION() stops running and the game will move over to the CRASH() function (which is currently empty, so the game will stop functioning).

At the bottom of the function ACTION(), replace this:

requestAnimationFrame(ACTION); 

With this:

    if(kill==1){                                        //Did the player crash?
        gTimer=0; requestAnimationFrame(CRASH);
    } else {                                            //Pretty much everything else.
        requestAnimationFrame(ACTION);
    }

Collecting a Snack

We can grow the worm, move around the map and get ourselved killed, but without an objective, it is hardly much of a game, so let's have it collect some snacks, now.

We will perform a collision check with the X & Y of a snack and if the head's value match we will change an assortment of values to: Add to the player's score, remove 1 from the snack counter, add 1 to the number of snacks collected and increase the maximum size of the worm. We will also throw in a check for pSnack and if it has finally reached 0 (No more snacks), we will open the walls and create an escape for the player to win the round.

What you are looking for is a code line in the subsection <b>//action</b>:

        for(let i=1; i<bL; i++){
            if(pBody[i][0]==pBody[0][0] && pBody[i][1]==pBody[0][1]){kill=1;}
        }

And we add the following right underneath it:  

        if(pBody[0][0]==snack[0] && pBody[0][1]==snack[1]){//DINNER!
            CHECKSCORE(5+pRound+snack[2]);                 //Add some points.
            pSnack--;                                      //Deduct one from the snack counter.
            pCollect++;                                    //Add one to the # of snacks munched.
            pSegMax+=addLength[Math.min(pCollect,10)];     //Lengthen the size of the worm.
            if(pSnack<=0){                                 //Open a patch to exit the map?
                snack=[-1,-1,0];                           //Place the snack outside the limits of the screen.
                let k=[7,8,96,111,112,127,215,216];        //which tiles to update
                for(let x=0; x<8; x++){
                    mapX[k[x]]=0;
                }
            } else {SNACKSPOT();}                          //Setup new snack
        }

This script calls for a function that is still empty, so let us fill that in.

CHECKSCORE() does three things: It adds points to pScore (The player's score), caps the maximum score at 99999 (Since there are only 5 digits on the screen) and compares the player's score to the pLifeScore (Life Score) value. If we have a match (or it's greater), we add +1 to pLife (Player's Life counter) and bump up the number of points required for the next bonus life.

Go to where CHECKSCORE() is and fill in the following:

//
// -- Check Score
function CHECKSCORE(a){
    pScore=Math.min(pScore+a,99999);   //Adds to the score and keeps it capped at 99999.
    if(pScore>=pLifeScore){            //check if score exceeds the score for giving a bonus life.
        pLifeScore+=pLifeBase;         //Update the score that get compared.
        pLifeBase+=pLifeInc;           //Update the base that is added to the score.
        pLives=Math.min(pLives+1,9);   //Give the player an extra life.  Cap it at 9.
    }
}

With a refresh to the game code, everything should be coming along nicely.

Winning a Round

After collecting all the snacks, the exits appear, but without a proper ending, the player cannot win a round, so let's fix that.

The first part of this is a bit of an overhaul to the action script, since we now have to factor for when a player has won a round. We are looking to see if the player exceeded the limits of the map's X and Y values (The X range would be 0-15 and Y would be 0-13). With the map open, the player can now exceed those values and it will toggle endRound=1 (which tells the game to make the worm exit the screen), otherwise the game carries on.

  • Do not miss the closing bracket at the end, below
        x=pBody[0][0], y=pBody[0][1];               //Current Position
>       if(x==-1 || y==-1 || x==16 || y==14){       //Did the player exceed the boundaries of the map?
>           endRound=1;                             //Round over.  Player has completed it.
>       } else {
            if(mapX[y*16+x]==1){                    //Collision with wall. If yes, player gets killed.
                kill=1;
            }
            bL=pBody.length;                        //Collision with body segment. If yes, player gets killed
            for(let i=1; i<bL; i++){
                if(pBody[i][0]==pBody[0][0] && pBody[i][1]==pBody[0][1]){kill=1;}
            }
            if(pBody[0][0]==snack[0] && pBody[0][1]==snack[1]){//DINNER!
                CHECKSCORE(5+pRound+snack[2]);                 //Add some points.
                pSnack--;                                      //Deduct one from the snack counter.
                pCollect++;                                    //Add one to the # of snacks munched.
                pSegMax+=addLength[Math.min(pCollect,10)];     //Lengthen the size of the worm.
                if(pSnack<=0){                                 //Open a patch to exit the map?
                    snack=[-1,-1,0];                           //Place the snack outside the limits of the screen.
                    let k=[7,8,96,111,112,127,215,216];        //which tiles to update
                    for(let x=0; x<8; x++){
                        mapX[k[x]]=0;
                    }
                } else {SNACKSPOT();}                          //Setup new snack
            }
>       }

Next, we will include what happens when endRound=1, which will ignore regular gameplay and do it's own thing, instead.

    if(pTimer==0 && tempDir!=4){                        //The player can move around and is given a direction to move.
>        if(endRound==0){                                //This operates when the worm is not leaving the map.
            pDir=COPY(tempDir), x=pBody[0][0], y=pBody[0][1];
            pBody.unshift([x+dirX[pDir],y+dirY[pDir]]); //Add a body segment to the start of the worm. notably the next tile the player is supposed to end up on.
            if(pBody.length>pSegMax){pBody.pop();}      //If the worm size exceeds the limit, remove the segment at the end.
            x=pBody[0][0], y=pBody[0][1];               //Current Position
            if(x==-1 || y==-1 || x==16 || y==14){       //Did the player exceed the boundaries of the map?
                endRound=1;                             //Round over.  Player has completed it.
            } else {
                if(mapX[y*16+x]==1){                    //Collision with wall. If yes, player gets killed.
                    kill=1;
                }
                bL=pBody.length;                        //Collision with body segment. If yes, player gets killed
                for(let i=1; i<bL; i++){
                    if(pBody[i][0]==pBody[0][0] && pBody[i][1]==pBody[0][1]){kill=1;}
                }
                if(pBody[0][0]==snack[0] && pBody[0][1]==snack[1]){//DINNER!
                    CHECKSCORE(5+pRound+snack[2]);                 //Add some points.
                    pSnack--;                                      //Deduct one from the snack counter.
                    pCollect++;                                    //Add one to the # of snacks munched.
                    pSegMax+=addLength[Math.min(pCollect,10)];     //Lengthen the size of the worm.
                    if(pSnack<=0){                                 //Open a patch to exit the map?
                        snack=[-1,-1,0];                           //Place the snack outside the limits of the screen.
                        let k=[7,8,96,111,112,127,215,216];        //which tiles to update
                        for(let x=0; x<8; x++){
                            mapX[k[x]]=0;
                        }
                    } else {SNACKSPOT();}                          //Setup new snack
                }
            }
>        } else { //Automatically remove body segments to show the worm exiting the room.
>          pBody.pop();
>        }
    }

Near the end of ACTION(), we will make a small change to increase the speed of the worm when exiting a map. This will also serve as an indicator that the end of round script is working.

Near the bottom of ACTION(), replace

 pTimer=(pTimer+1)%20; 

With

    pTimer=(pTimer+1)%(20/(endRound+1));                //Timer for when the player can moved is advanced a frame.

The exit should now be working

Pause the Game

We can play a full round, but we are still some missing some odds and ends to create cohesion within the game. Right now the entire game ends after playing a round or the player crashes into something. Let's do something about that.

Go to the top of the ACTION() and update the variable line to include another value, pause:

    let x, y, bL, kill=0, pause=0; //Useful in-function variables: X&Y positions | body Length | Player crashed? | Game Paused

Next go down to where KEYBOARD(4) is located. We have had something setup for Spacebar this whole time, but it has yet to do anything useful. We are using KEYBOARDHOLD() which prevents any further use of the spacebar until it has been let go of. This prevents the game from unintentionally resuming because the player did not let go of the spacebar.

    //Keyboard controls
    if(KEYBOARD(0)){if(pDir!=2){tempDir=0;}}      //Left
    else if(KEYBOARD(1)){if(pDir!=3){tempDir=1;}} //Up
    else if(KEYBOARD(2)){if(pDir!=0){tempDir=2;}} //Right
    else if(KEYBOARD(3)){if(pDir!=1){tempDir=3;}} //Down
>   if(KEYBOARD(4)){KEYBOARDHOLD(4); pause=1;}    //Pause Game?

Now, scroll down to the end of ACTION() and add a check for when the spacebar has been pressed and the pause variable has been set to 1.

    if(kill==1){                                        //Did the player crash?
        gTimer=0; requestAnimationFrame(CRASH);
>   } else if(pause==1){                                //Game paused?
>       requestAnimationFrame(PAUSEGAME);
    } else {                                            //Pretty much everything else.
        requestAnimationFrame(ACTION);
    }

We also need to add some code to the GAMEPAUSE() function. It is fairly straightforward: The function draws a box overtop the existing screen (using the same basic setup that is found in ACTION(), adds "GAME PAUSED" text and waits for the player to press Spacebar to resume the game.

//
// Pause Game
function PAUSEGAME(){
    let endPause=0;
    GAMEX.fillStyle=`rgb(48,48,48)`;              //Prep the box with 'game paused' text.
    GAMEX.fillRect(22,50,84,28);
    GAMEX.fillStyle=`rgb(255,255,255)`;
    GAMEX.fillRect(24,52,80,24);
    STARTGFX();
    //
    let p="GAME PAUSED";
    TEXT(64-(p.length*3),61,p);
    ENDGFX();                                     //Draw the graphics.
    if(KEYBOARD(4)){KEYBOARDHOLD(4); endPause=1;} //Hit spacebar to resume game.
    if(endPause==1){
        requestAnimationFrame(ACTION);
    } else {
        requestAnimationFrame(PAUSEGAME);
    }
}

If all works well, you should have a "GAME PAUSED" block that appears when you hit Spacebar. To resume your game, hit Spacebar again.

Game Over

Crashing into a wall produces an immediate game over where nothing else happens (since the game is directing to an empty function). Let's start filling that function in.

The function operates on a timer (gTimer). This increments by 1 every time it is run and when it reaches 90 it will determine what happens next:

  • pLives (Player's lives) is 1 or higher: The game continues on (Back to ACTION()).
  • pLives is 0: The game ends and will back to STARTGAME() to start all over.
  • gTimer also has an exception for when the value is 0 (since this should only be run once). This will draw the box with the appropriate text on top and update all the important values to reset the player (and the snack, if needed).

In summary, the CRASH() function is fairly straightforward:

  • At 0: Draw a "CRASH" or "GAME OVER" box and update some stuff.
  • At 90: Check if the player is out of the lives and reset or carry on with the game, minus 1 life.
//
// Crash
function CRASH(){
    if(gTimer==0){                            //1st frame updates the 'crash' sequence.
        GAMEX.fillStyle=`rgb(48,48,48)`;      //Prep the box with 'crash' or 'game over' text.
        GAMEX.fillRect(22,50,84,28);
        GAMEX.fillStyle=`rgb(255,255,255)`;
        GAMEX.fillRect(24,52,80,24);
        STARTGFX();
        if(pLives>=0){                        //Lives remaining
            TEXT(49,61,"CRASH");
        } else {                              //Out of lives
            TEXT(37,61,"GAME OVER");
        }
        ENDGFX();                             //Draw the graphics.
        //
        pBody=[[7,8]],                        //update some game variables.
        pDeaths++,
        pDir=4,
        pLives--,
        pTimer=0,
        tempDir=4;
        if(pSnack>0){SNACKSPOT();}            //Only run this if the exits are not visible.
    }
    gTimer++;
    if(gTimer==90){                           //end of the delay
        gTimer=0;
        if(pLives==0){
            requestAnimationFrame(STARTGAME); //Out of lives.  Start over.
        } else {
            requestAnimationFrame(ACTION);    //Back to the game.
        }
    } else {
        requestAnimationFrame(CRASH);
    }
}

And if all works well, you should have a CRASH box appear when you hit a wall or part of the worm's body.

Continue the Game

While we can complete a round, we cannot advance to the next one, yet. We have a function to fill in and couple other updates to make.

Similar to what happens in CRASH(), a lot of what goes on here is updating variables (namely reseting values, such at the length of the worm, how many snacks to collect, etc.), drawing the "NEXT ROUND" stuff and carrying on. We also take of some additional content, such as updating the current map, checking to see if the player played a flawless round (and awarding some bonus points along the way), etc.

First, we make a small adjustment to the ACTION() function (near the bottom):

    if(kill==1){                                        //Did the player crash?
        gTimer=0; requestAnimationFrame(CRASH);
>   } else if(endRound==1 && pBody.length==0){          //Auto-end the round.
>       requestAnimationFrame(CONTINUE);
    } else if(pause==1){                                //Game paused?
        requestAnimationFrame(PAUSEGAME);
    } else {                                            //Pretty much everything else.
        requestAnimationFrame(ACTION);
    }

Then we fill in the CONTINUE() function.

//
// Continue onto the next round
function CONTINUE(){
    if(gTimer==0){                                 //1st frame updates the 'crash' sequence.
        GAMEX.fillStyle=`rgb(48,48,48)`;           //Prep the box with 'round #X'.
        GAMEX.fillRect(0,16,128,112);
        GAMEX.fillStyle=`rgb(255,255,255)`;
        GAMEX.fillRect(2,18,124,108);
        STARTGFX();
        let p="ROUND "+(pRound+2);
        TEXT(64-(p.length*3),61,p);                //Lives remaining
        ENDGFX();                                  //Draw the graphics.
        //
        if(pDeaths==0){CHECKSCORE((pRound*3)+25)}  //No deaths = free bonus points.
        pBody=[[7,8]],                             //Update some variables.
        pCollect=0,
        pDeaths=0,
        pDir=4,
        pRound++,
        pSegMax=5,
        pSnack=Math.min(5+Math.floor(pRound/5),10), //Maximum snacks: 10 
        pTimer=0,
        //
        endRound=0,
        mapX=COPY(mapSrc[pRound%mapSrc.length]),
        tempDir=4;
        SNACKSPOT();
    }
    gTimer++;
    if(gTimer==90){
        gTimer=0;
        requestAnimationFrame(ACTION);             //Start the next round.
    } else {
        requestAnimationFrame(CONTINUE);
    }
}

And once you complete the ability to go to the next round, this is how things should look.


Now, we will have a look at the completed scripts.js page.

//FIRST THINGS, FIRST
"use strict";  //Help better identify problems in the code, especially with bad variable declarations.
//
//VARIABLES
let keyPress=[false,false,false,false,false],keyHold=[0,0,0,0,0]; //controls for keys, notably if one if being pressed/held down
//
let pBody,     //Player's body. This will be an array to store the X and Y positions of each body segment.
    pCollect,  //How many snacks the player had nomnom'ed during this round.
    pDeaths,   //Player deaths. We will add a score bonus if the player completes a level without dying once.
    pDir,      //Player's direction. This will match the keys layout (0=left, 1=up, 2=right, 3=down.
               //  4 is used as a placeholder to say there is no direction and the player will not automatically move)
    pLifeScore,//How many points a player must earn to gain another life.
    pLifeBase, //Base for score required for another life.
    pLifeInc,  //The bonus life score increment.
    pLives,    //Player's remaining lives. If this is at zero when the player crashes, it's game over.
    pSegMax,   //How many body segments the player should have.
    pRound,    //Player's current round.
    pScore,    //Player's score
    pSnack,    //How many snacks the player must collect to clear a round.
    pTimer;    //Player timer. This is used to track the # of remaining frames
let endRound,  //When the player exits the screen, this ends the round.
    gfx,       //The value that is used for building the graphics output.
    gTimer,    //Game timer. Useful for different things.
    mapX,      //Current map.
    snack,     //Snack. This holds the the X, Y and color values.
    tempDir;   //Temporary direction. This value is moved to pDir when the player moves a square.
//
//CONSTANTS
const addLength=[0,8,7,7,6,6,6,5,5,5,5],   //A setup for how many body segments are added on when eating a snack.
                                           //Connected to the pCollect variable.
      dirX=[-1,0,1,0],dirY=[0,-1,0,1],     //Controls the changes to the player's X and Y when moving.
      keys=[37,38,39,40,32],               //Keycode values while pressing keys. For WASD, use: keys=[37,38,39,40,32];
      GAME=document.getElementById("game"),//Shortcut for accessing the game canvas.
      GAMEX=GAME.getContext("2d"),         //Allows us to handle the game canvas more easily when making graphics.
      scrWidth=128, scrHeight=128,         //Screen width and height.
      snackColors=[[32,80,168],[168,32,168],[40,176,208],[208,200,32],[192,56,24]]; //Colour setup for snacks (in RGB).
//
//LISTENERS
addEventListener("resize",function(){SETSIZE();});  //Auto adjusts the game canvas when the browser window is resized.
addEventListener( //This records when certain keys are pressed down.
    'keydown',function(e){
        if(e.keyCode==32||e.keyCode==37||e.keyCode==38||e.keyCode==39||e.keyCode==40){e.preventDefault()};//This prevents the spacebar and arrow keys from doing their usual thing.
        keyPress[e.keyCode]=true
    }, true
);
addEventListener( //This records when certain keys are released.
    'keyup',function(e){
        keyPress[e.keyCode]=false;
        let i0=keys.length; for(let i=0; i<i0; i++){if(e.keyCode==keys[i]){keyHold[i]=0;}} //If a key is let go, this will free the 'it's held down' value.
    }, true
);
//
//FUNCTIONS
function SETSIZE(){ //Set the size of the screen.
    let i,i2;                                           //Set 'css' size (of the canvas).
    i=(innerHeight-4)/scrWidth,i2=innerWidth/scrHeight; //This will use get the dimensions of the screen as a multiplier of the game screen base size.
    if(i2<i){i=i2}                                      //Whichever is the smaller scaleup value will be used.
    if(i<1){i=1;}                                       //This sets a minimum size.
    GAME.style.width=Math.floor(scrWidth*i)+"px";       //Set the canvas 'css' size.
    GAME.style.height=Math.floor(scrHeight*i)+"px";
}
//
//Keyboard Handling
function KEYBOARD(a){return keyPress[keys[a]]&&keyHold[a]==0;} //Returns true when a key is pressed AND the key was not already held down.
function KEYBOARDHOLD(a){keyHold[a]=1;} //The key is now 'held down'.
//
// -- COPY value
function COPY(a){return JSON.parse(JSON.stringify(a));} //Duplicates the value.
//
// -- Get position of snack
function SNACKSPOT(){
    let x,y,z=1,i0;
    while(z==1){ //This runs while an obstacle prevents the snack from being
                 //generated. This includes walls and body segments.
        z=0,
        x=Math.floor(Math.random()*16),              //Pick a random X & Y spot on the map
        y=Math.floor(Math.random()*14);
        if(mapX[y*16+x]==1){                         //Cancel if this lands on a map tile.
            z=1;
        }
        i0=pBody.length;
        for(let i=0; i<i0; i++){
            if(pBody[i][0]==x && pBody[i][1]==y){    //Cancel is this lands on a body segment.
                z=1;
            }
        }
        if(z==0){                                    //Set the snack co-ordinates and colour, here.
            snack=[x,y,Math.floor(Math.random()*5)];
        }
    }
}
//
// -- Graphics.
function STARTGFX(){
    gfx=GAMEX.getImageData(0,0,scrWidth,scrHeight); //Create an array from the existing screen data.
}
function ENDGFX(){
    GAMEX.putImageData(gfx,0,0); //Draw a page based on the created and modified array data.
    gfx=null;                    //Clears the array.
}
//
// -- Draw some pixels on the screen.
function DRAWBLOCK(x,y,w,h,r,g,b){ //X & Y starting point, width, height, red, green, blue
    let a;
    for(let i=0; i<h; i++){                                          //Loop the width and height to draw a box.
        for(let j=0; j<w; j++){
            if(x+j>=0 && x+j<scrWidth && y+i>=0 && y+i<scrHeight){//Make sure the pixel in question is not out of bounds.
                a=((y*scrWidth)+(i*scrWidth)+x+j)*4;                    //Get the pixel position within the graphics array data.
                gfx.data[a+0]=r;                                        //Update the red, green and blue values.
                gfx.data[a+1]=g;
                gfx.data[a+2]=b;
            }
        }
    }
}
//
// -- Draw some text on the screen.
function TEXT(x,y,a){
    let i0, j0, txtSetup;            //Function-specific variables for looping and the text characters.
    a=a.toString();                  //Make sure that the input is not a number.
    i0=a.length;
    for(let i=0; i<i0; i++){
        txtSetup=fontSetup[a[i]];    //ID the letter
        let j0=txtSetup.length;      //Determine how many times the draw function is being called
        for (let j=0; j<j0; j+=4){//Shadow
            DRAWBLOCK(x+(i*6)+txtSetup[j],y+2+txtSetup[j+1],txtSetup[j+2],txtSetup[j+3],224,224,224);
        }
        for (let j=0; j<j0; j+=4){//Main colour
            DRAWBLOCK(x+(i*6)+1+txtSetup[j],y+1+txtSetup[j+1],txtSetup[j+2],txtSetup[j+3],48,48,48);
        }
    }
}
//
// -- Start the game.
function STARTGAME(){
    pBody=[[7,8]], //We start with the head at a specific X & Y.
    pCollect=0,
    pDeaths=0,
    pDir="x",
    pLifeScore=250,
    pLifeBase=250,
    pLifeInc=250,
    pLives=2,
    pRound=0,
    pSegMax=5,
    pScore=0,
    pSnack=5,
    pTimer=0;
    //
    endRound=0,
    gTimer=0,
    mapX=COPY(mapSrc[0]),
    tempDir=4;
    SNACKSPOT();
    //
    requestAnimationFrame(ACTION); //Fire up the game.
}
//
// -- Main game cycle
function ACTION(){
    let x, y, bL, kill=0, pause=0; //Useful in-function variables: X&Y positions | body Length | Player crashed? | Game Paused
    //Keyboard controls
    if(KEYBOARD(0)){if(pDir!=2){tempDir=0;}}      //Left
    else if(KEYBOARD(1)){if(pDir!=3){tempDir=1;}} //Up
    else if(KEYBOARD(2)){if(pDir!=0){tempDir=2;}} //Right
    else if(KEYBOARD(3)){if(pDir!=1){tempDir=3;}} //Down
    if(KEYBOARD(4)){KEYBOARDHOLD(4); pause=1;}    //Pause Game?
    //Player action
    if(pTimer==0 && tempDir!=4){                        //The player can move around and is given a direction to move.
         if(endRound==0){                                //This operates when the worm is not leaving the map.
            pDir=COPY(tempDir), x=pBody[0][0], y=pBody[0][1];
            pBody.unshift([x+dirX[pDir],y+dirY[pDir]]); //Add a body segment to the start of the worm. notably the next tile the player is supposed to end up on.
            if(pBody.length>pSegMax){pBody.pop();}      //If the worm size exceeds the limit, remove the segment at the end.
            x=pBody[0][0], y=pBody[0][1];               //Current Position
            if(x==-1 || y==-1 || x==16 || y==14){       //Did the player exceed the boundaries of the map?
                endRound=1;                             //Round over.  Player has completed it.
            } else {
                if(mapX[y*16+x]==1){                    //Collision with wall. If yes, player gets killed.
                    kill=1;
                }
                bL=pBody.length;                        //Collision with body segment. If yes, player gets killed
                for(let i=1; i<bL; i++){
                    if(pBody[i][0]==pBody[0][0] && pBody[i][1]==pBody[0][1]){kill=1;}
                }
                if(pBody[0][0]==snack[0] && pBody[0][1]==snack[1]){//DINNER!
                    CHECKSCORE(5+pRound+snack[2]);                 //Add some points.
                    pSnack--;                                      //Deduct one from the snack counter.
                    pCollect++;                                    //Add one to the # of snacks munched.
                    pSegMax+=addLength[Math.min(pCollect,10)];     //Lengthen the size of the worm.
                    if(pSnack<=0){                                 //Open a patch to exit the map?
                        snack=[-1,-1,0];                           //Place the snack outside the limits of the screen.
                        let k=[7,8,96,111,112,127,215,216];        //which tiles to update
                        for(let x=0; x<8; x++){
                            mapX[k[x]]=0;
                        }
                    } else {SNACKSPOT();}                          //Setup new snack
                }
            }
         } else { //Automatically remove body segments to show the worm exiting the room.
           pBody.pop();
         }
    }
    //Graphics
    GAMEX.fillStyle=`rgb(255,255,255)`; //Beginning drawing, starting with a white background
    GAMEX.fillRect(0,0,scrWidth,scrHeight);
    STARTGFX();
    for(let y=0; y<14; y++){            //Draw the map tiles; 16x14 grid.
        for(let x=0; x<16; x++){
            if(mapX[(y*16)+x]==1){DRAWBLOCK(x*8,(y*8)+16,8,8,48,48,48);}
        }
    }
    if(snack[0]>=0){                    //Should the snack be drawn?
        DRAWBLOCK(snack[0]*8+1,snack[1]*8+17,5,5,snackColors[snack[2]][0],snackColors[snack[2]][1],snackColors[snack[2]][2]);
    }
    bL=pBody.length;
    for(let y=bL-1; y>=0; y--){         //Worm. Check is included to see if the head, body or tail is being
                                        //drawn.  We do this backwards, so the head is drawn last.
    if(pBody[y][1]>=0){
            if(y==0 && endRound==0){DRAWBLOCK(pBody[y][0]*8,pBody[y][1]*8+16,7,7,224,128,48);}  //Head. Do not draw this if the head is outside the room.
            else if (y==bL-1){DRAWBLOCK(pBody[y][0]*8+1,pBody[y][1]*8+17,5,5,80,192,64);}       //Tail
            else {DRAWBLOCK(pBody[y][0]*8,pBody[y][1]*8+16,7,7,80,192,64);}                     //Body
        }
    }
    TEXT(0,0,"LIVES:"+pLives);                          //Top line details
    TEXT(0,8,"ROUND:"+(pRound+1));
    let p=pScore.toString(); while(p.length<5){p="0"+p;}
    TEXT(60,0,"SCORE:"+p);
    TEXT(60,8,"SNACKS:"+pSnack);
    ENDGFX();                                           //Draw the graphics.
    //Misc.
    pTimer=(pTimer+1)%(20/(endRound+1));                //Timer for when the player can moved is advanced a frame.
        if(kill==1){                                    //Did the player crash?
        gTimer=0; requestAnimationFrame(CRASH);
    } else if(endRound==1 && pBody.length==0){          //Auto-end the round.
        requestAnimationFrame(CONTINUE);
    } else if(pause==1){                                //Game paused?
        requestAnimationFrame(PAUSEGAME);
    } else {                                            //Pretty much everything else.
        requestAnimationFrame(ACTION);
    }
}
//
// Pause Game
function PAUSEGAME(){
    let endPause=0;
    GAMEX.fillStyle=`rgb(48,48,48)`;              //Prep the box with 'game paused' text.
    GAMEX.fillRect(22,50,84,28);
    GAMEX.fillStyle=`rgb(255,255,255)`;
    GAMEX.fillRect(24,52,80,24);
    STARTGFX();
    //
    let p="GAME PAUSED";
    TEXT(64-(p.length*3),61,p);
    ENDGFX();                                     //Draw the graphics.
    if(KEYBOARD(4)){KEYBOARDHOLD(4); endPause=1;} //Hit spacebar to resume game.
    if(endPause==1){
        requestAnimationFrame(ACTION);
    } else {
        requestAnimationFrame(PAUSEGAME);
    }
}
//
// Crash
function CRASH(){
    if(gTimer==0){                            //1st frame updates the 'crash' sequence.
        GAMEX.fillStyle=`rgb(48,48,48)`;      //Prep the box with 'crash' or 'game over' text.
        GAMEX.fillRect(22,50,84,28);
        GAMEX.fillStyle=`rgb(255,255,255)`;
        GAMEX.fillRect(24,52,80,24);
        STARTGFX();
        if(pLives>=0){                        //Lives remaining
            TEXT(49,61,"CRASH");
        } else {                              //Out of lives
            TEXT(37,61,"GAME OVER");
        }
        ENDGFX();                             //Draw the graphics.
        //
        pBody=[[7,8]],                        //update some game variables.
        pDeaths++,
        pDir=4,
        pLives--,
        pTimer=0,
        tempDir=4;
        if(pSnack>0){SNACKSPOT();}            //Only run this if the exits are not visible.
    }
    gTimer++;
    if(gTimer==90){                           //end of the delay
        gTimer=0;
        if(pLives==0){
            requestAnimationFrame(STARTGAME); //Out of lives.  Start over.
        } else {
            requestAnimationFrame(ACTION);    //Back to the game.
        }
    } else {
        requestAnimationFrame(CRASH);
    }
}
//
// Continue onto the next round
function CONTINUE(){
    if(gTimer==0){                                 //1st frame updates the 'crash' sequence.
        GAMEX.fillStyle=`rgb(48,48,48)`;           //Prep the box with 'round #X'.
        GAMEX.fillRect(0,16,128,112);
        GAMEX.fillStyle=`rgb(255,255,255)`;
        GAMEX.fillRect(2,18,124,108);
        STARTGFX();
        let p="ROUND "+(pRound+2);
        TEXT(64-(p.length*3),61,p);                //Lives remaining
        ENDGFX();                                  //Draw the graphics.
        //
        if(pDeaths==0){CHECKSCORE((pRound*3)+25)}  //No deaths = free bonus points.
        pBody=[[7,8]],                             //Update some variables.
        pCollect=0,
        pDeaths=0,
        pDir=4,
        pRound++,
        pSegMax=5,
        pSnack=Math.min(5+Math.floor(pRound/5),10), //Maximum snacks: 10 
        pTimer=0,
        //
        endRound=0,
        mapX=COPY(mapSrc[pRound%mapSrc.length]),
        tempDir=4;
        SNACKSPOT();
    }
    gTimer++;
    if(gTimer==90){
        gTimer=0;
        requestAnimationFrame(ACTION);             //Start the next round.
    } else {
        requestAnimationFrame(CONTINUE);
    }
}
//
// -- Check Score
function CHECKSCORE(a){
    pScore=Math.min(pScore+a,99999);   //Adds to the score and keeps it capped at 99999.
    if(pScore>=pLifeScore){            //check if score exceeds the score for giving a bonus life.
        pLifeScore+=pLifeBase;         //Update the score that get compared.
        pLifeBase+=pLifeInc;           //Update the base that is added to the score.
        pLives=Math.min(pLives+1,9);   //Give the player an extra life.  Cap it at 9.
    }
}
//
//EXECUTE THE GAME
GAME.width=scrWidth;   //Set 'canvas' size.
GAME.height=scrHeight;
SETSIZE();             //Set 'css' size.
STARTGAME();           //Get the game underway.

What you should have now is a fully playable, albeit small game with a start, end, score, ability to move around and progress as you collect objectives.

This concludes the tutorial. Thank you for sticking around to the end. Feel free to leave comments if you like what you saw.

Leave a comment

Log in with itch.io to leave a comment.