The game is a puzzle game made up of a grid of cells represented by html divs. Gameplay is as follows:
- On starting a new game, the number of rows and columns is selected from a form.
- The grid is created with all but one of the cells being initially filled.
- A filled cell is selected by clicking and moved to the empty cell while "jumping" over another filled cell.
- The cell that was jumped over is now emptied.
- 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>