<div id='top-bar' class='top-bar bar'>
  <a href='#' id='sz-decr' class='ctrl sz-ctrl'>-</a>
  <span id='sz-lbl' class='lbl-area'>5</span>
  <a href='#' id='sz-incr' class='ctrl sz-ctrl'>+</a>
  
  <a href='#' id='gen' class='ctrl big-btn'>Generate New</a>
</div>

<div id='game-area' class='game-area bar'>
  <div id='settings-bar' class='bar'>
    <ul id='next' class='next bar bar-comp'></ul>
    
    <span id='free' class='free lbl-area bar-comp'>
      Free cells: 
    </span>
    
    <span id='pts' class='pts lbl-area bar-comp'>
      Score: 
    </span>
  </div>
  
  <div id='grid' class='grid bar gridcomp'></div>
  
  <div>
    <a href='#' id='start' class='ctrl big-btn start'>Start!</a>
  </div>
</div>
html {
  font: 85%/1.5 Helvetica, sans-serif;
}
.top-bar {
  background: palegreen;
}
.bar, .ctrl {
  box-shadow: 1px 1px 3px;
  text-align: center;
}
.bar:not(.next) {
  margin: .75em auto;
  padding: .25em .5em;
}
.ctrl, .lbl-area, .next, .next *, .gridcomp:not(.gridrow) {
  display: inline-block;
  vertical-align: middle;
}
.ctrl {
  position: relative;
  height: 1.5em;
  background:
    linear-gradient(palegreen, lawngreen) 0 50%;
  background-size: 1px 100%;
  color: darkolivegreen;
  text-shadow: 0 1px 0 lemonchiffon;
  text-decoration: none;
  transition: .35s;
}
.ctrl:hover, .ctrl:focus {
  background-size: 1px 120%;
  color: black;
  text-shadow: 0 1px 0 white;
}
.ctrl:active {
  top: 1px;
  box-shadow: inset 1px 1px 3px;
}
.sz-ctrl {
  width: 1.5em;
  border-radius: 50%;
  font-weight: 900;
}
.big-btn, .lbl-area {
  padding: 0 .8em;
  margin: .25em 1em;
  border-radius: .15em;
}
.lbl-area {
  min-width: 6.5em;
  border: solid 1px grey;
  box-shadow: 0 1px 0 white;
}
.game-area {
  background: linear-gradient(mintcream -66%, limegreen 133%);
}
.next {
  padding: 0;
  margin: .5em;
}
.sym, .gridcell {
  width: 1.3em;
  height: 1.3em;
  font: 700 2.5em/1.3 Helvetica, sans-serif;
}
.next li:not(:first-child), 
.gridcell:not(:first-child) {
  border-left: solid 1px grey;
}
.gridrow:not(:first-child) {
	border-top: solid 1px grey;
}
.gridcomp, .grid.gridcomp {
	padding: 0;
  margin: 0;
}
.gridcell {
  position: relative;
  background: lightgreen;
  cursor: pointer;
}
.pulse {
	z-index: 9;
  box-shadow: 1px 1px 3px;
  animation: puls 1s infinite alternate;
}
@keyframes puls {
  to { transform: scale(1.09); }
}
.start { margin: 1em; }
var sym = ['❆', '❅', '❄', '✵', '✣', '✪', '☆', '✧', '☸', '♫', '☼', '♪', '❖', '✜', '☢', '☊'], 
    nxtsym = [], 
    gridstate = [], 
    started = false, 
    onhold = null;

var topbar = document.getElementById('top-bar'),
    gridszel = document.getElementById('sz-lbl'), 
    gsdecr = document.getElementById('sz-decr'), 
    gsincr = document.getElementById('sz-incr'),
    gen = document.getElementById('gen'),
    nxt = document.getElementById('next'), 
    freeel = document.getElementById('free'), 
    ptsel = document.getElementById('pts'), 
    gridel = document.getElementById('grid'), 
    start = document.getElementById('start');

var initUsedSym = function() {
  var used = [], symsz = sym.length;
  for(var i = 0; i < usedsymsz; i++) {
    var tmp = Math.round(Math.random()*(symsz-1));
    while(used.indexOf(sym[tmp]) != -1) {
      tmp = Math.round(Math.random()*(symsz-1));
    }
    used.push(sym[tmp]);
  }
  return used;
};

var gridsz = parseInt(gridszel.innerHTML, 10), 
    nextsz = Math.round(gridsz/3), 
    scorelinesz = Math.ceil(gridsz/2), 
    usedsymsz = Math.round(gridsz*.75), 
    usedsym = initUsedSym(), 
    free = gridsz*gridsz, 
    score = 0;

var initNext = function() {
  var txt = '';
  nxtsym = [];
  for(var i = 0; i < nextsz; i++) {
    var r = Math.round(Math.random()*(usedsymsz-1));
    nxtsym.push(usedsym[r]);
    txt += "<li><span class='sym sym-" + r + "'>" + usedsym[r] + "</span></li>";
  }
  nxt.innerHTML = txt;
};

initNext();

var initGrid = function() {
  var txt = '';
  onhold = null;
  for(var i = 0; i < gridsz; i++) {
    txt += "<div id='gridrow-" + i + "' class='gridcomp gridrow'>";
    for(var j = 0; j < gridsz; j++) {
      gridstate[i*gridsz + j] = null;
      txt += 
        "<div id='gridcell-" + i + "-" + j + "' class='gridcomp gridcell'></div>";
    }
    txt += "</div>"
  }
  gridel.innerHTML = txt;
};

initGrid();

var initFree = function() {
  var t = 'Free cels: ' + free;
  freeel.innerHTML = t;
};

initFree();

var initPts = function() {
  var t = 'Score: ' + score;
  ptsel.innerHTML = t;
};

initPts();

topbar.addEventListener('click', function(e){
  var target = e.target;
  if(target == gsdecr) {
    if(gridsz > 3) gridszel.innerHTML = --gridsz;
    return;
  }
  if(target == gsincr) {
    if(gridsz < 17) gridszel.innerHTML = ++gridsz;
    return;
  }
  if(target == gen) {
    nextsz = Math.round(gridsz/3), 
    scorelinesz = Math.ceil(gridsz/2), 
    usedsymsz = Math.round(gridsz*.75), 
    usedsym = initUsedSym(), 
    free = gridsz*gridsz, 
    score = 0;
    initNext();
    initFree();
    initPts();
    initGrid();
    started = false;
  }
}, false);

var nextS = function() {
  for(var i = 0; i < nextsz; i++) {
    if (free === 0) {
      alert("game over!");
      return;
    }
    var t = Math.round(Math.random()*(gridsz*gridsz-1)), 
        col, row, cell;
    while(gridstate[t] != null) {
      t = Math.round(Math.random()*(gridsz*gridsz-1));
    }
    --free;
    freeel.innerHTML = 'Free cells: ' + free;
    gridstate[t] = nxtsym[i];
    col = t%gridsz;
    row = (t - col)/gridsz;
    cell = document.getElementById("gridcell-" + row + "-" + col);
    cell.innerHTML = nxtsym[i];
  }
  initNext();
};

start.addEventListener('click', function(){
  if(started) initNext();
  initGrid();
  nextS();
  started = true;
  console.log(gridstate);
}, false);

var check1Fill = function(idx, csym, advance) {
  console.log('checkfor ' + idx + '/' + csym);
  var count = 1, cidx = idx-1, lidx = idx-1, ridx = idx+1;
  console.log('will check left & right of ' + idx);
  console.log('start from ' + idx);
  while(cidx%gridsz !== (gridsz-1) && gridstate[cidx] == csym) {
    console.log('going left');
    console.log('have ' + csym + ' at ' + cidx);
    count++;
    lidx = cidx--;
  }
  console.log('at ' + lidx + ': ' + gridstate[lidx]);
  if(gridstate[lidx] != csym) lidx++;
  cidx = idx+1;
  while(cidx%gridsz !== 0 && gridstate[cidx] == csym) {
    console.log('going right');
    console.log('have ' + csym + ' at ' + cidx);
    count++;
    ridx = cidx++;
  }
  console.log('at ' + ridx + ': ' + gridstate[ridx]);
  if(gridstate[ridx] != csym) ridx--;
  console.log('between ' + lidx + ' & ' + ridx);
  console.log(count + ' vs min is ' + scorelinesz);
  if(count >= scorelinesz) {
    score += 10 + (count - scorelinesz);
    ptsel.innerHTML = 'Score: ' + score;
    for(var i = lidx; i <= ridx; i++) {
      gridstate[i] = null;
      ++free;
      freeel.innerHTML = 'Free cells: ' + free;
      var c = i%gridsz, 
          r = (i-c)/gridsz, 
          cel = document.getElementById('gridcell-' + r + '-' + c);
      console.log('clean ' + i + ' gridcell-' + r + '-' + c);
      cel.innerHTML = '';
    }
    return true;
  }
  return false;
};

var checkFill = function(idx, csym) {
  if(check1Fill(idx, csym, 1)) return true;
  if(check1Fill(idx, csym, gridsz)) return true;
  if(check1Fill(idx, csym, gridsz+1)) return true;
  return false;
};

grid.addEventListener('click', function(e){
  if(!started) return;
  var target = e.target, 
      elid = target.id, 
      idparts = elid.split('-'), 
      row = parseInt(idparts[1], 10), 
      col = parseInt(idparts[2], 10), 
      cid = row*gridsz + col;
  if(onhold == null && gridstate[cid] == null)
    return;
  if(onhold != null) {
    var prevcol = onhold%gridsz,
        prevrow = (onhold - prevcol)/gridsz,
        prevclicked = document.getElementById('gridcell-' + prevrow + '-' + prevcol);
    prevclicked.classList.remove('pulse');
    if(gridstate[cid] != null) {
      onhold = cid;
      console.log('hold ' + onhold);
      target.classList.add('pulse');
      return;
    }
    prevclicked.innerHTML = '';
    var csym = gridstate[onhold];
    target.innerHTML = csym;
    gridstate[cid] = csym;
    gridstate[onhold] = null;
    onhold = null;
    if(!checkFill(cid, csym)){
      nextS();
      var gl = gridstate.length;
      for(var i = 0; i < gl; i++) {
        if(i%gridsz <= scorelinesz) continue;
        csym = gridstate[i];
        if(csym != null) checkFill(i, csym);
      }
    }
    return;
  }
  if(gridstate[cid] != null) {
    onhold = cid;
    console.log('hold ' + onhold);
    target.classList.add('pulse');
    return;
  }
}, false);

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

  1. //cdnjs.cloudflare.com/ajax/libs/jquery/2.1.3/jquery.min.js