making a tic tack toe game, can't make an 'x'

79 views Asked by At

I'm trying to teach myself no frills javascript game development. I've chosen to hold all possible places on the board for the game to need to render an x or o as possible moves in the logic object. I can't figure out how to draw the x inside the area of the rect it's to appear in. I want the player to eventually click or touch any space in the area of on of the possible moves object's rects. How do I do that? How do I redo this when I need to make them instances without any idea where the player will click or touch?

// the stage object holds the HTML5 canvas, it's 2d context, and a self starting function that sizes it. (unless all ready fired, canvas is not defined.)
var stage = {
    canvas: document.getElementById('canvas'),
    context: this.canvas.getContext('2d'),
    full_screen: (function () {
        this.canvas.width = document.documentElement.clientWidth;
        this.canvas.height = window.innerHeight;
        this.canvas.style.border = '1px solid black';
        console.log(this.canvas);
        return this.canvas;
    })()
};

stage.width = stage.canvas.width;
stage.height = stage.canvas.height;


var init = function () {
// ui for the game
var button = {
    pause: document.getElementById('pause'),
    restart: document.getElementById('restart'),
    options: document.getElementById('opt')
};

// this function assigns functions the ui buttons
var functionality = function () {
    button.pause.onclick = pause;
    button.restart.onclick = restart;
    button.options.onclick = options;
};

var logic = {
    player: { score: 0 },
    cpu: { score: 0 },
    possible_moves: {
        x: 0,
        y: 0,
        top_left: {
            x: stage.width * .05,
            y: stage.height * .02,
            width: stage.width * .22,
            height: stage.height * .22,
            draw: function () {
                stage.context.beginPath();
                stage.context.lineWidth = 1;
                stage.context.rect(this.x, this.y, this.width, this.height);
                stage.context.stroke();
            }
        },
        top_middle: {
            x: stage.canvas.width * .385,
            y: stage.canvas.height * .02,
            width: stage.width * .22,
            height: stage.height * .22,
            draw: function () {
                stage.context.beginPath();
                stage.context.lineWidth = 1;
                stage.context.rect(this.x, this.y, this.width, this.height);
                stage.context.stroke();
            }
        },
        top_right: {
            x: stage.canvas.width * .715,
            y: stage.canvas.height * .02,
            width: stage.width * .22,
            height: stage.height * .22,
            draw: function () {
                stage.context.beginPath();
                stage.context.lineWidth = 1;
                stage.context.rect(this.x, this.y, this.width, this.height);
                stage.context.stroke();
            }
        },
        middle_left: {
            x: stage.canvas.width * .05,
            y: stage.canvas.height * .35,
            width: stage.width * .22,
            height: stage.height * .22,
            draw: function () {
                stage.context.beginPath();
                stage.context.lineWidth = 1;
                stage.context.rect(this.x, this.y, this.width, this.height);
                stage.context.stroke();
            }
        },
        middle_middle: {
            x: stage.canvas.width * .385,
            y: stage.canvas.height * .35,
            width: stage.width * .22,
            height: stage.height * .22,
            draw: function () {
                stage.context.beginPath();
                stage.context.lineWidth = 1;
                stage.context.rect(this.x, this.y, this.width, this.height);
                stage.context.stroke();
            }
        },
        middle_right: {
            x: stage.canvas.width * .715,
            y: stage.canvas.height * .35,
            width: stage.width * .22,
            height: stage.height * .22,
            draw: function () {
                stage.context.beginPath();
                stage.context.lineWidth = 1;
                stage.context.rect(this.x, this.y, this.width, this.height);
                stage.context.stroke();
            }
        },
        bottom_left: {
            x: stage.canvas.width * .05,
            y: stage.canvas.height * .68,
            width: stage.width * .22,
            height: stage.height * .22,
            draw: function () {
                stage.context.beginPath();
                stage.context.lineWidth = 1;
                stage.context.rect(this.x, this.y, this.width, this.height);
                stage.context.stroke();
            }
        },
        bottom_middle: {
            x: stage.canvas.width * .385,
            y: stage.canvas.height * .68,
            width: stage.width * .22,
            height: stage.height * .22,
            draw: function () {
                stage.context.beginPath();
                stage.context.lineWidth = 1;
                stage.context.rect(this.x, this.y, this.width, this.height);
                stage.context.stroke();
            }
        },
        bottom_right: {
            x: stage.canvas.width * .715,
            y: stage.canvas.height * .68,
            width: stage.width * .22,
            height: stage.height * .22,
            draw: function () {
                stage.context.beginPath();
                stage.context.lineWidth = 1;
                stage.context.rect(this.x, this.y, this.width, this.height);
                stage.context.stroke();
            }
        },
        draw_top_row: function () {
            logic.possible_moves.top_left.draw();
            logic.possible_moves.top_middle.draw();
            logic.possible_moves.top_right.draw();
        },
        draw_middle_row: function () {
            logic.possible_moves.middle_left.draw();
            logic.possible_moves.middle_middle.draw();
            logic.possible_moves.middle_right.draw();
        },
        draw_bottom_row: function () {
            logic.possible_moves.bottom_left.draw();
            logic.possible_moves.bottom_middle.draw();
            logic.possible_moves.bottom_right.draw();
        },
        draw_left_column: function () {
            logic.possible_moves.top_left.draw();
            logic.possible_moves.middle_left.draw();
            logic.possible_moves.bottom_left.draw();
        },
        draw_middle_column: function () {
            logic.possible_moves.top_middle.draw();
            logic.possible_moves.middle_middle.draw();
            logic.possible_moves.bottom_middle.draw();
        },
        draw_right_column: function () {
            logic.possible_moves.top_right.draw();
            logic.possible_moves.middle_right.draw();
            logic.possible_moves.bottom_right.draw();
        },
        draw_left_to_right_diagonal: function () {
            logic.possible_moves.top_left.draw();
            logic.possible_moves.middle_middle.draw();
            logic.possible_moves.bottom_right.draw();
        },
        draw_right_to_left_diagonal: function () {
            logic.possible_moves.top_right.draw();
            logic.possible_moves.middle_middle.draw();
            logic.possible_moves.bottom_left.draw();
        },
        draw_all_moves: function () {
            logic.possible_moves.top_left.draw();
            logic.possible_moves.top_middle.draw();
            logic.possible_moves.top_right.draw();
            logic.possible_moves.middle_left.draw();
            logic.possible_moves.middle_middle.draw();
            logic.possible_moves.middle_right.draw();
            logic.possible_moves.bottom_left.draw();
            logic.possible_moves.bottom_middle.draw();
            logic.possible_moves.bottom_right.draw();
        },
        generate_logic_map: (function () {

        })()
    }
};

// I had to add the scoreboard to the logic object as an after thought because I wanted to just reference the two individual player and cpu objects in case I need to increase complextity to those cbjects seperately. Also, jaascript won't allow me to reference these propties "inside" the object.
logic.score_board = {
    p: logic.player.score,
    c: logic.cpu.score
};

// this object holds the visual elements of the game
var assets = {
    x: {
        left_to_right: {
            x1: logic.possible_moves.top_left.x,
            y1: logic.possible_moves.top_left.y,
            x2: logic.possible_moves.top_left.width,
            y2: logic.possible_moves.top_left.height,
            draw: function () {
                stage.context.beginPath();
                stage.context.moveTo(this.x1, this.y1);
                stage.context.lineTo(this.x2, this.y2);
                stage.context.stroke();
                console.log(this.x1, this.x2, this.y1, this.y2);
            }
        },
        right_to_left: {
            x1: logic.possible_moves.top_left.width,
            y1: logic.possible_moves.top_left.height,
            x2: 0,
            y2: 43,
            draw: function () {
                stage.context.beginPath();
                stage.context.moveTo(this.x1, this.y1);
                stage.context.lineTo(this.x2, this.y2);
                stage.context.stroke();
                console.log(this.x1, this.x2, this.y1, this.y2);
            }
        },
        draw: function () {
            console.log(this.left_to_right.x1, this.left_to_right.y1, this.left_to_right.x2, this.left_to_right.y2);
            stage.context.lineWidth = 5;
            stage.context.strokeStyle = 'black';
            this.left_to_right.draw();
            //this.right_to_left.draw();
        }
    },
    o: {},
    grid: {
        x: 0,
        y: 0,
        horizontal_line_l: {
            x1: stage.canvas.width * .02,
            y1: stage.canvas.height * .33,
            x2: stage.canvas.width * .98,
            y2: stage.canvas.height * .33,
            draw: function () {
                stage.context.beginPath();
                stage.context.moveTo(this.x1, this.y1);
                stage.context.lineTo(this.x2, this.y2);
                stage.context.stroke();
            }
        },
        horizontal_line_r: {
            x1: stage.canvas.width * .02,
            y1: stage.canvas.height * .66,
            x2: stage.canvas.width * .98,
            y2: stage.canvas.height * .66,
            draw: function () {
                stage.context.beginPath();
                stage.context.moveTo(this.x1, this.y1);
                stage.context.lineTo(this.x2, this.y2);
                stage.context.stroke();
            }
        },
        vertical_line_u: {
            x1: stage.canvas.width * .33,
            y1: stage.canvas.height * .02,
            x2: stage.canvas.width * .33,
            y2: stage.canvas.height * .98,
            draw: function () {
                stage.context.beginPath();
                stage.context.moveTo(this.x1, this.y1);
                stage.context.lineTo(this.x2, this.y2);
                stage.context.stroke();
            }
        },
        vertical_line_d: {
            x1: stage.canvas.width * .66,
            y1: stage.canvas.height * .02,
            x2: stage.canvas.width * .66,
            y2: stage.canvas.height * .98,
            draw: function () {
                stage.context.beginPath();
                stage.context.moveTo(this.x1, this.y1);
                stage.context.lineTo(this.x2, this.y2);
                stage.context.stroke();
            }
        },
        draw: function () {
            stage.context.lineWidth = 20;
            stage.context.strokeStyle = '#0000ff';
            stage.context.lineCap = 'round';
            this.horizontal_line_l.draw();
            this.horizontal_line_r.draw();
            this.vertical_line_u.draw();
            this.vertical_line_d.draw();
        }
    },
    text: {}
};

    assets.grid.draw();
    logic.possible_moves.draw_all_moves();
    assets.x.draw();
};

window.onload = init();

<!DOCTYPE html>

<html lang="en" xmlns="http://www.w3.org/1999/xhtml">
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
    <title>Tik Tack Toe</title>
    <link rel="stylesheet" type="text/css" href="http://yui.yahooapis.com/3.18.1/build/cssreset/cssreset-min.css">
    <link rel="stylesheet" type="text/css" href="game.css" />
</head>
<body>
    <div id="container">
        <canvas id="canvas"></canvas>
        <div id="UI" class="">
            <ul>
                <li><button id="pause">Pause</button></li>
                <li><button id="restart">Restart</button></li>
                <li><button id="opt">Options</button></li>
            </ul>
        </div>
    </div>
    <script src="game.js"></script>
</body>
</html>
1

There are 1 answers

1
Blindman67 On

Basic Rendering and IO

It is the basics of game development that you have assets that are rendered many times, in a variety of places, scales, orientations, etc.

Rendering

So lets start with drawing a basic cross (X) and assuming you have the 2D canvas context as ctx

First set up the context

ctx.strokeStyle = "black"; // the colour/style of the cross
ctx.lineWidth = 10; // the width of a stroke in pixels.

Then add some path elements, we will set the cross to be in a square 100 by 100 pixels.

// Very important that you use the following line whenever creating new paths
// if not you end up adding to the existing path
ctx.beginPath(); // tell the context we are starting a new path. 
ctx.moveTo(10,10); // start of first line top left
ctx.lineTo(90,90); // create a line to the bottom right
ctx.moveTo(90,10); // move to the top right
ctx.lineTo(10,90); // create a line to the bottom left

// now the path is defined we can render it
ctx.stroke();

    const canvas = document.createElement("canvas");
    canvas.width = canvas.height = 100;
    const ctx = canvas.getContext("2d");
    document.body.appendChild(canvas);


    ctx.strokeStyle = "black"; // the colour/style of the cross
    ctx.lineWidth = 10; // the width of a stroke in pixels.
    // Very important that you use the following line whenever creating new paths
    // if not you end up adding to the existing path
    ctx.beginPath(); // tell the context we are starting a new path. 
    ctx.moveTo(10,10); // start of first line top left
    ctx.lineTo(90,90); // create a line to the bottom right
    ctx.moveTo(90,10); // move to the top right
    ctx.lineTo(10,90); // create a line to the bottom left

    // now the path is defined we can render it
    ctx.stroke();

And it is much the same for the circle

    const canvas = document.createElement("canvas");
    canvas.width = canvas.height = 100;
    const ctx = canvas.getContext("2d");
    document.body.appendChild(canvas);


    ctx.strokeStyle = "black"; // the colour/style of the cross
    ctx.lineWidth = 10; // the width of a stroke in pixels.
    // Very important that you use the following line whenever creating new paths
    // if not you end up adding to the existing path
    ctx.beginPath(); // tell the context we are starting a new path. 
    ctx.arc(50,50,40,0,Math.PI * 2); // create a circle path

    // now the path is defined we can render it
    ctx.stroke();

We want to make the cross and circle an entity we can draw anywhere so we will wrap each in a function definition, adding some arguments to set where and some extra details like colour.

// draw a cross with the top left at x,y
function drawCross(x,y,col){
    ctx.save(); // save the current canvas context state
    ctx.translate(x,y); // set where on the canvas the top left will be
    ctx.strokeStyle = col;
    ctx.lineWidth = 10; 
    ctx.beginPath(); 
    ctx.moveTo(10,10); 
    ctx.lineTo(90,90); 
    ctx.moveTo(90,10); 
    ctx.lineTo(10,90); 
    ctx.stroke();
    ctx.restore(); // now restore the canvas state
}
function drawCircle(x,y,col){
    ctx.save(); // save the current canvas context state
    ctx.translate(x,y); // set where on the canvas the top left will be
    ctx.strokeStyle = col;
    ctx.lineWidth = 10; 
    ctx.beginPath(); 
    ctx.arc(50,50,40,0,Math.PI * 2); // create a circle path
    ctx.stroke();
    ctx.restore(); // now restore the canvas state
}

Game state

Now we want to create some means of storing the gameboard. We can use a simple array with one items for each of the 9 areas. Also some constants to define what is held in each location

// a 2d array containing 3 arrays one for each row
var gameBoard = [[0,0,0],[0,0,0],[0,0,0]];
const empty = 0;
const cross = 1;
const circle = 2; 
var turn = circle; // whos turn it is

Now a function to let us set a location. We do not want this function to just blindly set a location. It will first check if it is empty and if so only then add the move. It will return true for a valid move or false if not. That makes it easy for us to add moves without having to check the board elsewhere for valid moves.

// set a board position x y with a type
function setBoard(x,y,type){
     if(gameBoard[y][x] === empty){ // only if empty
         gameBoard[y][x] = type;
         return true; // indicate we have set the position
     }
     return false; // could not set location 
}

So now we can put these parts together to render the board

function renderBoard(){
    var x, y;
    // as we may have some stuff already drawn we need to clear the
    // board
    ctx.clearRect(0,0,300,300);
    // lets draw the horizontal and vertical lines
    // We can use fillRect as it does not need the beginPath command
    // or a line width
    ctx.fillStyle = "black";
    ctx.fillRect(97,0,6,300);
    ctx.fillRect(197,0,6,300);
    ctx.fillRect(0,97,300,6);
    ctx.fillRect(0,197,300,6);

    for(y = 0; y < 3; y ++){
        for(x = 0; x < 3; x++){
            var loc = gameBoard[y][x]; // get what is at the location
            if(loc === cross){ // is it a cross?
                 // as the area is 100 by 100 pixels we need th correct top left
                 // coordinate, so multiply the x and y by 100
                 drawCross(x * 100, y * 100, "red");
            }else if(loc === circle){ // is it a circle
                 drawCircle(x * 100, y * 100, "red");
            }
        }
    }

}

IO the mouse wrangler

Now we have all the rendering set up we need some input so create some mouse listeners.

 // fisrt a mouse object to hold mouse state
 const mouse = {};
 function mouseEvent(event){
     var bounds = canvas.getBoundingClientRect(); // get the canvas loc
     // get the mouse position relative to the canvas top left
     mouse.x = event.pageX - (bounds.left + scrollX);
     mouse.y = event.pageY - (bounds.top + scrollY);          
     if(event.type === "mouseup"){  // when the mouse button is up we have a click
         mouse.clicked = true;             
     }
  }  
  canvas.addEventListener("mousemove",mouseEvent);
  canvas.addEventListener("mouseup",mouseEvent);

Putting it all together

To ensure we don't get in the way of the DOM we need to sync our rendering with it. To do this we create a timed rendering loop. Though we well not render everything each time we can just keep it happening 60 times a second for convenience.

 var turn = circle; // who turn it is
 function mainLoop(){
      requestAnimationFrame(mainLoop); // ask the DOM for the next convenient time to render
      // now check the mouse button
      if(mouse.clicked){ // yes a click
          mouse.clicked = false; // clear the click
          // now convert the pixel coords of mouse to game board coords
          var bx = Math.floor(mouse.x / 100);
          var by = Math.floor(mouse.y / 100);
          if(setBoard(dx,dy,turn)){ // set the location. Function returns true if a valid move
               // all good so draw the board
               renderBoard();
               // getthe next turn
               turn = turn === circle ? cross : circle;
          }
      }
  }

  // start it all going                                        
  requestAnimationFrame(mainLoop); 

Snippet

As a snippet with code to add the canvas.

    const canvas = document.createElement("canvas");
    canvas.width = canvas.height = 300;
    const ctx = canvas.getContext("2d");
    document.body.appendChild(canvas);

    // draw a cross with the top left at x,y
    function drawCross(x,y,col){
        ctx.save(); // save the current canvas context state
        ctx.translate(x,y); // set where on the canvas the top left will be
        ctx.strokeStyle = col;
        ctx.lineWidth = 10; 
        ctx.beginPath(); 
        ctx.moveTo(10,10); 
        ctx.lineTo(90,90); 
        ctx.moveTo(90,10); 
        ctx.lineTo(10,90); 
        ctx.stroke();
        ctx.restore(); // now restore the canvas state
    }
    function drawCircle(x,y,col){
        ctx.save(); // save the current canvas context state
        ctx.translate(x,y); // set where on the canvas the top left will be
        ctx.strokeStyle = col;
        ctx.lineWidth = 10; 
        ctx.beginPath(); 
        ctx.arc(50,50,40,0,Math.PI * 2); // create a circle path
        ctx.stroke();
        ctx.restore(); // now restore the canvas state
    }
    // a 2d array containing 3 arrays one for each row
    var gameBoard = [[0,0,0],[0,0,0],[0,0,0]];
    const empty = 0;
    const cross = 1;
    const circle = 2; 
    // set a board position x y with a type
    function setBoard(x,y,type){
         if(gameBoard[y][x] === empty){ // only if empty
             gameBoard[y][x] = type;
             return true; // indicate we have set the position
         }
         return false; // could not set location 
    }
    function renderBoard(){
        var x, y;
        // as we may have some stuff already drawn we need to clear the
        // board
        ctx.clearRect(0,0,300,300);
        // lets draw the horizontal and vertical lines
        // We can use fillRect as it does not need the beginPath command
        // or a line width
        ctx.fillStyle = "black";
        ctx.fillRect(97,0,6,300);
        ctx.fillRect(197,0,6,300);
        ctx.fillRect(0,97,300,6);
        ctx.fillRect(0,197,300,6);

        for(y = 0; y < 3; y ++){
            for(x = 0; x < 3; x++){
                var loc = gameBoard[y][x]; // get what is at the location
                if(loc === cross){ // is it a cross?
                     // as the area is 100 by 100 pixels we need th correct top left
                     // coordinate, so multiply the x and y by 100
                     drawCross(x * 100, y * 100, "red");
                }else if(loc === circle){ // is it a circle
                     drawCircle(x * 100, y * 100, "blue");
                }
            }
        }

     }
     // fisrt a mouse object to hold mouse state
     const mouse = {};
     function mouseEvent(event){
         var bounds = canvas.getBoundingClientRect(); // get the canvas loc
         // get the mouse position relative to the canvas top left
         mouse.x = event.pageX - (bounds.left + scrollX);
         mouse.y = event.pageY - (bounds.top + scrollY);          
         if(event.type === "mouseup"){  // when the mouse button is up we have a click
             mouse.clicked = true;             
         }
      }  
      canvas.addEventListener("mousemove",mouseEvent);
      canvas.addEventListener("mouseup",mouseEvent);

     var turn = circle; // who turn it is
     function mainLoop(){
          requestAnimationFrame(mainLoop); // ask the DOM for the next convenient time to render
          // now check the mouse button
          if(mouse.clicked){ // yes a click
              mouse.clicked = false; // clear the click
              // now convert the pixel coords of mouse to game board coords
              var bx = Math.floor(mouse.x / 100);
              var by = Math.floor(mouse.y / 100);
              if(setBoard(bx,by,turn)){ // set the location. Function returns true if a valid move
                   // all good so draw the board
                   renderBoard();
                   // getthe next turn
                   turn = turn === circle ? cross : circle;
              }
          }
         
      }
      // draw the empty board
      renderBoard();
      // start it all going                                        
      requestAnimationFrame(mainLoop); 

Hope that helps..