15
\$\begingroup\$

The code contains an AI function, that searches breadth-first all possible moves and get the score of their grids, a depth of 6 is used which makes at most \4ドル+4^2+...+4^6\$ computed grids. The highest scores (and their associated moves) are kept for each level of depth, the most redundant one is chosen.

This function uses a 4x4 grid, and other functions to make it move in the 4 directions, so it can run in the background and return the final grid (when no more moves are possible). You also see a visualization of it as an HTML table, whose cells are in the cells variable and updated after each turn. You can also play directly with no solver, with keyboard's arrow keys.

Some remarks on the code either on the form, or on the design of the game solver would be great.

var n= 4; // grid size
var demoAI = new AI({maxDepth:6, minDepth:3, monoWeight:20, smoothWeight:0, freeWeight:40, maxWeight:60})
function AI(o){
 this.init=function(){
 this.grid = [];
 for(var i=0;i<n;i++){
 this.grid[i]=[]
 for(var j=0;j<n;j++){
 this.grid[i][j]='';
 }
 }
 rand(2, this.grid)
 rand(2, this.grid)
 this.steps=0;
 }
 this.init();
 this.move=function(p){ //0:up, 1:right, 2:down, 3:left
 var newgrid = mv(p, this.grid);
 if (!equal(newgrid, this.grid)){
 this.grid = newgrid;
 try{
 rand(2, this.grid)
 }catch(e){
 console.log('no room', e) 
 }
 }
 }
 this.predict = function(){
 var root={path:[], grid:this.grid};
 var levels = [[root]];
 for (var depth=1; depth<o.maxDepth; depth++){
 levels[depth] = [];
 for (var node of levels[depth-1])
 levels[depth] = levels[depth].concat(expand(node))
 }
 var best = []; //best first moves by level
 levels.map(maxScore).forEach(function(x){if (!!x.path) best.push(x.path[0])});
 if (!best.length) return null;
 var votedBest=[];
 var votes=[0,0,0,0]; //votes for each direction
 for (var i=best.length-1; i>=o.minDepth; i--){ // avoid votes of low depth
 votes[best[i]]++;
 votedBest.push(maxIndex(votes))
 }
 //pick the latest non-null winner
 for (var i=votedBest.length-1; i>=0; i--)
 if (votedBest[i]!=null)
 return votedBest[i]
 return null;
 }
 this.run=function(){
 var p = this.predict();
 if (p!=null) {
 this.move(p);
 this.steps++;
 this.run()
 }
 }
 this.test=function(){//run 10 times and average
 var s=0;
 for (var i=0; i<10; i++){
 this.init();
 this.run()
 s+=maxValue(this.grid)
 }
 return s/10;
 }
 function expand(node){ // mode={grid,score,path}
 var children= [];
 for (var move of [0,1,2,3]){
 var g = mv(move, node.grid);
 if (!equal(g, node.grid)) {
 try{
 rand(2, g)
 children.push({grid:g, score:score(g), path:node.path.concat([move])});
 }catch(e){
 console.log('no more room', e) 
 }
 }
 }
 return children;
 }
 function score(grid){
 var s = stats(grid);
 //console.log(s)
 return o.monoWeight*s.monotonicity+
 o.smoothWeight*s.smoothness+
 o.freeWeight*s.freeCells+
 o.maxWeight*s.maxValue;
 }
}
var g = [];// transformation matrix in the 4 directions g[i][j] = [up, right, down, left]
var ig= [];// the inverse tranformations
for(var i=0;i<n;i++){
 g[i]=[]; ig[i]=[];
 for(var j=0;j<n;j++){
 g[i][j]= [[j,i], [i,n-1-j], [j,n-1-i], [i,j]];
 ig[i][j]=[[j,i], [i,n-1-j], [n-1-j,i], [i,j]];
 }
}
function transform(g, k, grid){
 var newgrid=[];
 for(var i=0;i<grid.length;i++){
 newgrid[i]=[];
 for(var j=0;j<grid.length;j++)
 newgrid[i][j] = grid[ g[i][j][k][0] ][ g[i][j][k][1] ];
 }
 return newgrid;
}
var cells=[];
var table = document.createElement("table");
for(var i=0;i<n;i++){
 var tr= document.createElement("tr");
 cells[i]=[];
 for(var j=0;j<n;j++){
 cells[i][j] = document.createElement("td");
 tr.appendChild(cells[i][j])
 }
 table.appendChild(tr);
}
document.querySelector('div').appendChild(table);
//var flatCells = cells.reduce(function(v,a){return v.concat(a)},[])
cells.forEach(function(a, i){a.forEach(function(el,j){el.innerHTML=demoAI.grid[i][j]})});
function runAI(){
 var p = demoAI.predict();
 if (p!=null && ai.run) {
 demoAI.move(p)
 cells.forEach(function(a, i){a.forEach(function(el,j){el.innerHTML=demoAI.grid[i][j]})});
 requestAnimationFrame(runAI)
 }
}
ai.onclick = function(){
 if (!ai.run){
 this.innerHTML='stop AI';
 ai.run=true;
 runAI();
 }else{
 this.innerHTML='run AI';
 ai.run=false;
 } 
}
hint.onclick=function(){
 hintvalue.innerHTML = ['up','right','down','left'][demoAI.predict()]
}
document.addEventListener("keydown", function (event) {
 if (event.which in map){
 demoAI.move(map[event.which])
 cells.forEach(function(a, i){a.forEach(function(el,j){el.innerHTML=demoAI.grid[i][j]})});
 console.log(stats(demoAI.grid))
 }
})
var map = {
 38: 0, // Up
 39: 1, // Right
 40: 2, // Down
 37: 3, // Left
};
init.onclick = function(){
 demoAI.init();
 cells.forEach(function(a, i){a.forEach(function(el,j){el.innerHTML=demoAI.grid[i][j]})});
}
function stats(grid){ 
 var tgrid = transform(g, 0, grid); // transpose
 var cx = compact(grid), cy = compact(tgrid);
 // array of differences
 var dx = cx.map(function(a){ return a.map(function(_, i){return a[i]-a[i-1]}).slice(1, a.length) }),
 dy = cy.map(function(a){ return a.map(function(_, i){return a[i]-a[i-1]}).slice(1, a.length) }); 
 // sum abs(differences)
 var sx = dx.reduce(function(v,x){ return v+x.reduce(function(t,y){return t+(y==0?1:1/Math.abs(y))},0) },0), // Math.log2?
 sy = dy.reduce(function(v,x){ return v+x.reduce(function(t,y){return t+(y==0?1:1/Math.abs(y))},0) },0);
 // flatten differences and get their sign only
 var fx = dx.reduce(function(v, a){return v.concat(a)},[]).map(Math.sign),
 fy = dy.reduce(function(v, a){return v.concat(a)},[]).map(Math.sign);
 // count 1 and -1
 var mx = [fx.reduce(function(v,x){return x==1?v+1:v},0), fx.reduce(function(v,x){return x==-1?v+1:v},0)],
 my = [fy.reduce(function(v,x){return x==1?v+1:v},0), fy.reduce(function(v,x){return x==-1?v+1:v},0)];
 var Mx = Math.max.apply(null, mx)/Math.min.apply(null, mx),
 My = Math.max.apply(null, my)/Math.min.apply(null, my);
 if(Mx==Infinity || isNaN(Mx)) Mx=2*grid.length*grid.length;
 if(My==Infinity || isNaN(My)) My=2*grid.length*grid.length;
 var freeCells = grid.length*grid.length-filledCells(grid);
 return {monotonicity: Mx+My==0?0:Math.log2(Mx*Mx+My*My), smoothness: sx*sx+sy*sy, freeCells: freeCells*freeCells, maxValue: maxValue(grid)}
}
function maxValue(grid){
 return Math.round(Math.log2( Math.max.apply(null, grid.map(function(a){ return Math.max.apply(null, a)})) ));
}
grid1 = [[2, '', '', ''],
 ['', '', 4, 2],
 ['', 8, 16, 4],
 [4, 2, 4, 8]];
grid2 = [[2, '', '', ''],
 ['', '', 4, 2],
 ['', 8, 16, 4],
 [4, 8, 16, 8]]
//console.log(stats(grid1), stats(grid2))
function filledCells(grid){
 return grid.reduce(function(v,a){
 return v+a.reduce(function(t,x){
 return t+Math.abs(Math.sign(x))
 },0)
 },0)
}
function maxIndex(arr){ // return the index of the max, or null if there are 2 equal max
 var max=[-1,null];
 for (var i=0;i<arr.length; i++){
 if (arr[i]>max[0]) max=[arr[i], i];
 else if (arr[i]==max[0]) max[1]=null;
 }
 return max[1]
}
function maxScore(nodes){
 var max={score:0};
 for (var node of nodes)
 if (node.score>max.score)
 max = node;
 return max;
}
function mv(k, grid){
 var tgrid = transform(ig, k, grid);
 for (var i=0;i<tgrid.length;i++){
 var a=tgrid[i];
 for (var j=0, _j=0;j<a.length;j++)
 if (a[j]!='') a[_j++] = (j<a.length-1 && a[j]==a[j+1]) ? 2*a[j++] : a[j]
 for (;_j<a.length;_j++)
 a[_j] = ''; 
 }
 return transform(g, k, tgrid);
}
function compact(grid){ // remove empty values [2, 4, '', 2] -> [2, 4, 2]
 var ngrid=[];
 for (var row of grid)
 ngrid.push(row.filter(Number))
 return ngrid;
}
function random(v){
 var free = $('td').filter(function(el){ return el.innerHTML==''});
 var r = Math.floor(Math.random()*free.length);
 free[r].innerHTML = v;
}
function rand(v, grid){
 var r = Math.floor(Math.random()*(grid.length*grid.length-filledCells(grid))), _r=0;
 for (var i = 0; i < grid.length; i++) {
 for (var j = 0; j < grid.length; j++) {
 if (!Number(grid[i][j])){
 if (_r==r) {grid[i][j]=v}
 _r++;
 }
 }
 }
}
function equal(grid1, grid2){
 for (var i=0;i<grid1.length;i++)
 for (var j=0;j<grid1.length;j++) 
 if (grid1[i][j]!=grid2[i][j]) return false;
 return true;
}
function $(s) {
 return [].slice.apply(typeof s=="string"?document.querySelectorAll(s):s);
}
table, th, td {
 border: 1px solid black;
}
td {
 width: 35px;
 height: 35px;
 text-align: center;
}
<div></div>
<button id=init>init</button><button id=ai>run AI</button><button id=hint>hint</button><span id=hintvalue></span>

jsFiddle

edit: final version

asked Feb 13, 2015 at 21:53
\$\endgroup\$
4
  • \$\begingroup\$ Note that Math.sign() is currently not supported in many browsers. \$\endgroup\$ Commented Feb 13, 2015 at 23:10
  • \$\begingroup\$ I've taken the liberty of copying the little bit of HTML and CSS from your jsFiddle into the question as a Stack Snippet, changing just the cell size from 50px to 35px to make it fit better. \$\endgroup\$ Commented Feb 13, 2015 at 23:26
  • \$\begingroup\$ See: What you may and may not do after receiving answers In this case, a follow-up question is the right way to go. \$\endgroup\$ Commented Feb 21, 2015 at 15:51
  • \$\begingroup\$ ok, thanks, done \$\endgroup\$ Commented Feb 21, 2015 at 16:46

2 Answers 2

11
\$\begingroup\$

First, you are usually good about naming your variables, but you still have a few single-letter variables in here, o, p, and v, to name a few. These should be more descriptive of what they are.

Second, your code would be a bit more readable if you put spaces around your operators, like for(var i = 0; i < n; i++) {. JSLint says you should do this too.

Third, I think your algorithm can be improved. The very highest number that can be created is 2^18, which can only happen when you have 2^17 in one of the corner squares, and 2^16, 2^15, and 2^14 along one of the edges. Then, you need to 2^13 above 2^14, 2^12 next to 2^13, and so on:

enter image description here

Given the nature of the game, the odds of this happening are extremely low, but your algorithm does not seem to weight the squares to follow this pattern.

Finally, once the board gets filled when playing with the keyboard, nothing happens. When you ask for a hint, it returns "undefined". Perhaps you should implement win/loss situations to cover this scenario.

answered Feb 16, 2015 at 4:19
\$\endgroup\$
2
  • \$\begingroup\$ thanks, I'm trying hard to improve it, I've tried to force descending high values into a kind of 'snake' jsfiddle.net/crl/hmthq78t/57 but it's inefficient, I'll try to make the highest values stick to an edge, because this post's version of the AI is too erratic \$\endgroup\$ Commented Feb 17, 2015 at 1:56
  • 1
    \$\begingroup\$ No problem. Glad to help. \$\endgroup\$ Commented Feb 17, 2015 at 1:57
6
\$\begingroup\$

Sloppy code

This code is extremely sloppy, making it very hard to review. Next time, before posting here, please paste it on http://jshint.com/ or http://jslint.com/ first, and make the suggested corrections.

Don't repeat yourself

Copy-pasting code is a very dangerous practice. Don't do it. For example:

var dx = cx.map(function(a){ return a.map(function(_, i){return a[i]-a[i-1]}).slice(1, a.length) }),
 dy = cy.map(function(a){ return a.map(function(_, i){return a[i]-a[i-1]}).slice(1, a.length) });

You do this in many other places too: you write the same anonymous function twice. Give the function a nice, descriptive name, make it nicely indented and readable, and rewrite the two map calls in terms of it. For example:

// note: try to give this a more descriptive name than "mapper"
function mapper(a) {
 return a.map(function(_, i) { return a[i] - a[i-1] }).slice(1, a.length)
}
var dx = cx.map(mapper),
 dy = cy.map(mapper);

On closer look, the entire stats method is about deriving Mx and My by applying the exact same sequence of operations on grid and tgrid, respectively. Instead of writing all those non-trivial steps twice, it would be better to move the common logic to a helper function with no duplicated code, so that you can rewrite as:

var Mx = helper(grid),
 My = helper(tgrid);
answered Feb 16, 2015 at 6:27
\$\endgroup\$
1
  • \$\begingroup\$ thanks, the stats function is indeed the most ugly, it's still in dev, I've factorized like you suggest the code, by concatenating grid and tgrid's rows in a same array jsfiddle.net/crl/hmthq78t/52, but I changed other things and I'm getting worse results... anyway the AI part is definitely not ready \$\endgroup\$ Commented Feb 17, 2015 at 2:00

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.