function Grid(size) { this.size = size; this.startTiles = 2; this.cells = []; this.build(); this.playerTurn = true; } // pre-allocate these objects (for speed) Grid.prototype.indexes = []; for (var x=0; x<4; x++) { Grid.prototype.indexes.push([]); for (var y=0; y<4; y++) { Grid.prototype.indexes[x].push( {x:x, y:y} ); } } // Build a grid of the specified size Grid.prototype.build = function () { for (var x = 0; x < this.size; x++) { var row = this.cells[x] = []; for (var y = 0; y < this.size; y++) { row.push(null); } } }; // Find the first available random position Grid.prototype.randomAvailableCell = function () { var cells = this.availableCells(); if (cells.length) { return cells[Math.floor(Math.random() * cells.length)]; } }; Grid.prototype.availableCells = function () { var cells = []; var self = this; this.eachCell(function (x, y, tile) { if (!tile) { //cells.push(self.indexes[x][y]); cells.push( {x:x, y:y} ); } }); return cells; }; // Call callback for every cell Grid.prototype.eachCell = function (callback) { for (var x = 0; x < this.size; x++) { for (var y = 0; y < this.size; y++) { callback(x, y, this.cells[x][y]); } } }; // Check if there are any cells available Grid.prototype.cellsAvailable = function () { return !!this.availableCells().length; }; // Check if the specified cell is taken Grid.prototype.cellAvailable = function (cell) { return !this.cellOccupied(cell); }; Grid.prototype.cellOccupied = function (cell) { return !!this.cellContent(cell); }; Grid.prototype.cellContent = function (cell) { if (this.withinBounds(cell)) { return this.cells[cell.x][cell.y]; } else { return null; } }; // Inserts a tile at its position Grid.prototype.insertTile = function (tile) { this.cells[tile.x][tile.y] = tile; }; Grid.prototype.removeTile = function (tile) { this.cells[tile.x][tile.y] = null; }; Grid.prototype.withinBounds = function (position) { return position.x >= 0 && position.x < this.size && position.y >= 0 && position.y < this.size; }; Grid.prototype.clone = function() { newGrid = new Grid(this.size); newGrid.playerTurn = this.playerTurn; for (var x = 0; x < this.size; x++) { for (var y = 0; y < this.size; y++) { if (this.cells[x][y]) { newGrid.insertTile(this.cells[x][y].clone()); } } } return newGrid; }; // Set up the initial tiles to start the game with Grid.prototype.addStartTiles = function () { for (var i=0; i= 0 && x <= 3 && y >= 0 && y <= 3 && self.cells[x][y] && self.cells[x][y].value == value && !self.cells[x][y].marked ) { self.cells[x][y].marked = true; for (direction in [0,1,2,3]) { var vector = self.getVector(direction); mark(x + vector.x, y + vector.y, value); } } } var islands = 0; for (var x=0; x<4; x++) { for (var y=0; y<4; y++) { if (this.cells[x][y]) { this.cells[x][y].marked = false } } } for (var x=0; x<4; x++) { for (var y=0; y<4; y++) { if (this.cells[x][y] && !this.cells[x][y].marked) { islands++; mark({ x:x, y:y }, this.cells[x][y].value); } } } return islands; } // measures how smooth the grid is (as if the values of the pieces // were interpreted as elevations). Sums of the pairwise difference // between neighboring tiles (in log space, so it represents the // number of merges that need to happen before they can merge). // Note that the pieces can be distant Grid.prototype.smoothness = function() { var smoothness = 0; for (var x=0; x<4; x++) { for (var y=0; y<4; y++) { if ( this.cellOccupied( this.indexes[x][y] )) { var value = Math.log(this.cellContent( this.indexes[x][y] ).value) / Math.log(2); for (var direction=1; direction<=2; direction++) { var vector = this.getVector(direction); var targetCell = this.findFarthestPosition(this.indexes[x][y], vector).next; if (this.cellOccupied(targetCell)) { var target = this.cellContent(targetCell); var targetValue = Math.log(target.value) / Math.log(2); smoothness -= Math.abs(value - targetValue); } } } } } return smoothness; } Grid.prototype.monotonicity = function() { var self = this; var marked = []; var queued = []; var highestValue = 0; var highestCell = {x:0, y:0}; for (var x=0; x<4; x++) { marked.push([]); queued.push([]); for (var y=0; y<4; y++) { marked[x].push(false); queued[x].push(false); if (this.cells[x][y] && this.cells[x][y].value > highestValue) { highestValue = this.cells[x][y].value; highestCell.x = x; highestCell.y = y; } } } increases = 0; cellQueue = [highestCell]; queued[highestCell.x][highestCell.y] = true; markList = [highestCell]; markAfter = 1; // only mark after all queued moves are done, as if searching in parallel var markAndScore = function(cell) { markList.push(cell); var value; if (self.cellOccupied(cell)) { value = Math.log(self.cellContent(cell).value) / Math.log(2); } else { value = 0; } for (direction in [0,1,2,3]) { var vector = self.getVector(direction); var target = { x: cell.x + vector.x, y: cell.y+vector.y } if (self.withinBounds(target) && !marked[target.x][target.y]) { if ( self.cellOccupied(target) ) { targetValue = Math.log(self.cellContent(target).value ) / Math.log(2); if ( targetValue > value ) { //console.log(cell, value, target, targetValue); increases += targetValue - value; } } if (!queued[target.x][target.y]) { cellQueue.push(target); queued[target.x][target.y] = true; } } } if (markAfter == 0) { while (markList.length > 0) { var cel = markList.pop(); marked[cel.x][cel.y] = true; } markAfter = cellQueue.length; } } while (cellQueue.length > 0) { markAfter--; markAndScore(cellQueue.shift()) } return -increases; } // measures how monotonic the grid is. This means the values of the tiles are strictly increasing // or decreasing in both the left/right and up/down directions Grid.prototype.monotonicity2 = function() { // scores for all four directions var totals = [0, 0, 0, 0]; // up/down direction for (var x=0; x<4; x++) { var current = 0; var next = current+1; while ( next<4 ) { while ( next<4 && !this.cellOccupied( this.indexes[x][next] )) { next++; } if (next>=4) { next--; } var currentValue = this.cellOccupied({x:x, y:current}) ? Math.log(this.cellContent( this.indexes[x][current] ).value) / Math.log(2) : 0; var nextValue = this.cellOccupied({x:x, y:next}) ? Math.log(this.cellContent( this.indexes[x][next] ).value) / Math.log(2) : 0; if (currentValue > nextValue) { totals[0] += nextValue - currentValue; } else if (nextValue > currentValue) { totals[1] += currentValue - nextValue; } current = next; next++; } } // left/right direction for (var y=0; y<4; y++) { var current = 0; var next = current+1; while ( next<4 ) { while ( next<4 && !this.cellOccupied( this.indexes[next][y] )) { next++; } if (next>=4) { next--; } var currentValue = this.cellOccupied({x:current, y:y}) ? Math.log(this.cellContent( this.indexes[current][y] ).value) / Math.log(2) : 0; var nextValue = this.cellOccupied({x:next, y:y}) ? Math.log(this.cellContent( this.indexes[next][y] ).value) / Math.log(2) : 0; if (currentValue > nextValue) { totals[2] += nextValue - currentValue; } else if (nextValue > currentValue) { totals[3] += currentValue - nextValue; } current = next; next++; } } return Math.max(totals[0], totals[1]) + Math.max(totals[2], totals[3]); } Grid.prototype.maxValue = function() { var max = 0; for (var x=0; x<4; x++) { for (var y=0; y<4; y++) { if (this.cellOccupied(this.indexes[x][y])) { var value = this.cellContent(this.indexes[x][y]).value; if (value > max) { max = value; } } } } return Math.log(max) / Math.log(2); } // WIP. trying to favor top-heavy distributions (force consolidation of higher value tiles) /* Grid.prototype.valueSum = function() { var valueCount = []; for (var i=0; i<11; i++) { valueCount.push(0); } for (var x=0; x<4; x++) { for (var y=0; y<4; y++) { if (this.cellOccupied(this.indexes[x][y])) { valueCount[Math.log(this.cellContent(this.indexes[x][y]).value) / Math.log(2)]++; } } } var sum = 0; for (var i=1; i<11; i++) { sum += valueCount[i] * Math.pow(2, i) + i; } return sum; } */ // check for win Grid.prototype.isWin = function() { var self = this; for (var x=0; x<4; x++) { for (var y=0; y<4; y++) { if (self.cellOccupied(this.indexes[x][y])) { if (self.cellContent(this.indexes[x][y]).value == 2048) { return true; } } } } return false; } //Grid.prototype.zobristTable = {} //for //Grid.prototype.hash = function() { //}