telegram Mineroobot solver

resolve telegram mineroobot automatically

// ==UserScript==
// @name         telegram Mineroobot solver
// @version      0.0.1
// @include      https://web.telegram.org/*
// @description  resolve telegram mineroobot automatically
// @namespace    mineroobot-solver.mmis1000.me
// @run-at       document-start
// @grant        none
// ==/UserScript==

window.name = 'NG_ENABLE_DEBUG_INFO!' + window.name;

/**
 * Converts an HSL color value to RGB. Conversion formula
 * adapted from http://en.wikipedia.org/wiki/HSL_color_space.
 * Assumes h, s, and l are contained in the set [0, 1] and
 * returns r, g, and b in the set [0, 255].
 *
 * @param Number h The hue
 * @param Number s The saturation
 * @param Number l The lightness
 * @return Array The RGB representation
 */

function hslToRgb(h, s, l) {
  var r, g, b;
  if (s == 0) {
    r = g = b = l; // achromatic

  }
  else {
    var hue2rgb = function hue2rgb(p, q, t) {
      if (t < 0) t += 1;
      if (t > 1) t -= 1;
      if (t < 1 / 6) return p + (q - p) * 6 * t;
      if (t < 1 / 2) return q;
      if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
      return p;
    };

    var q = l < 0.5 ? l * (1 + s) : l + s - l * s;
    var p = 2 * l - q;
    r = hue2rgb(p, q, h + 1 / 3);
    g = hue2rgb(p, q, h);
    b = hue2rgb(p, q, h - 1 / 3);
  }
  return [Math.round(r * 255), Math.round(g * 255), Math.round(b * 255)];
}

/**
 * @param {number} r range between 0-256
 * @param {number} g range between 0-256
 * @param {number} b range between 0-256
 * @return {string}
 */
function rgbColor(r, g, b) {
  // return `\u001b[${bg ? 48 : 38};2;${r};${g};${b}m`;
  var str = ((r << 16) + (g << 8) + b).toString(16);
  while (str.length < 6) str = '0' + str;
  return '#' + str;
}

/**
 * @param {number} h range between 0-1
 * @param {number} s range between 0-1
 * @param {number} l range between 0-1
 * @return {string}
 */
function hslColor(h, s, l) {
   var [r, g, b] = hslToRgb(h, s, l);
   return rgbColor(r, g, b);
}

/**
 * @param {number} m total
 * @param {number} n selected
 * @return {number}
 */
function c(m, n) {
  var val = 1;

  for (let temp = m; temp > m - n; temp--) {
    val = val * temp / (temp - m + n);
  }
  return val;
}

/**
 * @param {number} w width
 * @param {number} h height
 * @return {function}
 */
function p(w, h) {
  return function ptr(x, y) {
    var pos = {x, y};
    pos.next = function () {
      if (x < 0 || x >= w || y < 0 && y >= h) {
        return null;
      }

      // highest valid pointer
      if (x >= w - 1 && y >= h - 1) {
        return null;
      }

      if (x < w - 1) {
        return ptr(x + 1, y);
      } else {
        return ptr(0, y + 1);
      }
    };

    if (x >= 0 && x < w && y >= 0 && y < h) {
      pos.offset = x + y * w;
    } else {
      pos.offset = null;
    }

    pos.neighbors = function() {
      return [
        ptr(x - 1, y - 1),
        ptr(x, y - 1),
        ptr(x + 1, y - 1),
        ptr(x - 1, y),
        ptr(x + 1, y),
        ptr(x - 1, y + 1),
        ptr(x, y + 1),
        ptr(x + 1, y + 1)
      ];
    };

    return pos;
  };
}

/**
 * @param {number} w width
 * @param {number} h height
 * @param (any} init init value
 * @param {any} outBoundVal value access out of table bound
 * @return {function}
 */
function t(w, h, init, outBoundVal) {
  var data = [];
  var Ptr = p(w, h);

  function table(ptr, val) {
    if (val != null) {
      if (ptr.offset != null) {
        data[ptr.offset] = val;
      }
    } else {
      if (ptr.offset != null) {
        return data[ptr.offset];
      } else {
        return outBoundVal;
      }
    }
  }

  table.w = w;
  table.h = h;
  table.data = data;

  for (let ptr = Ptr(0, 0); ptr; ptr = ptr.next()) {
    data[ptr.offset] = init;
  }

  table.clone = function () {
    var newTable = t(w, h, init, outBoundVal);
    var ptr = p(w, h)(0, 0);

    for (let ptr = Ptr(0, 0); ptr; ptr = ptr.next()) {
      newTable(ptr, table(ptr));
    }

    return newTable;
  };

  //regex must has global flag
  table.countRegex = function(regex) {
    var counter = 0;

    for (let ptr = Ptr(0, 0); ptr; ptr = ptr.next()) {
      if (regex.test(table(ptr))) {
        counter += 1;
      }
    }

    return counter;
  };

  table.count = function (val) {
    var counter = 0;

    for (let ptr = Ptr(0, 0); ptr; ptr = ptr.next()) {
      if (table(ptr) === val) {
        counter += 1;
      }
    }

    return counter;
  };

  table.toString = function (seperator) {
    var res = [];
    seperator = seperator == null ? ' ,' : seperator;
    for (var i = 0; i < data.length; i += w) {
      res.push(data.slice(i, i + w).join(seperator));
    }

    return res.join('\r\n');
  };

  return table;
}

/**
 * @param {number} w width
 * @param {number} h height
 * @param (number} totalMines total mines
 * @param {string} str the game board
 * @return {function}
 */
function resolve(w, h, totalMines, str) {
  /*
    '-': unknown
    'x': empty
    'o': bomb
    '0' - '9': count near by
  */
  var table = t(w, h, '-', 'x');

  // init
  var Ptr = p(w, h);
  var ptr = Ptr(0, 0);
  var temp = str.replace(/[\r\n]/g, '');

  for (let ptr = Ptr(0, 0); ptr; ptr = ptr.next()) {
    table(ptr, temp.slice(ptr.offset, ptr.offset + 1));
  }

  // found slot with no no number neighbors
  // true: has neighbor
  // false: no neighbor
  var mask = t(w, h, false, false);

  for (let ptr = Ptr(0, 0); ptr; ptr = ptr.next()) {
    let neightborPtrs = ptr.neighbors();
    mask(ptr, neightborPtrs.reduce(function (prev, ptr) {
      var val = table(ptr);
      return prev || !!val.match(/[0-9]/);
    }, false));
  }

  var unPredicableSlots = mask.count(false);
  var totalFoundMines = table.count('o');
  var minesLeft = totalMines - totalFoundMines;

  // situation when there are n mines drops in unprediactable area;
  var unPredicableMultiplier = {};
  var combinationCounts = {};
  var guessTables = [];

  for (let unPredicableCount = minesLeft; unPredicableCount >= 0; unPredicableCount--) {
    unPredicableMultiplier[unPredicableCount] = c(unPredicableSlots, unPredicableCount);
    combinationCounts[unPredicableCount] = 0;
    guessTables[unPredicableCount] = t(w, h, 0, 0);
  }

  // solve until there is no other possible combination
  function solve(ptr, currentTable, minesLeft) {
    // move until guessable Slot
    while (ptr && (!mask(ptr) || currentTable(ptr) !== '-')) {
      ptr = ptr.next();
    }

    if (!ptr && minesLeft >= 0) {
      // found a possible solution;
      // console.log('hit #' + combinationCounts[minesLeft] + '\r\n' + currentTable.toString());
      combinationCounts[minesLeft] += 1;
      for (let ptr = Ptr(0, 0); ptr; ptr = ptr.next()) {
        if (mask(ptr)) {
          var guessTable = guessTables[minesLeft];
          if (!guessTable) {
            console.log(minesLeft);
            return;
          }
          if (currentTable(ptr) === 'o') {
            guessTable(ptr, guessTable(ptr) + 1);
          }
        }
      }

      return;
    }

    // check if this slot can be mine or not be mine
    // and abort the branch if that branch failed;
    var neightbors = ptr.neighbors();
    var canBeMine = true;
    var canBeEmpty = true;

    neightbors.forEach(function (neightbor) {
      if (currentTable(neightbor).match(/[0-9\s]/)) {
        var neightborsOfNeighbor = neightbor.neighbors();
        var number = currentTable(neightbor);
        number = number === ' ' ? 0 : parseInt(number, 10);

        var minesNearBy = neightborsOfNeighbor.reduce(function (prev, curr) {
          if (currentTable(curr) === 'o') {
            return prev + 1;
          } else {
            return prev;
          }
        }, 0);

        var emptyNearBy = neightborsOfNeighbor.reduce(function (prev, curr) {
          if (currentTable(curr).match(/[0-9x\s]/)) {
            return prev + 1;
          } else {
            return prev;
          }
        }, 0);

        var unknownNearBy = 8 - minesNearBy - emptyNearBy;

        var minesToPlace = number - minesNearBy;
        if (minesToPlace === unknownNearBy) canBeEmpty = false;
        if (minesToPlace === 0) canBeMine = false;
      }
    });

    if (canBeMine && minesLeft !== 0) {
      let newTable = currentTable.clone();
      newTable(ptr, 'o');
      solve(ptr.next(), newTable, minesLeft - 1);
      // we still have mine, and we can put mine here
    }

    if (canBeEmpty) {
      let newTable = currentTable.clone();
      newTable(ptr, 'x');
      solve(ptr.next(), newTable, minesLeft);
    }

    // if (!canBeMine && !canBeEmpty) {
    //   console.log('badGuess\r\n' + currentTable)
    // }
  }

  solve(Ptr(0, 0), table.clone(), minesLeft);

  // console.log(unPredicableMultiplier)
  // console.log(combinationCounts)
  // console.log(guessTables.map(function (table, i) {
  //   return '# guessTable ' + i + '\r\n' + table.toString();
  // }).join('\r\n'))

  // sum up the possibility
  var finalBoardCount = 0;
  var finalBoard = t(w, h, 0, 0);

  for (let unPredicableCount = minesLeft; unPredicableCount >= 0; unPredicableCount--) {
    finalBoardCount += (unPredicableMultiplier[unPredicableCount] * combinationCounts[unPredicableCount]);

    // init possibility for unPredicable slot;
    for (let ptr = Ptr(0, 0); ptr; ptr = ptr.next()) {
      if (!mask(ptr)) {
        guessTables[unPredicableCount](ptr, combinationCounts[unPredicableCount] * (unPredicableCount / unPredicableSlots));
      }
    }

    for (let ptr = Ptr(0, 0); ptr; ptr = ptr.next()) {
      finalBoard(ptr, finalBoard(ptr) + guessTables[unPredicableCount](ptr) * unPredicableMultiplier[unPredicableCount]);
    }
  }

  if (finalBoardCount === 0) {
    throw new Error('invalid board');
  }

  function formatToPercent(num, small) {
    small = small || 2;
    var val = parseFloat(num * 100).toFixed(small) + "%";
    // xxx.<small>
    var totalLength = small + 5;
    while (val.length < totalLength) {
      val = ' ' + val;
    }
    return val;
  }

  var finalBoardPercent = t(w, h, "", "");

  for (let ptr = Ptr(0, 0); ptr; ptr = ptr.next()) {
    finalBoard(ptr, finalBoard(ptr) / finalBoardCount);
    finalBoardPercent(ptr, formatToPercent(finalBoard(ptr)));

    if (table(ptr) !== '-') {
      finalBoard(ptr, -1);
      finalBoardPercent(ptr, '  <N/A>');
    }
  }

  var map = {
    '-8': '░',
    '0': '□',
    '1': '▁',
    '2': '▂',
    '3': '▃',
    '4': '▄',
    '5': '▅',
    '6': '▆',
    '7': '▇',
    '8': '█'
  };

  var visualBoard = t(w, h, "  ", "  ");
  // var visualColorBoard = t(w, h, "  ", "  ");

  for (let ptr = Ptr(0, 0); ptr; ptr = ptr.next()) {
    visualBoard(ptr, map[Math.floor(finalBoard(ptr) * 8)]);
    // if(finalBoard(ptr) >= 0) {
    //   visualColorBoard(ptr, hslColor(finalBoard(ptr) / 3, 1, 0.5, true) + ' ' + table(ptr) + '\u001b[0m');
    // } else {
    //   visualColorBoard(ptr, hslColor(2 / 3, 1, 0.5, true) + ' ' + table(ptr) + '\u001b[0m' );
    // }
  }

  console.log('board with ' + totalMines + ' mines');
  console.log(table.toString());
  console.log('result is');
  console.log(finalBoardPercent.toString());
  console.log(visualBoard.toString(' '));
  // console.log(visualColorBoard.toString(''));
  return finalBoard;
}

var map = {
  '?': 'o',
  '?️': 'o',
  '1\u20E3': '1',
  '2\u20E3': '2',
  '3\u20E3': '3',
  '4\u20E3': '4',
  '5\u20E3': '5',
  '6\u20E3': '6',
  '7\u20E3': '7',
  '8\u20E3': '8',
  '9\u20E3': '9',
  ' ': '0',
  '⬜️': '-',
  '?': 'o',
};

var ended = /^? Winner\:/g;

function check() {
  var historyRoot = $('.im_history_messages_peer:visible');
  var topScope = historyRoot.scope();
  var messages = topScope.peerHistory.messages;
  var boardMesssages = messages.filter((m)=> m.viaBotID === 223493268);
  // interate through messages to find board
  // and find board element by id
  var messageEls = historyRoot.find('.im_content_message_wrap');
  boardMesssages.forEach((message)=>{
    var messageEl = messageEls.filter((i, el)=>{
      return $(el).scope().historyMessage.$$hashKey === message.$$hashKey;
    });

    if (messageEl.length > 0 && !messageEl.get(0).watching) {
      track(messageEl.scope(), messageEl);
    }
  });
}

if (typeof unsafeWindow !== 'undefined') {
  window.check = unsafeWindow.check = check;
} else {
  window.check = check;
}


function track($scope, $el) {
  var reply_markup = $scope.historyMessage.reply_markup.rows.map((i)=>i.buttons);
  var buttonEls = $el.find('.reply_markup_button_wrap');
  var reply_markup_els = reply_markup.map((row)=>row.map((button)=>
    buttonEls.filter((i,el)=>$(el).scope().button.$$hashKey === button.$$hashKey)
  ));
  console.log(reply_markup_els);

  if(reply_markup_els.length < 8) {
    // not yet started
    return;
  }

  //$el.get(0).watching = true;

  function compute() {
    var reply_markup = $scope.historyMessage.reply_markup.rows.map((i)=>i.buttons);
    var buttonEls = $el.find('.reply_markup_button_wrap');
    var reply_markup_els = reply_markup.map((row)=>row.map((button)=>
      buttonEls.filter((i,el)=>$(el).scope().button.$$hashKey === button.$$hashKey)
    ));

    console.log(reply_markup_els);
    reply_markup = reply_markup.slice(0, 8);
    reply_markup_els = reply_markup_els.slice(0, 8);
    var board = reply_markup.map((row)=>{
      return row.map((button)=>{
        var mapped = map[button.text];
        if (mapped == null) {
          throw new Error('cannot map ' + button.text);
        }
        return mapped;
      }).join('');
    }).join('\r\n');
    console.log(board);
    try {
    var result = resolve(7, 8, 15, board);
    var ptr =  p(7, 8)(0, 0);
    for (let ptr = p(7, 8)(0, 0); ptr; ptr = ptr.next()) {
      if (result(ptr) >= 0) {
        $(reply_markup_els[ptr.y][ptr.x]).find('button').css('background', hslColor(result(ptr) / 3, 1, 0.5));
      } else {
        console.log(hslColor(2 / 3, 1, 0.5));
        $(reply_markup_els[ptr.y][ptr.x]).find('button').css('background', hslColor(2 / 3, 1, 0.5));
      }
    }
    } catch(e){}
  }

  compute();

  $scope.$watch('historyMessage.message', compute);
}

setInterval(check, 10000);