4
\$\begingroup\$

The game is a puzzle game made up of a grid of cells represented by html divs. Gameplay is as follows:

  1. On starting a new game, the number of rows and columns is selected from a form.
  2. The grid is created with all but one of the cells being initially filled.
  3. A filled cell is selected by clicking and moved to the empty cell while "jumping" over another filled cell.
  4. The cell that was jumped over is now emptied.
  5. The objective of the game is to remain with only one cell filled.

I usually code in PHP so most of this was done with Google's help. How can it be improved to fit JavaScript standards and best practices?

var gameArea = $('#game-area');

function Cell(row, col) {
  this.row = row;
  this.col = col;
  this.element = null;

  Cell.filledClass = 'filled';
  Cell.selectedClass = 'selected';
  Cell.itemClass = 'item';

  this.fill = function() {
    this.element.addClass(Cell.filledClass);
    return this;
  };

  this.empty = function() {
    this.element.removeClass(Cell.filledClass);
    return this;
  };

  this.isFilled = function() {
    return this.element.hasClass(Cell.filledClass);
  }

  this.select = function() {
    this.element.addClass(Cell.selectedClass);
    return this;
  };

  this.unselect = function() {
    this.element.removeClass(Cell.selectedClass);
    return this;
  };

  this.isSelected = function() {
    return this.element.hasClass(Cell.selectedClass);
  }

  this.init = function() {
    this.element = $('<div></div>')
      .data({
        'row': this.row,
        'col': this.col
      })
      .attr({
        'id': 'item-' + this.row + '-' + this.col,
        'class': Cell.itemClass + ' ' + Cell.filledClass
      });
    gameArea.append(this.element);
    return this;
  };
}

function Game(rows, cols) {
  this.rows = rows;
  this.cols = cols;
  this.cells = [];
  this.currentCell = null;

  this.init = function() {
    gameArea.html('');
    for (var row = 1; row <= this.rows; row++) {
      var r = [];
      for (var col = 1; col <= this.cols; col++) {
        r[col - 1] = new Cell(row, col).init();
      }
      this.cells[row - 1] = r;
      gameArea.append('<br/>');
    }

    this.getCell(Math.ceil(Math.random() * this.rows), Math.ceil(Math.random() * this.cols)).empty();

    $("." + Cell.itemClass, gameArea).on('click', {
      'game': this
    }, clickHandler);
  };

  this.getCell = function(row, col) {
    return this.cells[row - 1][col - 1];
  }

  this.filledCellsLeft = function() {
    var total = 0;
    for (row = 1; row <= this.rows; row++) {
      for (col = 1; col <= this.cols; col++) {
        if (this.getCell(row, col).isFilled()) {
          total++;
        };
      }
    }
    return total;
  };

  this.hasMovesLeft = function() {
    for (row = 1; row <= this.rows; row++) {
      for (col = 1; col <= this.cols; col++) {
        if (this.getCell(row, col).isFilled()) {
          if ((col > 1 && this.getCell(row, col - 1).isFilled()) // check left
            || (col < this.cols && this.getCell(row, col + 1).isFilled()) // check right
            || (row > 1 && this.getCell(row - 1, col).isFilled()) // check top
            || (row < this.rows && this.getCell(row + 1, col).isFilled())) { // check bottom
            return true;
          }
        }
      };
    }
    return false;
  }
}

function clickHandler(event) {
  game = event.data.game;
  var cell = game.getCell($(this).data('row'), $(this).data('col'));

  if (cell.isFilled()) {
    if (!game.currentCell) {
      cell.select();
      game.currentCell = cell;
    } else {
      game.currentCell.unselect();
      game.currentCell = null;
    }
  } else if (game.currentCell) {
    var rowDiff = Math.abs(game.currentCell.row - cell.row),
      colDiff = Math.abs(game.currentCell.col - cell.col);
    if ((colDiff === 0 || colDiff === 2) 
        && (rowDiff === 0 || rowDiff === 2) 
        && ((colDiff === 2 && rowDiff === 0) || (colDiff === 0 && rowDiff === 2))) 
    {
      var over = game.getCell((game.currentCell.row + cell.row) / 2, (game.currentCell.col + cell.col) / 2);
      if (over.isFilled()) {
        cell.fill();
        over.empty();
        game.currentCell.empty().unselect();

        if (game.filledCellsLeft() == 1) {
          alert(':) You Win!!');
        } else if (!game.hasMovesLeft()) {
          // You lose!
          alert(':( No moves left. You lose.');
        }
      }
    }
  }
}

$(function() {
  var game = new Game(4, 4);
  game.init();
  $('#reset-form').on("submit", function(event) {
    new Game(parseInt($("#rows").val()), parseInt($("#cols").val())).init();
    $("#game-area").show();
    $("#form, #how-to").hide();
    event.preventDefault();
  });

  $('#new-game').on('click', function(event) {
    $("#form").show();
    $("#game-area, #how-to").hide();
  });

  $('#rules').on('click', function(event) {
    $("#how-to").show();
    $("#game-area, #form").hide();
  });
});

var gameArea = $('#game-area');

function Cell(row, col) {
  this.row = row;
  this.col = col;
  this.element = null;

  Cell.filledClass = 'filled';
  Cell.selectedClass = 'selected';
  Cell.itemClass = 'item';

  this.fill = function() {
    this.element.addClass(Cell.filledClass);
    return this;
  };

  this.empty = function() {
    this.element.removeClass(Cell.filledClass);
    return this;
  };

  this.isFilled = function() {
    return this.element.hasClass(Cell.filledClass);
  }

  this.select = function() {
    this.element.addClass(Cell.selectedClass);
    return this;
  };

  this.unselect = function() {
    this.element.removeClass(Cell.selectedClass);
    return this;
  };

  this.isSelected = function() {
    return this.element.hasClass(Cell.selectedClass);
  }

  this.init = function() {
    this.element = $('<div></div>')
      .data({
        'row': this.row,
        'col': this.col
      })
      .attr({
        'id': 'item-' + this.row + '-' + this.col,
        'class': Cell.itemClass + ' ' + Cell.filledClass
      });
    gameArea.append(this.element);
    return this;
  };
}

function Game(rows, cols) {
  this.rows = rows;
  this.cols = cols;
  this.cells = [];
  this.currentCell = null;

  this.init = function() {
    gameArea.html('');
    for (var row = 1; row <= this.rows; row++) {
      var r = [];
      for (var col = 1; col <= this.cols; col++) {
        r[col - 1] = new Cell(row, col).init();
      }
      this.cells[row - 1] = r;
      gameArea.append('<br/>');
    }

    this.getCell(Math.ceil(Math.random() * this.rows), Math.ceil(Math.random() * this.cols)).empty();

    $("." + Cell.itemClass, gameArea).on('click', {
      'game': this
    }, clickHandler);
  };

  this.getCell = function(row, col) {
    return this.cells[row - 1][col - 1];
  }

  this.filledCellsLeft = function() {
    var total = 0;
    for (row = 1; row <= this.rows; row++) {
      for (col = 1; col <= this.cols; col++) {
        if (this.getCell(row, col).isFilled()) {
          total++;
        };
      }
    }
    return total;
  };

  this.hasMovesLeft = function() {
    for (row = 1; row <= this.rows; row++) {
      for (col = 1; col <= this.cols; col++) {
        if (this.getCell(row, col).isFilled()) {
          if ((col > 1 && this.getCell(row, col - 1).isFilled()) // check left
            || (col < this.cols && this.getCell(row, col + 1).isFilled()) // check right
            || (row > 1 && this.getCell(row - 1, col).isFilled()) // check top
            || (row < this.rows && this.getCell(row + 1, col).isFilled())) { // check bottom
            return true;
          }
        }
      };
    }
    return false;
  }
}

function clickHandler(event) {
  game = event.data.game;
  var cell = game.getCell($(this).data('row'), $(this).data('col'));

  if (cell.isFilled()) {
    if (!game.currentCell) {
      cell.select();
      game.currentCell = cell;
    } else {
      game.currentCell.unselect();
      game.currentCell = null;
    }
  } else if (game.currentCell) {
    var rowDiff = Math.abs(game.currentCell.row - cell.row),
      colDiff = Math.abs(game.currentCell.col - cell.col);
    if ((colDiff === 0 || colDiff === 2) 
        && (rowDiff === 0 || rowDiff === 2) 
        && ((colDiff === 2 && rowDiff === 0) || (colDiff === 0 && rowDiff === 2))) 
    {
      var over = game.getCell((game.currentCell.row + cell.row) / 2, (game.currentCell.col + cell.col) / 2);
      if (over.isFilled()) {
        cell.fill();
        over.empty();
        game.currentCell.empty().unselect();

        if (game.filledCellsLeft() == 1) {
          alert(':) You Win!!');
        } else if (!game.hasMovesLeft()) {
          // You lose!
          alert(':( No moves left. You lose.');
        }
      }
    }
  }
}

$(function() {
  var game = new Game(4, 4);
  game.init();
  $('#reset-form').on("submit", function(event) {
    new Game(parseInt($("#rows").val()), parseInt($("#cols").val())).init();
    $("#game-area").show();
    $("#form, #how-to").hide();
    event.preventDefault();
  });

  $('#new-game').on('click', function(event) {
    $("#form").show();
    $("#game-area, #how-to").hide();
  });

  $('#rules').on('click', function(event) {
    $("#how-to").show();
    $("#game-area, #form").hide();
  });
});
body {
  background: #eee;
  text-align: center;
  font-family: "monospace";
  font-size: 16px;
  color: white;
  background: #024555;
}
a,
a:visited,
a:hover,
a:focus,
a:link {
  color: #A7A300;
  text-decoration: none;
  cursor: pointer;
}
h1,
h2 {
  font-size: 3em;
  font-weight: normal;
}
h2 {
  font-size: 2em;
}
div#game-area {
  margin: auto;
}
div#how-to,
div#form {
  display: none;
}
div.item {
  height: 40px;
  width: 40px;
  display: inline-block;
  border: 2px solid #888;
  margin: 5px;
  border-radius: 50%;
  cursor: pointer;
  transition: all 0.5s ease;
}
div.item.filled {
  background: #009688;
}
div.item.selected {
  background: #A7A300;
}
select,
input {
  border: 2px solid #AAA;
  font-family: "monospace";
  padding: 5px;
  font-size: 20px;
  text-align: center;
  width: 50px;
}
input[type="submit"] {
  width: auto;
  background: #A7A300;
  cursor: pointer;
}
#how-to {
  max-width: 767px;
  margin: auto;
}
#how-to span.item {
  height: 12px;
  width: 12px;
  margin: 0;
  display: inline-block;
  border: 2px solid #888;
  border-radius: 50%;
}
#how-to span.item.filled {
  background: #009688;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>

<h1>Last Dot Standing</h1>
<div><a id="new-game">New Game</a> | <a id="rules">Rules</a> | <a target="_blank" href="https://github.com/kanyuga/last-dot-standing">source</a>
</div>
<div id="how-to">
  <h2>How to Play</h2>
  <p>Select a dot <span class="item filled"></span> and an empty slot <span class="item"></span> to jump to.</p>
  <p>Only horizontal or vertical jumps are allowed.</p>
  <p>In the process you must jump over a single dot which will disappear like magic (Disclaimer: <em>not real magic</em>).</p>
  <p>The aim of the game is to only have the last dot standing.</p>
</div>
<div id="form" class="form">
  <h2>New Game</h2>
  <form id="reset-form">
    <p>Select the Size of the Grid</p>
    <br>
    <select id="cols" name="cols" value="4">
      <option value="4" selected="selected">4</option>
      <option value="5">5</option>
      <option value="6">6</option>
      <option value="7">7</option>
      <option value="8">8</option>
    </select>
    x
    <select id="rows" name="rows" value="4">
      <option value="4" selected="selected">4</option>
      <option value="5">5</option>
      <option value="6">6</option>
      <option value="7">7</option>
      <option value="8">8</option>
    </select>
    <input value="Go" type="submit">
  </form>
</div>
<div id="game-area">
  <div class="item filled" id="item-1-1"></div>
  <div class="item filled" id="item-1-2"></div>
  <div class="item filled" id="item-1-3"></div>
  <div class="item" id="item-1-4"></div>
  <br>
  <div class="item filled" id="item-2-1"></div>
  <div class="item filled" id="item-2-2"></div>
  <div class="item filled" id="item-2-3"></div>
  <div class="item filled" id="item-2-4"></div>
  <br>
  <div class="item filled" id="item-3-1"></div>
  <div class="item filled" id="item-3-2"></div>
  <div class="item filled" id="item-3-3"></div>
  <div class="item filled" id="item-3-4"></div>
  <br>
  <div class="item filled" id="item-4-1"></div>
  <div class="item filled" id="item-4-2"></div>
  <div class="item filled" id="item-4-3"></div>
  <div class="item filled" id="item-4-4"></div>
  <br>
</div>

\$\endgroup\$
1
  • 1
    \$\begingroup\$ I don't have any comment on the code, but having played the game in Vivaldi browser, launching it through the Code Review link at the bottom of your post, the game seems to run well, but I had difficulty selecting the dot to jump every time. I basically had to click it twice each time to select it. You might see if you can replicate the issue and apply a fix \$\endgroup\$
    – Zack
    Commented Feb 26, 2016 at 1:11

1 Answer 1

2
\$\begingroup\$

Good clean code with good object structure; you made a good transition from php objects to js. There are a few tiny things though to do with consistency you might want to clean up.

You don't have semi-colons after all of your object functions. JavaScript isn't all that picky about them but good practice is to be consistent in whichever method you use. This is especially important in js since not all browsers parse in the same way. By being consistent you'll ensure your code either works or it doesn't, and you won't have to track down elusive bugs.

You don't always declare your 'for' loop variables using the 'var' keyword. Again not critical but it would be best to be consistent.

\$\endgroup\$

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.