1
\$\begingroup\$

Made fifteen in JavaScript. I would like JavaScript programmers to point out my mistakes. My code works fine. I made fifteen using a two-dimensional array and a one-dimensional array. The function for determining the winner in a simplified form. How can I make the function for determining the winner more correctly?

<!DOCTYPE html>

<html>

<head>

<meta name="viewport" content="width=device-width, initial-scale=1.0"/>

<title>javascript Canvas 15taggame</title>

<style>

body 
{
  text-align: center;

}

#my_canvas
{

  border: 2px solid green;
  background-color: lightblue;  

}

</style>

</head>

<body>

<h1>javascript Canvas 15taggame</h1>
<p>javascript Canvas 15taggame</p>
<canvas id="my_canvas"></canvas>

<br>
<br>

<div id="coord"></div>

<br>
<br>


<span>CoordMouse: X - <tt id="myX">0</tt> --- Y - <tt id="myY">0</tt></span>

<script>

var canvas = document.getElementById('my_canvas');


// set native size of the canvas
canvas.width = 800;

canvas.height = 500;

const mouse = { x:0, y:0};      // координаты мыши


var ctx = canvas.getContext("2d");


let SPACE_SIZE = 100;
let limit = 4;

let arrVal = [];
let arr15_02 = [];

let arr___03 = [];

let array = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15];

shufleArray();

rect = 
{
  startX: 1,
  startY: 1,
  w: 400,
  h: 400
}

// randomize (shuffle) array
function shufleArray()
{

  for (var i = 0; i < 10; i++)
  {

    array.sort(()=> Math.random() - 0.5);
  }

  for (var i = 0; i < array.length; i += limit) 
  {
    arr___03.push(array.slice(i, i + limit));
  }
  return arr___03;
}

//Filling the array
for(i = 0; i < 4; i++)
{

  arr15_02[i] = [];

  for(j = 0; j < 4; j++)
  {
    arr15_02[i][j] = arr___03[i][j];


    arrVal.push(arr15_02[i][j]);



  }
}


console.log("arrVal: " + arrVal);

draw();
buildBoard();

//Building a board      
function buildBoard()
{
  for(i = 0; i < 4; i++)
  {

    arr15_02[i] = [];

    for(j = 0; j < 4; j++)
    {
      arr15_02[i][j] = arr___03[i][j];

      drawSquare(j * SPACE_SIZE, i * SPACE_SIZE, arr15_02[i][j]);


    }   
  }


}

//----------------------------------------------------------
//Draw a grid
function draw()
{
  ctx.beginPath();

  ctx.rect(rect.startX, rect.startY, rect.w, rect.h);

  ctx.fillStyle = "rgba(0,122,0, 0.2)";
  ctx.fill();
  ctx.closePath();

  // Horizontal ------------------------
  ctx.beginPath();
  ctx.moveTo(0, 100);
  ctx.lineTo(400, 100);

  ctx.moveTo(0, 200);
  ctx.lineTo(400, 200);

  ctx.moveTo(0, 300);
  ctx.lineTo(400, 300);

  ctx.moveTo(0, 400);
  ctx.lineTo(400, 400);

  // Vertical --------------------------------  
  ctx.moveTo(100, 0);
  ctx.lineTo(100, 400);

  ctx.moveTo(200, 0);
  ctx.lineTo(200, 400);

  ctx.moveTo(300, 0);
  ctx.lineTo(300, 400);

  ctx.moveTo(400, 0);
  ctx.lineTo(400, 400);

  ctx.strokeStyle = "fuchsia";
  ctx.closePath();    //  закрываем путь
  ctx.stroke();


  ctx.fill();
  ctx.closePath();
}

//-------------------------------------------------------
//Draw a square - cell of fifteen
function drawSquare(x, y, val)
{   
  ctx.fillStyle = "blue"

  ctx.shadowColor = '#000';


  ctx.fillRect(x, y, 100, 100)

  if(val === 0)
  {
    ctx.fillStyle = "white"
    ctx.fillRect(x + 5, y + 5, 90, 90);
  }

  else
  {
    ctx.fillStyle = "teal"
    ctx.shadowBlur = 4;
    ctx.shadowOffsetX = 0;
    ctx.shadowOffsetY = 2;
    ctx.fillRect(x + 5, y + 5, 90, 90);
  }



  ctx.font = "60px Arial"

  ctx.fillStyle = "white"

  if(val === 0)
  {
    ctx.font = "0px Arial"
  }

  if(val < 10)
  {
    ctx.fillText(val, x + 35, y + 70)
  }

  else
  {
    ctx.fillText(val, x + 15, y + 70)
  }
}


//Draw on a Canvas Tag - cell position
function drawTag(pos, val)
{
  switch(pos)
  {
    case 0:
    drawSquare(0, 0, val);
    break

    case 1:
    drawSquare(100, 0, val);
    break   

    case 2:
    drawSquare(200, 0, val);
    break

    case 3:
    drawSquare(300, 0, val);
    break

    case 4:
    drawSquare(0, 100, val);
    break

    case 5:
    drawSquare(100, 100, val);
    break

    case 6:
    drawSquare(200, 100, val);
    break

    case 7:
    drawSquare(300, 100, val);
    break

    case 8:
    drawSquare(0, 200, val);
    break

    case 9:
    drawSquare(100, 200, val);
    break

    case 10:
    drawSquare(200, 200, val);
    break

    case 11:
    drawSquare(300, 200, val);
    break

    case 12:
    drawSquare(0, 300, val);
    break

    case 13:
    drawSquare(100, 300, val);
    break

    case 14:
    drawSquare(200, 300, val);
    break

    case 15:
    drawSquare(300, 300, val);
    break

  }
}

//Determine the click location on the canvas
function checkPlace(evX)
{
  if (evX < 100)
  {
    console.log("column 1");
    return 1
  } 

  if (evX < 200)
  {
    console.log("column 2");
    return 2
  }

  if (evX < 300)
  {
    console.log("column 3");
    return 3
  }

  if (evX < 400)
  {
    console.log("column 4");
    return 4
  }
}

//------------------------------------
// Processing mouse clicks
canvas.addEventListener("mousedown", function(e)
{
  const x = e.offsetX;
  const y = e.offsetY;

  //console.log("row 1")
  console.log("mousedown__x: " + x);
  console.log("mousedown__y: " + y);




  <!-- console.log(event); -->
  <!-- console.log("event ---  " + event); -->




  if (y < 100)
  {
    console.log("row 1")
    var place = checkPlace(x);

    switch(place)
    {
      case 1: 
      var clickPos = 0



      break

      case 2: 
      var clickPos = 1

      break

      case 3: 
      var clickPos = 2

      break

      case 4: 
      var clickPos = 3

      break

    }
  }

  if (y > 100 && y < 200)
  {
    console.log("row 2");

    var place = checkPlace(x);

    switch(place)
    {
      case 1: 
      var clickPos = 4

      break

      case 2: 
      var clickPos = 5

      break

      case 3: 
      var clickPos = 6

      break

      case 4: 
      var clickPos = 7

      break

    }

  }

  if (y > 200 && y < 300)
  {
    console.log("row 3");

    var place = checkPlace(x);

    switch(place)
    {
      case 1: 
      var clickPos = 8

      break

      case 2: 
      var clickPos = 9

      break

      case 3: 
      var clickPos = 10

      break

      case 4: 
      var clickPos = 11

      break

    }

  }

  if (y > 300 && y < 400)
  {
    console.log("row 4");

    var place = checkPlace(x);

    switch(place)
    {
      case 1: 
      var clickPos = 12

      break

      case 2: 
      var clickPos = 13

      break

      case 3: 
      var clickPos = 14

      break

      case 4: 
      var clickPos = 15

      break

    }

  }

  console.log("clickPos --- " + clickPos);
  console.log("arrVal[clickPos] --- " + arrVal[clickPos]);


  // если нулевая плитка сверху
  //if the zero tile is on top
  if (arrVal[clickPos - 4] === 0)
  {
    arrVal[clickPos - 4] = arrVal[clickPos]
    arrVal[clickPos] = 0


    for (var i = 0; i <=15; i++)
    {
      drawTag(i, arrVal[i])     
    }

    console.log("нулевая плитка сверху --- ");
  }

  // если нулевая плитка снизу
  //if the zero tile is below
  if (arrVal[clickPos + 4] === 0)
  {
    arrVal[clickPos + 4] = arrVal[clickPos]
    arrVal[clickPos] = 0

    for (var i = 0; i <= 15; i++)
    {
      drawTag(i, arrVal[i])     
    }
  }

  // если нулевая плитка справа
  // if the zero tile is on the right
  if (arrVal[clickPos + 1] === 0 && clickPos !== 3
  && clickPos !== 7 && clickPos !== 11)
  {
    arrVal[clickPos + 1] = arrVal[clickPos]
    arrVal[clickPos] = 0

    for (var i = 0; i <=15; i++)
    {
      drawTag(i, arrVal[i])     
    }
  }

  // если нулевая плитка слева
  // if the zero tile is on the left
  if (arrVal[clickPos - 1] === 0 && clickPos !== 4
  && clickPos !== 8 && clickPos !== 12)
  {
    arrVal[clickPos - 1] = arrVal[clickPos]
    arrVal[clickPos] = 0

    for (var i = 0; i <=15; i++)
    {
      drawTag(i, arrVal[i])     
    }
  }

  checkWin();

});




//----------------------------------------------------------
//Determine the winner
function checkWin()
{
  if (arrVal[0] === 1 && arrVal[1] === 2 && arrVal[2] === 3
  && arrVal[3] === 4 && arrVal[4] === 5 && arrVal[5] === 6
  && arrVal[6] === 7 && arrVal[7] === 8 && arrVal[8] === 9
  && arrVal[9] === 10 && arrVal[10] === 11 && arrVal[11] === 12
  && arrVal[12] === 13 && arrVal[13] === 14 && arrVal[14] === 15
  && arrVal[15] === 0)
  { 
    console.log("ПОБЕДА! ---  ");

    ctx.font = "60px Verdana";
    ctx.fillStyle = "red";
    ctx.fillText("VICTORY", 60, 200);
    <!-- ctx.fillText("ПОБЕДА!", 70, 200); -->

  }
}

//----------------------------------------------------------
// mouse click
canvas.addEventListener("mousedown", function(e)
{
  mouse.x = e.pageX - this.offsetLeft;
  mouse.y = e.pageY - this.offsetTop;

  coord.innerHTML = 'Coord mouseup: (' + mouse.x + ',' + mouse.y + ')';


  myX.innerHTML = mouse.x;
  myY.innerHTML = mouse.y;

});

</script>



</body>

</html>

\$\endgroup\$
5
  • 2
    \$\begingroup\$ Too much vertical whitespace. Use Prettier and make sure most lines of code don't have vertical space between them, except use a single vertical space to delimit functions, classes, blocks and small chunks of code that are responsible for one piece of logic. Also avoid switch--use object or array lookups or ifs. \$\endgroup\$
    – ggorlen
    Commented Jul 29 at 19:16
  • 9
    \$\begingroup\$ Who or what is fifteen? \$\endgroup\$
    – Mast
    Commented Jul 29 at 19:29
  • \$\begingroup\$ After running the code, it looks to be a 15 puzzle - a puzzle where you have a 4x4 grid with one square missing. You move numbered squares until they are in numerical order. \$\endgroup\$ Commented Aug 2 at 19:08
  • \$\begingroup\$ Added a link to the Wikipedia page describing the game. \$\endgroup\$ Commented Aug 2 at 19:09
  • \$\begingroup\$ Would you like to implement a solver? Just curious. \$\endgroup\$
    – coderodde
    Commented Aug 3 at 7:10

2 Answers 2

3
\$\begingroup\$

The end of your post talks about checking the winning condition being in a simplified form. At first, I didn't see any improvements. Honestly, even though the if statement condition is pretty big, there is only one way to win this game. In my opinion, it makes sense to hard-code the winning condition. The only improvement I can see is to declare a local variable to assign the boolean value to. This gives you some additional formatting options so the code is easier to read:

const isWin = arrVal[0] === 1
           && arrVal[1] === 2
           && arrVal[2] === 3
           && arrVal[3] === 4
           && arrVal[4] === 5
           && arrVal[5] === 6
           && arrVal[6] === 7
           && arrVal[7] === 8
           && arrVal[8] === 9
           && arrVal[9] === 10
           && arrVal[10] === 11
           && arrVal[11] === 12
           && arrVal[12] === 13
           && arrVal[13] === 14
           && arrVal[14] === 15
           && arrVal[15] === 0;

if (isWin)

Now that we have this code lined up, we can clearly see a pattern allowing us to algorithmically check for the winning condition. Let n be the array index number. We can see that arrVal[n] === n + 1 except for the very last index, which we expect to be zero. We could write a for loop to check for this winning condition, and it would be fewer lines of code, but it's hard to argue that it would be easier to understand. We only have a 4x4 grid. The checkWin function just needs a bit of tidying up, but that's all.

CSS Code Formatting

In general, there are too many empty lines. Typically you have empty lines between CSS style declarations, but not inside them. The opening { is usually put on the same line as the CSS selector:

<style>

body 
{
  text-align: center;

}

#my_canvas
{

  border: 2px solid green;
  background-color: lightblue;  

}

</style>

Becomes:

<style>
  body {
    text-align: center;
  }

  #my_canvas {
    border: 2px solid green;
    background-color: lightblue;  
  }
</style>

Note that the CSS code should be indented one tabstop from the <style> tag to help provide visual separation and structure.

HTML Code Formatting

  • No need for empty lines between elements with the same parent, but we make an exception for the <head> and <body> elements because every other element we write is nested under one of those two.

    <!DOCTYPE html>
    <html>
    <head>
      <meta...>
      <title>...</title>
      <style>...</style>
    </head>
    <body>
      <h1>...</h1>
      <p>...</p>
      <canvas>...</canvas>
      ...
    </body>
    </html>
    
  • Depending on how easy the code is to read, you can justify empty lines between elements nested in the same level. For example, you could put empty lines between the H1, P and CANVAS elements:

    <body>
      <h1>...</h1>
    
      <p>...</p>
    
      <canvas>...</canvas>
    </body>
    

    But there is no need to add an empty line when changing levels of nesting. Note that this rule is not written down anywhere. It's just a convention I've seen over the years. HTML can look really messy, and whitespace is about the only tool we have to clean up the look. When code is too crunched together, it is difficult to read. If the code has too many blank lines it becomes difficult to see the structure. It's a balance.

In reality, there is minimal HTML code required for this game, so HTML whitespace is not tops on my list.

JavaScript

I see the biggest room for improvement in the JavaScript code, but most of my suggestions revolve around whitespace.

While putting the opening { on its own line is customary in some languages (C# comes to mind, and I've seen a good amount of C++ follow this convention as well), JavaScipt tends to follow Java code formatting conventions. Block statements should not have the { on its own line:

if (condition) {
  ...
}

switch (value) {
  ...
}

function (a, b, c) {
  ...
}

let obj = {
  ...
};

The cases for switch statements should also be indented:

switch (value) {
  case 1:
    // statements inside this case
    break;

  case 2:
    // statements inside this case
    break;

I prefer an empty line between the break ending one case and the beginning of another case, but I've seen plenty of JavaScript without it.

Missing default case

The switch statement in the drawTag function is missing a default case. Either make case 15: the default case, or add a default case to throw an error, log something to the console, or put up an alert. In this particular use case, logging to the console is probably enough.

switch (pos) {
  case 1:
  ...
  default:
    console.error('Invalid position: ' + pos);
    break;
}

Other JavaScript Improvements

  • Use const for variables you do not expect to change. The SPACE_SIZE variable is written in SHOUT_CASE, which is idiomatic for a constant, but it uses the let keyword to declare the variable. Use const instead.

  • Several variables need more meaningful names. For example, I basically cannot understand most of the code using these identifiers:

    • arrVal (I think I know what this means...)
    • arr15_02
    • arr___03
    • array (I can see this is an array, but what does it mean in the context of this application?)
    • evX
  • HTML comments intermingled with JavaScript (see the console.log statements in canvas.addEventListener). I'm sure this was a simple oversight, but use JavaScript inline // or block comments /* ... */ for commenting out JavaScript code embedded in HTML. Browsers might be a little forgiving on this one, but I wouldn't rely on it.

  • Consider declaring variables with a good name instead of if statement conditions with comments.

    For example in the mousedown event handler:

    // если нулевая плитка слева
    // if the zero tile is on the left
    if (arrVal[clickPos - 1] === 0 && clickPos !== 4
    && clickPos !== 8 && clickPos !== 12)
    

    Can become:

    const isZeroTileOnLeft = arrVal[clickPos - 1] === 0
                          && clickPos !== 4
                          && clickPos !== 8
                          && clickPos !== 12;
    
    if (isZeroTileOnLeft) {
      arrVal[clickPos - 1] = arrVal[clickPos]
      ...
    }
    

    No comments necessary here. Plus, that complex check can be formatted easily on multiple lines for readability. Fewer lines of code do not necessarily mean the code is easier to read.

  • Split big functions into smaller functions.

    The mousedown event handler has quite a bit of logic in it. I see a bunch of if statements that are meaningful somehow. Those would be good candidates for new functions with good names.

  • 2 mousedown event handlers? This is very confusing. There are two event handlers for the same event on the same element. Consider moving this into one handler and decomposing it into smaller functions. This is an extension of the previous bullet point.

  • Replace the switch in the drawTag function with an array of x/y values. The whole function can be condensed to:

    function drawTag(pos, val) {
      const cells = [
        { x: 0, y: 0 },
        { x: 100, y: 0 },
        { x: 200, y: 0 },
        { x: 300, y: 0 },
        ...
        { x: 300, y: 300 }
      ];
    
      const cell = cells[pos];
    
      if (cell) {
        drawSquare(cell.x, cell.y, val);
      }
    }
    
  • I realize this is an oversight, but shufleArray is misspelled (it's missing an "f" in shuffle).

I honestly feel bad about pointing out so many things. Most of the improvements revolve around code formatting, which is notoriously up for debate, however the way the JavaScript was written actually made it more difficult to review.

I hope this helps, and keep at it! This is a good exercise to dive into a new skillset. I look forward to seeing another rendition of this game.

\$\endgroup\$
1
  • \$\begingroup\$ I didn't expect to get such a detailed and constructive answer to my question. I read the answer carefully. Thanks to Greg Burghardt for very useful advice for me. In the future, I will write my code according to these tips. Yes, I admit that I should write my script in accordance with the generally accepted style of design. And the recommendations for creating an algorithm, I will also use in the future. \$\endgroup\$ Commented Aug 5 at 9:29
1
\$\begingroup\$

Less is better

There is a lot to learn when starting. Most of it are tricks one learns over time that help make the task of writing code less repetitive.

This answer does not address code style, rather it targets alternatives to some of the very repetitive code. The rewrite is half the size, 150 lines of JavaScript, compared your 350 lines.

Less is better when coding (I do not mean code golf).

Using constants defined at the top of the game rather than many magic numbers in the code, it is easy to make changes.

For example you can change the board size by just changing two values.

const ROWS = 4, COLS = 4;

For a 5 by 5 board

const ROWS = 5, COLS = 5;

Note: any size board above 2 by 2 will work, but I do not scale the cells so you need to ensure that the board will fit the page.

Rewrite

I have rewritten the game as an example for you.

To simplify locating cells and locating cells in directions, I have defined some constants.

Define directions

    const DIRS = {
        up:    0,
        right: 1,
        down:  2,
        left:  3,
    };
    const MOVES = [ /* BM67 to match DIRS idxs */
        {x:  0, y: -1, idxOffset: -COLS},
        {x:  1, y:  0, idxOffset:  1},
        {x:  0, y:  1, idxOffset:  COLS},
        {x: -1, y:  0, idxOffset: -1},
    ];

A direction indexes into MOVES and a move provides a vector x, y and array index offset for the direction.

For example const move = MOVES[DIRS.up] sets move to up. The object move can be used to find a cell that is up from a x,y coord, or up from a cell index.

The mouse down event gets the x, y cell index by dividing the mouse offsets by the cell size. No need for a long set of statement (is x in column 1, is x in column 2, etc)

Functions for repeated tasks.

Often the same task needs to be done in many places. This is best done with functions. If you find sections of code looking similar, it's a good sign that a function can do the task.

Helper functions check if a x, y position is on the board `function isOnBoard(x, y)'

To check if a clicked cell is near a empty cell the function function isCellEmpty(clickIdx, dirToCheck) will return true if a direction has an empty cell.

For example if clicked on the 5th cell (one down one right of top left) to check if the cell above is empty call if (isCellEmpty(5, DIRS.up)) {

Aliases for readability

Renaming a variable to make source code more readable is called aliasing.

Sometimes a name can be rather long and using it in code becomes unwieldy.

For example...

    const boardPosition = {x: 0, y: 0, w: COLS * CELL_SIZE, h: ROWS * CELL_SIZE};
    // inside a function it is used
    ctx.rect(boardPosition.x, boardPosition.y, boardPosition.w, boardPosition.h);

We don't want to lose the name in the games scope, but we don't want to get lost in code that uses the same long name over and over.

In this case we can use an alias. Defined very close to the scope and place it within a IDE page length, so if the meaning of the alias is unsure a quick glance up will see what it is

    const bP = boardPosition; /* Alias only use bP if you can see this line */
    ctx.rect(bP.x, bP.y, bP.w, bP.h);

Solvable game

Not all random layouts of the board are solvable. I did not check your shuffle code to see if it created solvable games.

In the rewrite to scramble the game the function scrammbleCells(zeroCellIdx, moveCount) plays the game using the existing (mouse) handler to make random moves. This ensures that the game is solvable.

moveCount defines how scrambled the cells will be.

Win

To check if the game is won the function isWin uses a while loop to check each cell for the correct winning state. If any cell is not the correct value the function return false.

The value in each cell must be the cell index plus one. As the last cell index 15 plus one is 16 (which is not 0) we use the remainder operator % to get the correct value at 16. (cellIdx + 1) % CELL_COUNT

const canvas = document.getElementById('canvasElement');
const ctx = canvas.getContext("2d");
const CELL_SIZE = 100;
const ROWS = 4, COLS = 4;
const CELL_COUNT = ROWS * COLS;
const PLAY_COLOR = "teal";
const WIN_COLOR = "#A54";
var cellCol = PLAY_COLOR;
const boardPosition = {x: 0, y: 0, w: COLS * CELL_SIZE, h: ROWS * CELL_SIZE};
canvas.width = boardPosition.x + boardPosition.w;
canvas.height = boardPosition.y + boardPosition.h;
var moveCount = 0;
const cells = [];
const DIRS = {
    up:    0,
    right: 1,
    down:  2,
    left:  3,
};
const MOVES = [ /* BM67 to match DIRS idxs */
    {x:  0, y: -1, idxOffset: -COLS},
    {x:  1, y:  0, idxOffset:  1},
    {x:  0, y:  1, idxOffset:  COLS},
    {x: -1, y:  0, idxOffset: -1},
];
function setFont() {
    ctx.font = "60px Arial";
    ctx.textAlign = "center";
    ctx.textBaseline = "middle";
}
function isOnBoard(x, y) {
    return (x >= 0 && x < COLS && y >= 0 && y < ROWS);
}
function isCellEmpty(idx, dir) {
    const move = MOVES[dir];
    const x = (idx % COLS) + move.x;
    const y = (idx / COLS | 0) + move.y;
    return isOnBoard(x, y) && cells[idx + move.idxOffset] === 0;
}
function isWin() {
    var cellIdx = 0;
    while (cellIdx < CELL_COUNT) {
        if (cells[cellIdx] !== (cellIdx + 1) % CELL_COUNT) {
            return false;
        }
        cellIdx ++;
    }
    return true;
}
function initBoard() { 
    var cellIdx = 0;
    while (cellIdx < CELL_COUNT) {
        cells[cellIdx] = (cellIdx + 1) % CELL_COUNT;
        cellIdx ++;
    }    
    moveCount = 0;
    cellCol = PLAY_COLOR;
    gameStateEl.textContent = "Played no moves.";
}
function scrammbleCells(zeroCellIdx, moveCount) { /* To ensure solvable scrammble with random moves */
    while (moveCount > 0) {
        const move = MOVES[Math.random() * MOVES.length | 0];
        const clickIdx = zeroCellIdx + move.idxOffset;
        if (cellClicked(clickIdx)) { zeroCellIdx = clickIdx; }
        moveCount--;
    }        
}
function drawBoardBackground() {
    ctx.beginPath();
    const bP = boardPosition; /* BM67 local alias */
    ctx.rect(bP.x, bP.y, bP.w, bP.h);
    ctx.fillStyle = "rgba(0,122,0, 0.2)";
    ctx.strokeStyle = "fuchsia";
    ctx.fill();
    ctx.beginPath();
    for (let i = 0; i <= COLS; i++) {
        ctx.moveTo(bP.x,        bP.y + i * CELL_SIZE);
        ctx.lineTo(bP.x + bP.w, bP.y + i * CELL_SIZE);
        ctx.moveTo(bP.x + i * CELL_SIZE, bP.y);
        ctx.lineTo(bP.x + i * CELL_SIZE, bP.y + bP.h);
    }
    ctx.stroke();
}
function drawCell(cellIdx) {
    const val = cells[cellIdx];
    const x = (cellIdx % COLS) * CELL_SIZE;
    const y = (cellIdx / COLS | 0) * CELL_SIZE;
    ctx.fillStyle = "blue";
    ctx.shadowColor = '#000';
    ctx.fillRect(x, y, CELL_SIZE, CELL_SIZE);
    if (val === 0) {
        ctx.fillStyle = "white";
        ctx.fillRect(x + 5, y + 5, CELL_SIZE - 10, CELL_SIZE - 10);
        return;
    }
    ctx.fillStyle = cellCol;
    ctx.shadowBlur = 4;
    ctx.shadowOffsetX = 0;
    ctx.shadowOffsetY = 2;
    ctx.fillRect(x + 5, y + 5, CELL_SIZE - 10, CELL_SIZE - 10);
    ctx.fillStyle = "white"
    ctx.fillText(val, x + CELL_SIZE * 0.5, y + CELL_SIZE * 0.5);
}
function drawGame() {
    setFont();
    var cellIdx = 0;
    ctx.clearRect(0, 0,  ctx.canvas.width, ctx.canvas.height);
    drawBoardBackground();
    while (cellIdx < CELL_COUNT) { drawCell(cellIdx ++); }    
}
function cellClicked(clickIdx) {  /* BM67 returns true if board needs redraw */
    const x = clickIdx % COLS;
    const y = clickIdx / COLS | 0;    
    if (isOnBoard(x, y)) {
        if (cells[clickIdx] !== 0) {
            let dir = 0;
            let emptyIdx;
            while (dir < MOVES.length) { 
                if (isCellEmpty(clickIdx, dir)) {
                    emptyIdx = clickIdx + MOVES[dir].idxOffset;
                    break;
                }
                dir ++;
            }
            if (dir < MOVES.length) { /* BM67 if near empty cell swap clicked and empty cells */
                [cells[clickIdx], cells[emptyIdx]] = [cells[emptyIdx], cells[clickIdx]];
                return true;
            }
        }        
    }
    return false;    
}
canvas.addEventListener("mousedown", function (e) {
    const bP = boardPosition; /* BM67 local alias */
    const x = Math.floor((e.offsetX - bP.x) / CELL_SIZE); /* 0 - 3 on board cell */
    const y = Math.floor((e.offsetY - bP.y) / CELL_SIZE); /* 0 - 3 on board cell */
    if (cellClicked(x + y * COLS)) { 
        moveCount += 1;
        if (isWin()) {
            gameStateEl.textContent = "Winner in " + moveCount + " moves.";
            cellCol = WIN_COLOR;
        } else {
            gameStateEl.textContent = "Played " + moveCount + " moves.";
            cellCol = PLAY_COLOR;
        }
        drawGame(); 
    }
});

initBoard();
scrammbleCells(CELL_COUNT - 1, 300);
drawGame();
body {
  text-align: center;
}
#canvas {
  border: 2px solid green;
  background-color: lightblue;  
}
<h1>javascript Canvas 15taggame</h1>
<p id="gameStateEl"></p>
<canvas id="canvasElement"></canvas>

\$\endgroup\$
1
  • \$\begingroup\$ Thanks to Blindman67 for valuable advice. I carefully read your detailed and useful for me answer. You even wrote your version of the game code. Thank you. I will use it as a template. As a generally accepted design style and as a sample for building an algorithm for other games. \$\endgroup\$ Commented Aug 5 at 9:31

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.