Skip to content

Instantly share code, notes, and snippets.

@chrisrzhou
Last active August 29, 2015 14:11
Show Gist options
  • Save chrisrzhou/23ad9e21d11587df37a1 to your computer and use it in GitHub Desktop.
Save chrisrzhou/23ad9e21d11587df37a1 to your computer and use it in GitHub Desktop.
ng2048

ng2048

bl.ocks.org link

This is an AngularJS clone of the original 2048 game.

Some code guidance obtained from ng-2048 with different implementations on merge and keypress functions.


Notes

  • Game is based off manipulating a tile array of values i.e. [0, 2, 2, 0, ...., 0] where indices of the array represent the tile ID.

  • Animations done using ngAnimate.

  • Game is mainly governed by the Grid module in grid.js and Game module in game.js.

  • Grid module controls directive for displaying tiles, as well as methods to setTile, getTile, setEmptyTile, copy, merge.

  • Game module contains the logic for ngKeydown for the merge and score update functions.

Analytics

(function() {
angular.module("ng2048", ["ngAnimate", "Game", "Grid"])
.controller("GameCtrl", GameCtrl);
// Controller
function GameCtrl(GameService, GridService) {
// Assign controller variables
ctrl = this;
// Initialize game
init();
// Controller functions
function init() {
/* Initialize */
ctrl.game = GameService.game;
ctrl.game.newGame();
}
}
})();
(function() {
angular.module("Game", [])
.service("GameService", GameService);
// Service
function GameService(GridService) {
var grid = GridService.grid;
// Service variables
this.game = {
directions: { // define direction keymap for merge function
37: {
tileIncrement: 1,
lineIncrement: grid.dim,
reverse: false
}, // left
38: {
tileIncrement: grid.dim,
lineIncrement: 1,
reverse: false
}, // up
39: {
tileIncrement: 1,
lineIncrement: grid.dim,
reverse: true
}, // right
40: {
tileIncrement: grid.dim,
lineIncrement: 1,
reverse: true
} // down
},
state: {
win: false,
gameover: false,
continue: false
},
moves: 0,
score: {
merge: 0,
current: 0,
best: 0,
spm: 0
},
grid: grid,
newGame: newGame,
continueGame: continueGame,
update: update,
merge: merge,
updateScore: updateScore,
isGameOver: isGameOver
};
// Service functions
function newGame() {
/* Start a new game */
this.state = {
win: false,
gameover: false,
continue: false
};
this.moves = 0;
this.score.merge = 0;
this.score.current = 0;
this.score.spm = 0;
this.grid.newTiles();
}
function continueGame() {
this.state.
continue = true;
}
function update(event) {
/* Update event based on direction key pressed */
event.preventDefault();
if (!this.state.gameover && (!this.state.win || this.state.
continue)) {
var direction = this.directions[event.which];
if (direction) { // if valid direction is in directions
var tilesCopy = this.grid.copyTiles(); // make a deep copy of grid
this.moves++; // update moves
this.score.merge = this.merge(direction, updateTiles = true); // merge grid based on direction pressed
this.updateScore(); // update score
if (!this.grid.identicalTiles(tilesCopy)) { // if old gridCopy is not identical to the new grid, generate a new tile
this.grid.setEmptyTile();
}
this.isGameOver(); // check if game is over
}
}
}
function merge(direction, updateTiles) {
/* Merge grid tiles based on direction using the directions matrix
The merge function will perform three main phases:
- Obtain the position of tiles for each line in the grid (number of lines = grid.dim) based on the direction matrix provided above.
- For each line, take only the non-zero tiles, which allows for a simpler merge step on non-zero tiles.
- Append the zeros to the front or back of the line depending on the direction selected (i.e. based on the value of direction.reverse).
- Update the old grid values to the new merged values.
- Update score and check win condition
- Return mergeScore
*/
// For each line in a grid, get positional tiles depending on direction pressed.
var i = 0;
var mergeScore = 0;
while (i < this.grid.dim) {
var k = 0;
var positionTiles = [];
var positionTile = i * direction.lineIncrement; // set up line increment
while (k < this.grid.dim) {
positionTiles.push(positionTile);
positionTile += direction.tileIncrement; // set up tile increment
k++;
}
// Get non-zero tiles from positional tiles
var mergedTiles = [];
for (var m = 0; m < positionTiles.length; m++) {
var tile = this.grid.getTile(positionTiles[m]);
if (tile) {
mergedTiles.push(tile);
}
}
// Merge non-zero tiles
var position = 0;
reverseTiles(mergedTiles, direction); // reverse tile for merge function if direction.reverse = true
while (position < mergedTiles.length - 1) {
if (mergedTiles[position] === mergedTiles[position + 1]) {
mergeScore = mergedTiles[position] * 2;
mergedTiles[position] = mergeScore; // update value of mergedTile position
mergedTiles[position + 1] = 0; // update value of mergedTile position + 1
}
position++;
}
mergedTiles = mergedTiles.filter(function(value) {
return value !== 0;
}); // strip mergedTiles of empty tiles
reverseTiles(mergedTiles, direction); // reverse tile for merge function if direction.reverse = true
if (mergedTiles.indexOf(2048) > -1) { // if 2048 tile is in mergedTiles, you win!
this.state.win = true;
}
// Append zero tiles to the front/back of mergedTiles depending on direction pressed.
var zeroLength = this.grid.dim - mergedTiles.length;
for (var z = 0; z < zeroLength; z++) {
if (direction.reverse) {
mergedTiles.unshift(0);
} else {
mergedTiles.push(0);
}
}
// Update actual grid tiles to new merged values if updateTiles == true
if (updateTiles) {
for (var tile = 0; tile < mergedTiles.length; tile++) {
this.grid.setTile(positionTiles[tile], mergedTiles[tile]);
}
}
i++;
}
return mergeScore; // return mergeScore
}
function isGameOver() {
/* Check if game is over */
var mergeScore = 0;
if (!this.grid.isEmpty()) { // if grid is completely full
for (var d in this.directions) {
var direction = this.directions[d];
mergeScore += this.merge(direction, updateTiles = false);
}
if (!mergeScore) { // if mergeScore = 0, then there are no merges left on a full board, hence game is over.
this.state.gameover = true;
}
}
}
function updateScore() {
/* Update the current and best score */
this.score.current += this.score.merge; // update score
this.score.spm = this.score.current / this.moves;
if (this.score.best < this.score.current) { // update best score
this.score.best = this.score.current;
}
}
function reverseTiles(tiles, direction) {
/* Reverse tiles if direction.reverse is true */
if (direction.reverse) {
tiles.reverse();
}
}
}
})();
(function() {
angular.module("Grid", [])
.service("GridService", GridService)
.directive("grid", grid);
// Service
function GridService() {
// Service variables
this.tiles = [];
this.newTiles = newTiles;
this.grid = {
dim: 4,
tiles: [],
newTiles: newTiles,
getTile: getTile,
setTile: setTile,
setEmptyTile: setEmptyTile,
copyTiles: copyTiles,
identicalTiles: identicalTiles,
isEmpty: isEmpty
};
// Service functions
function newTiles() {
/* Initiate new empty tile in grid */
this.tiles = [];
for (var i = 0; i < this.dim * this.dim; i++) {
tile = 0;
this.tiles.push(tile);
}
this.setEmptyTile();
this.setEmptyTile();
}
function getTile(n) {
/* Get the nth tile */
return this.tiles[n];
}
function setTile(n, value) {
/* Set nth tile in grid to a given value */
this.tiles[n] = value;
}
function setEmptyTile() {
/* Set a random empty tile to value 2 or 4 with probability 0.9 or 0.1 respectively */
var emptyTiles = [];
for (var i = 0; i < this.dim * this.dim; i++) {
if (!this.tiles[i]) {
emptyTiles.push(i);
}
}
emptyTile = randomChoice(emptyTiles); // pick a random empty tile from emptyTiles
if (Math.random() > 0.1) {
randomValue = 2;
} else {
randomValue = 4;
}
this.setTile(emptyTile, randomValue);
}
function copyTiles() {
/* Create a deep copy of current grid tiles */
gridCopy = [];
this.tiles.forEach(function(tile) {
gridCopy.push(tile);
});
return gridCopy;
}
function identicalTiles(tiles) {
/* Return true if current grid tiles and newGrid tiles are identical */
for (var i = 0; i < this.tiles.length; i++) {
if (this.tiles[i] !== tiles[i]) {
return false;
}
}
return true;
}
function isEmpty() {
/* Return true if grid has empty tiles */
for(var i=0; i < this.tiles.length; i++) {
if(this.tiles[i] === 0) {
return true;
}
}
return false;
}
}
// Directive
function grid() {
return {
scope: {
tiles: "="
},
restrict: "E",
templateUrl: "grid.tpl.html"
};
}
// Helper Functions
function randomChoice(arr) {
/* Return random choice element from given array */
return arr[Math.floor(arr.length * Math.random())];
}
})();
<div class="tile tile-{{ tile }}" ng-repeat="tile in tiles track by $index" ng-class="{'tile-merge': tile > 0}">
<p>{{ tile }}</p>
</div>
<!DOCTYPE html>
<html ng-app="ng2048">
<head>
<meta charset="utf-8" />
<title>ng2048</title>
<link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/font-awesome/4.2.0/css/font-awesome.min.css" />
<link rel="stylesheet" href="style.css" />
</head>
<body ng-controller="GameCtrl as GameCtrl" ng-keydown="GameCtrl.game.update($event)">
<div class="container">
<!-- header -->
<header>
<h1>ng2048</h1>
<p>Join the numbers and get to the <strong>2048 tile!</strong>
<button class="newgame" ng-click="GameCtrl.game.newGame()">New Game</button>
</p>
</header>
<!-- scoreboard -->
<div class="scoreboard">
<div>
SCORE
<p>{{ GameCtrl.game.score.current }}</p>
</div>
<div>
BEST
<p>{{ GameCtrl.game.score.best }}</p>
</div>
<div>
MOVES
<p>{{ GameCtrl.game.moves }}</p>
</div>
<div>
SPM
<p>{{ GameCtrl.game.score.spm | number: 2 }}</p>
</div>
</div>
<!-- grid logic -->
<div class="grid">
<grid tiles="GameCtrl.game.grid.tiles"></grid>
<div class="game-overlay" ng-show="GameCtrl.game.state.gameover">
<h2>Game over!</h2>
<button class="newgame" ng-click="GameCtrl.game.newGame()">Try again</button>
</div>
<div class="game-overlay" ng-show="GameCtrl.game.state.win && !GameCtrl.game.state.continue">
<h2>You won!</h2>
<button class="newgame" ng-click="GameCtrl.game.continueGame()">Play on!</button>
<button class="newgame" ng-click="GameCtrl.game.newGame()">Try again</button>
</div>
</div>
<!-- instructions -->
<div class="instructions">
<p><b>HOW TO PLAY:</b> Use your <strong>arrow keys</strong> to move the tiles. When two tiles with the same number touch, they <strong>merge into one!</strong>
</p>
</div>
<!-- footer -->
<footer>
<p><a href="http://gist.github.com/chrisrzhou/23ad9e21d11587df37a1" target="_blank">ng2048</a> by chrisrzhou, 2014-12-16
<br />
<a href="http://github.com/chrisrzhou" target="_blank"><i class="fa fa-github"></i></a> |
<a href="http://bl.ocks.org/chrisrzhou" target="_blank"><i class="fa fa-cubes"></i></a> |
<a href="http://www.linkedin.com/in/chrisrzhou" target="_blank"><i class="fa fa-linkedin"></i></a>
</p>
</footer>
</div>
<!-- scripts -->
<script src="http://cdnjs.cloudflare.com/ajax/libs/angular.js/1.2.20/angular.min.js"></script>
<script src="http://cdnjs.cloudflare.com/ajax/libs/angular.js/1.2.20/angular-animate.min.js"></script>
<script src="http://cdnjs.cloudflare.com/ajax/libs/d3/3.4.13/d3.min.js"></script>
<script src="app.js"></script>
<script src="game.js"></script>
<script src="grid.js"></script>
<script type="text/javascript">
// Hack to make this example display correctly in an iframe on bl.ocks.org
d3.select(self.frameElement).style("height", "1000px");
</script>
</body>
</html>
body {
background: #faf8ef;
padding-bottom: 50px;
}
body,
html {
color: #776E65;
font-family: "Open Sans", "Helvetica Neue", Arial, sans-serif;
font-size: 18px;
}
a,
a:hover, a:visited {
color: #D2A000;
}
h1 {
font-size: 2.5em;
}
.container {
width: 500px;
margin: 0 auto;
}
.p-small {
font-size: 12px;
font-style: italic;
}
.scoreboard {
padding-bottom: 30px;
width: 400px;
margin: 0 auto;
text-align: center;
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
.scoreboard div {
display: inline-block;
height: 50px;
width: 80px;
padding: 5px;
margin: 2px;
color: #E1D6CC;
background: #bbada0;
text-align: center;
font-size: 0.7em;
font-weight: 800;
border-radius: 5px;
}
.scoreboard p {
font-size: 2.0em;
color: #ffffff;
margin: 0 auto;
}
.newgame {
height: 25px;
width: 100px;
margin-left: 20px;
border-radius: 5px;
color: #ffffff;
background: #8f7a66;
font-weight: 800;
cursor: pointer;
}
.grid {
position: relative;
background: #bbaaa0;
width: 384px;
height: 384px;
padding: 8px;
border-radius: 5px;
margin: 0 auto;
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
.game-overlay {
background: #DFD4CA;
opacity: 0.8;
position: absolute;
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
z-index: 10;
text-align: center;
}
.game-overlay h2 {
padding-top: 100px;
font-size: 2.5em;
}
.instructions {
padding: 20px 0;
width: 400px;
margin: 0 auto;
}
footer {
color: white;
padding-top: 5px;
border-top: 1px solid gray;
font-size: 12px;
position: fixed;
left: 0;
bottom: 0;
height: 50px;
width: 100%;
background: black;
text-align: center;
}
/* Tile CSS */
.tile {
display: inline-block;
margin: 8px;
width: 80px;
height: 80px;
border-radius: 5px;
text-align: center;
font-weight: 800;
z-index: 10;
font-size: 1.3em;
cursor: default;
}
.tile-0 {
-webkit-transform: scale(1.1);
-moz-transform: scale(1.1);
-ms-transform: scale(1.1);
-o-transform: scale(1.1);
transform: scale(1.1);
color: #CDC1B4;
background: #CDC1B4;
}
.tile-2 {
color: #776E65;
background: #eee4da;
}
.tile-4 {
color: #776E65;
background: #ede0c8;
}
.tile-8 {
color: #f9f6f2;
background: #f2b179;
}
.tile-16 {
color: #f9f6f2;
background: #f59563;
}
.tile-32 {
color: #f9f6f2;
background: #f67c5f;
}
.tile-64 {
color: #f9f6f2;
background: #f65e3b;
}
.tile-128 {
color: #f9f6f2;
background: #edcf72;
}
.tile-256 {
color: #f9f6f2;
background: #edcc61;
}
.tile-512 {
color: #f9f6f2;
background: #edc850;
}
.tile-1024 {
color: #f9f6f2;
background: #edc53f;
}
.tile-2048 {
color: #f9f6f2;
background: #edc22e;
}
/* CSS animations */
.game-overlay.ng-hide-remove {
-webkit-transition: 0.5s linear all;
-moz-transition: 0.5s linear all;
-ms-transition: 0.5s linear all;
-o-transition: 0.5s linear all;
transition: 0.5s linear all;
display: block!important;
opacity: 0.8;
}
.game-overlay.ng-hide {
opacity: 0;
}
.tile-merge {
-webkit-transform: scale(1.1);
-moz-transform: scale(1.1);
-ms-transform: scale(1.1);
-o-transform: scale(1.1);
transform: scale(1.1);
transition: 0.2s ease-in-out;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment