<!-- main -->
<main class="wrapper">
<!-- intro -->
<div class="intro clearfix">
<h1 class="intro_title"> 2048 </h1>
</div>
<!-- /end intro -->
<!-- controls -->
<div class="controls clearfix">
<div class="controls_game">
<button data-js="newGame" class="controls_game-btn"> New Game </button>
</div>
<div class="controls_score">
<span class="controls_score-label">Score </span>
<br>
<span class="controls_score-text" data-js="score"> </span>
</div>
</div>
<!-- gameboard -->
<div id="touchGameboard" class="gameboard">
<div class="grid">
</div>
<div class="tile-container">
</div>
</div>
<!-- /end gameboard -->
<!-- guide -->
<section class="guide clearfix ">
<h2> What is this? </h2>
<p> Although coded entirely from scratch, this game is a (lackluster) copy of Gabriele Cirulli's 2048, http://2048game.com/.
</p>
</section>
<section class="guide clearfix">
<h2> How do I play? </h2>
<p> Tiles with matching number values can be merged into a single tile, which receives the values' sum.
</p>
<p> To move the board, use the directional arrows - or swipe.</p>
<div class="guide_arrow-wrap">
<span class="guide_arrow"> ⥣ </span>
<span class="guide_arrow"> ⥢ </span>
<span class="guide_arrow"> ⥤ </span>
<span class="guide_arrow"> ⥥ </span>
</div>
<p> To win, get a 2048 tile.
</p>
</section>
<!-- /end guide -->
</main>
<!-- /end main -->
<!-- templates -->
<script type="text/html" id="template_grid_cell">
<div class="grid_cell"></div>
</script>
<script type="text/html" id="template_tile">
<div class="tile">
<span class="tile_number"> </span>
</div>
</script>
<!-- /end templates -->
/*
Variables:
*/
$grid-max-width: 500px;
$grid-padding: 8px;
$grid-border-radius: 5px;
// white
$color-background: #f2efea;
$color-accent1: #f9d49a;
$color-accent2: #d4a8cf;
$color-list: #00d0a4, #dd7373, #7d53de, #6622cc, #00bfb2, #c06ff2, #340068,
#3e92cc, #d8315b, #1c0b19, #1c0b19;
/**/
*,
*:before,
*:after {
box-sizing: border-box;
}
button,
a {
&:hover {
cursor: pointer;
}
}
.clearfix::after {
content: "";
display: block;
clear: both;
}
html {
min-height: 100%;
width: 100%;
font-size: 16px;
font-family: "Rubik", sans-serif;
line-height: 1.5em;
color: #fff;
background: #160140;
background: linear-gradient(to top, #160140, #261535);
}
.wrapper {
max-width: $grid-max-width;
margin: 0 auto;
padding: 15px;
}
h2 {
font-style: italic;
}
/* Introduction */
.intro {
margin-bottom: 60px;
&_title {
text-align: center;
color: $color-accent1;
font-size: 3rem;
}
}
/**/
/* Guide instructions */
.guide {
border-bottom: 1px solid grey;
&:first-of-type {
margin-top: 4rem;
border-top: 1px solid gray;
}
&_arrow {
display: inline-block;
margin: 15px;
font-size: 3rem;
color: #fff;
}
}
.controls {
&_game,
&_score {
display: inline-block;
width: 50%;
float: left;
@media all and (max-width: 767px) {
width: 100%;
}
}
&_game-btn {
margin-bottom: 1rem;
padding: 0.5em 0.75em;
background: transparent;
color: #f9d49a;
outline: 2px solid #f9d49a;
appearance: none;
border: 5px solid transparent;
box-shadow: inset 0 0 0px 2px #d4a8cf;
letter-spacing: 0.1em;
font-weight: bold;
text-transform: lowercase;
}
&_score {
display: inline-block;
min-width: 4em;
margin-bottom: 4rem;
padding: 0.5em 0.75em;
background: #0000003b;
text-align: center;
background: linear-gradient(90deg, #f9d49a, #d4a8cf);
&-label,
&-text {
display: inline-block;
}
&-label {
color: initial;
}
&-text {
color: #4a3647;
font-size: 2rem;
}
}
}
/**/
/*
Gameboard:
the container for the static grid background; and generated tiles/numbers;
*/
.gameboard {
/* Position: relative; set for tile-container, which absolutely positions over it to match grid's dimensions; */
position: relative;
width: 100%;
max-width: 500px;
height: 100%;
max-height: 500px;
margin: auto;
padding: $grid-padding;
background: #ffffff08;
border-radius: $grid-border-radius;
box-shadow: 0 0 8px 0px $color-accent1;
&::before {
content: "";
display: block;
padding-bottom: 100%;
}
}
/**/
/*
Grid:
Creates the static grid background and individual grid cells;
*/
.grid {
width: 100%;
height: 100%;
position: absolute;
top: 0;
bottom: 0;
right: 0;
left: 0;
margin: auto;
&_cell {
display: inline-block;
height: 25%;
width: 25%;
padding: $grid-padding;
float: left;
background: rgba(238, 228, 218, 0.35);
background-clip: content-box;
}
}
/**/
/*
Tile container:
Contains the dynamically-generated tiles;
absolutely positioned over gameboard to match grid dimensions;
*/
.tile-container {
/* absolutely positioned over gameboard to match dimensions */
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
margin: auto;
border-radius: $grid-border-radius;
}
.tile {
@extend .grid_cell; //display:table is used to vertically align number
display: table;
background: #eee4da;
background-clip: content-box;
position: absolute;
z-index: 2;
will-change: top, left;
transition-property: top, left;
transition-duration: 0.175s;
transition-timing-function: ease-out;
&.initialize {
animation-name: newTile;
animation-duration: 0.175s;
animation-timing-function: linear;
animation-fill-mode: forwards;
}
@keyframes newTile {
0% {
opacity: 0;
}
50% {
opacity: 0;
transform: scale(0);
}
75% {
opacity: 1;
transform: scale(0.5);
}
100% {
opacity: 1;
transform: scale(1);
}
}
&_number {
display: table-cell;
vertical-align: middle;
text-align: center;
font-size: 2rem;
font-weight: bold;
color: white;
}
}
@for $g from 1 through 16 {
$h: $g + 1;
.tile:nth-of-type(#{$g}) {
z-index: $h;
}
}
@for $i from 0 through 4 {
@for $j from 0 through 4 {
$convertX: $i * (100 / 4);
$convertXstring: unquote("#{$convertX}" + "%");
$convertY: $j * (100 / 4);
$convertYstring: unquote("#{$convertY}" + "%");
.tile[data-x="#{$convertX}"][data-y="#{$convertY}"] {
top: $convertYstring;
left: $convertXstring;
}
}
}
$i: 2;
$listCounter: 1;
// increment by * 2 until 2048
@while $i <=2048 {
.tile_number[data-value="#{$i}"] {
background: nth($color-list, $listCounter);
color: #fff;
box-shadow: 0 0 1px 1px nth($color-list, $listCounter);
}
$i: $i * 2;
$listCounter: $listCounter+1;
}
View Compiled
/**
* TODO:
* - Style win/lose, move out of "alert"
* - Add in previous high score via localstorage
* - Update footer
*/
/*
* Dependencies:
* Lodash, jQuery, hammerjs
*/
function gameStart() {
window.game = new Game(4);
window.game.initialize();
}
$(document).ready(gameStart);
/*
* Game Board
*/
function Game(size) {
this.rows = size;
this.columns = size;
// board is set as 2d array, with grid cell object for each position
this.board = [];
this.boardFlatten = function() {
return _.flatten(this.board);
};
//
// score setup
this.score = 0;
$('[data-js="score"]').html(this.score.toString());
//
// flag to check whether any tile movement is in progress;
this.moveInProgress = false;
//
}
/**
* Run all initializations
*/
Game.prototype.initialize = function() {
// clear any previous grid; per jQuery docs, empty also removes event listeners
$(".grid").empty();
$(".tile-container").empty();
//
// run new setup
this.initBoard();
this.initTile();
this.initEventListeners();
//
};
/**/
/**
* Initialize grid
*/
Game.prototype.initBoard = function() {
// return grid cell object
function initGridCell(x, y) {
var getGridCell = $.parseHTML($("#template_grid_cell").html());
$(getGridCell).appendTo(".grid");
return {
x: x,
y: y,
tilesArray: []
};
}
//
// create 2d array and push grid cell object
for (var x = 0; x < this.rows; x++) {
var newArray = [];
this.board.push(newArray);
for (var y = 0; y < this.columns; y++) {
var gridObj = initGridCell(x, y);
var rowCell = this.board[x];
rowCell.push(gridObj);
}
}
//
};
/**
* Initialize tiles
*/
Game.prototype.initTile = function() {
// isGameOver determines whether the game is finished; needs to be run: before and after creating tile
this.isGameOver();
//
var emptyCell = this.getRandomEmptyCell();
var tile = new Tile(emptyCell.x, emptyCell.y, game);
// isGameOver determines whether the game is finished; needs to be run: before and after creating tile
this.isGameOver();
//
};
/**/
/**
* Set event listeners
*/
Game.prototype.initEventListeners = function() {
var self = this;
var getGameboard = document.getElementById("touchGameboard");
/*
Touch events with Hammerjs
*/
window.hammertime && window.hammertime.destroy();
window.hammertime = new Hammer(getGameboard, {
recognizers: [[Hammer.Swipe, { direction: Hammer.DIRECTION_ALL }]]
});
window.hammertime
.on("swipeleft", function(ev) {
self.move("left");
})
.on("swiperight", function(ev) {
self.move("right");
})
.on("swipedown", function(ev) {
self.move("down");
})
.on("swipeup", function(ev) {
self.move("up");
});
/**/
/*
NOTE: Remove event listeners before applying new listeners,
because this initialization runs each time a new game is created
*/
// keypress events for up, down, left, right
$(document)
.off("keydown.move")
.on("keydown.move", function(event) {
event.preventDefault();
switch (event.which) {
// left
case 37:
self.move("left");
break;
// up
case 38:
self.move("up");
break;
// right
case 39:
self.move("right");
break;
// down
case 40:
self.move("down");
break;
}
});
//
// New game click handler
$('[data-js="newGame"]')
.off("click.newGame")
.on("click.newGame", window.gameStart);
//
};
/**/
/**
* Game is WON!
*/
Game.prototype.gameWon = function() {
alert("you won");
};
/**/
/**
* Game is LOST!
*/
Game.prototype.gameLost = function() {
alert("what a loser!");
};
/**/
/**
* Check if game over
*/
Game.prototype.isGameOver = function() {
var gameBoard = this.boardFlatten();
var is2048 = false;
var canAnyTileMove = false;
var hasEmptyCells = false;
// check if 2048
gameBoard.forEach(function(val, index, array) {
val.tilesArray.forEach(function(val, index, array) {
if (val.valueProp === 2048) {
is2048 = true;
}
});
});
// check if there are empty cells
if (this.getEmptyCells().length > 0) {
hasEmptyCells = true;
}
// Check if move possible
gameBoard.forEach(function(val, index, array) {
val.tilesArray.forEach(function(val, index, array) {
val.moveCheck();
if (val.canMove === true) {
canAnyTileMove = true;
}
});
});
// if game won
if (is2048) {
this.gameWon();
return true;
} else if (!hasEmptyCells && !canAnyTileMove) {
// if no empty cells || no tile can move, the game is lost
this.gameLost();
return true;
} else {
// if there is an empty || a tile can move, return false for isGameOver
return false;
}
//
};
/**
* Get empty cells
*/
Game.prototype.getEmptyCells = function() {
var emptyCells = _.filter(this.boardFlatten(), function(val) {
return !val.tilesArray.length;
});
return emptyCells;
};
/**/
/**
* Return random empty cell for new tile creation
*/
Game.prototype.getRandomEmptyCell = function() {
var emptyGridCells = this.getEmptyCells();
var randomIndex = Math.floor(
Math.random() * Math.floor(emptyGridCells.length)
);
return emptyGridCells[randomIndex];
};
/**/
/**
* Merge tiles
*/
Game.prototype.TileMerge = function() {
var gameBoard = this.boardFlatten();
var newScore = this.score;
// loop through all tiles
gameBoard.forEach(function(val, index, array) {
if (val.tilesArray.length === 2) {
// get current value of 1st tile
var currentValue = val.tilesArray[0].valueProp;
// update value
val.tilesArray[0].value = currentValue * 2;
// remove 2nd tile
var x = val.tilesArray.pop();
x.el.remove();
// update score
newScore += currentValue;
}
});
// update game score at the end
this.score = newScore;
$('[data-js="score"]').html(this.score.toString());
};
/**/
/**
* Move animations
*/
Game.prototype.moveAnimations = function(gameBoard) {
var self = this;
var promiseArray = [];
if (this.moveInProgress) {
return false;
}
this.moveInProgress = true;
gameBoard.forEach(function(val, index, array) {
val.tilesArray.forEach(function(val, index, array) {
promiseArray.push(val.animatePosition());
});
});
$.when.apply($, promiseArray).then(function() {
self.moveInProgress = false;
self.TileMerge();
self.initTile();
});
if (promiseArray.length === 0) {
self.moveInProgress = false;
self.TileMerge();
self.initTile();
}
};
/**/
/**
* Move logic
*/
Game.prototype.move = function(getDirection) {
var gameBoard;
// direction passed as argument
var direction = getDirection.toLowerCase();
//
// flag to check whether any
var hasAnyTileMoved = false;
//
if (this.moveInProgress) {
return false;
}
// if UP:
if (direction === "up") {
gameBoard = _.orderBy(this.boardFlatten(), "y", "asc");
} else if (direction === "right") {
// if RIGHT:
gameBoard = _.orderBy(this.boardFlatten(), "x", "desc");
} else if (direction === "down") {
// if DOWN
gameBoard = _.orderBy(this.boardFlatten(), "y", "desc");
} else if (direction === "left") {
// if LEFT
gameBoard = _.orderBy(this.boardFlatten(), "y", "asc");
}
// loop through all tiles and run tile move foreach
//
gameBoard.forEach(function(val, index, array) {
val.tilesArray.length
? val.tilesArray.forEach(function(val) {
if (val.move(direction, true)) {
hasAnyTileMoved = true;
val.move(direction);
}
})
: false;
});
//
// run animation logic at the end
hasAnyTileMoved ? this.moveAnimations(gameBoard) : false;
};
/**/
/*
* Tile
*/
function Tile(x, y, game) {
this.game = game;
// jQuery element
this.el;
// current position
this.x = x;
this.y = y;
// Getter/Setter for value; updates html on set; defaulted to 2 on creation
this.valueProp = 2;
Object.defineProperties(this, {
value: {
get: function() {
return this.valueProp;
},
set: function(val) {
this.valueProp = val;
this.el
.find(".tile_number")
.html(this.valueProp)
.attr("data-value", val);
}
}
});
// can move flag
this.canMove = false;
// initialize
this.initialize();
}
/**
* Initialize
*/
Tile.prototype.initialize = function() {
// Get html from template and set number text
var getTile = $.parseHTML($("#template_tile").html());
this.el = $(getTile);
this.el
.find(".tile_number")
.html(this.valueProp)
.attr("data-value", 2);
// Set position and append to page; initializeFlag is set to True to remove animation and append immediately in correct position
this.setPosition(this.x, this.y);
this.animatePosition(true);
this.el.appendTo(".tile-container");
};
/**/
/**
* Set new position
*/
Tile.prototype.setPosition = function(getX, getY) {
this.x = getX;
this.y = getY;
this.game.board[getX][getY].tilesArray.push(this);
};
/**/
/**
* Remove old position
*/
Tile.prototype.removeOldPosition = function(getX, getY) {
this.game.board[getX][getY].tilesArray.pop();
};
/**/
/**
* Animate to position
*/
Tile.prototype.animatePosition = function(initalizeFlag) {
var self = this;
var fromLeft = this.x * (100 / this.game.rows);
var fromTop = this.y * (100 / this.game.columns);
// Initialize flag sets animationDuration to 0 to append immediately in correct position
var animationDuration = 175;
var getPromise = $.Deferred();
if (initalizeFlag) {
this.el.addClass("initialize");
} else {
this.el.removeClass("initialize");
}
function resolvePromise() {
getPromise.resolve();
self.el.removeClass("animate");
self.el.removeClass("initialize");
}
function setPosition() {
self.el.addClass("animate");
self.el.attr({
"data-x": fromLeft,
"data-y": fromTop
});
}
if (initalizeFlag) {
setPosition();
window.setTimeout(resolvePromise, animationDuration + 50);
} else {
setPosition();
window.setTimeout(resolvePromise, animationDuration);
}
return getPromise;
};
/**/
/**
* Check if move is possible
*/
Tile.prototype.moveCheck = function() {
// run all checks; return true if any moves are possible
if (
this.move("up", true) ||
this.move("right", true) ||
this.move("down", true) ||
this.move("left", true)
) {
this.canMove = true;
return true;
} else {
this.canMove = false;
return false;
}
};
/**/
/**
* Move logic
*/
Tile.prototype.move = function(getDirection, checkFlag) {
var checkFlag = checkFlag ? true : false;
var direction = getDirection.toLowerCase();
var getX = this.x;
var getY = this.y;
var getNext;
var isNextMatch;
var isNextEmpty;
var nextPositionArray = [];
// if UP: check next position
if (direction === "up") {
getNext = this.y > 0 ? this.game.board[this.x][this.y - 1] : false;
nextPositionArray.push(this.x, this.y - 1);
} else if (direction === "right") {
// if RIGHT: check next position
getNext = this.x < 3 ? this.game.board[this.x + 1][this.y] : false;
nextPositionArray.push(this.x + 1, this.y);
} else if (direction === "down") {
// if DOWN: check next position
getNext = this.y < 3 ? this.game.board[this.x][this.y + 1] : false;
nextPositionArray.push(this.x, this.y + 1);
} else if (direction === "left") {
// if LEFT: check next position
getNext = this.x > 0 ? this.game.board[this.x - 1][this.y] : false;
nextPositionArray.push(this.x - 1, this.y);
}
// Check if next position contains match or is empty
isNextMatch =
getNext &&
getNext.tilesArray.length === 1 &&
getNext.tilesArray[0].valueProp === this.valueProp;
isNextEmpty = getNext && getNext.tilesArray.length === 0;
//
// "check only" mode; only to check if tile can move
if (checkFlag) {
return isNextEmpty || isNextMatch ? true : false;
} else if (isNextEmpty || isNextMatch) {
// not "check only" mode; will actually run move logic
this.setPosition(nextPositionArray[0], nextPositionArray[1]);
this.removeOldPosition(getX, getY);
// do NOT continue to move if a tile has matched - and therefore MERGED into adjoining tile
if (!isNextMatch) {
this.move(direction);
}
}
};
/**/
View Compiled
This Pen doesn't use any external CSS resources.