From 94f97b5a8bcac6aeec3d370ab712a41aea2a50c4 Mon Sep 17 00:00:00 2001 From: Dmitry Khalansky Date: Thu, 14 Jul 2016 13:20:01 +0300 Subject: [PATCH 01/23] Started rewriting --- index.html | 1 + solver.js | 569 ++++++++++++++++------------------------------------- util.js | 65 ++++++ 3 files changed, 239 insertions(+), 396 deletions(-) create mode 100644 util.js diff --git a/index.html b/index.html index 32020c8..923ad5f 100644 --- a/index.html +++ b/index.html @@ -4,6 +4,7 @@ Антисапёр + diff --git a/solver.js b/solver.js index 48b98b2..4be0286 100644 --- a/solver.js +++ b/solver.js @@ -1,399 +1,176 @@ -function create_map(n, func) { - return Array.apply(null, new Array(+n)).map(function(x, index, arr) { - return func(index); - }); -} - -function foreach_obj(obj, func) { - for (var key in obj) { - if (obj.hasOwnProperty(key)) { - func(obj[key], key); - } - } -} - -function create_matrix(n, m, func) { - var result = create_map(n, function(i) { - return create_map(m, function (j) { - return func(i, j); - }) - }); - result.width = m; - result.height = n; - result.matrForEach = function (callback, thisArg) { - var that = thisArg ? thisArg : this; - for (var i = 0; i < result.height; ++i) { - for (var j = 0; j < result.width; ++j) { - callback(that[i][j], i, j, that); - } - } - }; - return result; -} - -function copy_matrix(matr) { - var result = []; - result.width = matr.width; - result.height = matr.height; - for (var i = 0; i < matr.height; ++i) { - result[i] = []; - for (var j = 0; j < matr.width; ++j) { - result[i][j] = matr[i][j]; - } - } - result.matrForEach = matr.matrForEach; - return result; -} - -function Sweep(width, height, mines) { - this.mines = +mines; - this.field = create_matrix(height, width, - function() { return new Cell('q') }); -} - -function Cell(type) { - function is_of_type (type) { - return function () { - return this.type === type - } - } - - this.type = type; - this.isUnknown = is_of_type('q'); - this.isEmpty = is_of_type('0'); - this.isFree = is_of_type('f'); - this.isBomb = is_of_type('b'); - this.isNumber = function () { - var eln = parseInt(this.type, 10); - return !isNaN(eln) && (eln !== 0) - }; -} - -function Position2D(row, col) { - this.row = +row; - this.col = +col; - this.toString = function () { return '(' + row + ', ' + col + ')' }; -} - -var solver = {}; - -solver.solve = function (swp) { - try { - var calc = this.calculate(swp); - } catch (e) { - alert(e.message); - return - } - calc.new_field.matrForEach(function (curr, i, j) { - if (!curr.isFree()) { - swp.field[i][j] = curr; - } - }); - return calc.probs; -} - -function fold_around (mi, mj, Mi, Mj, i, j, a, accFunc) { - if ((mi != i) && (mj != j)) { a = accFunc(a, i-1, j-1) } - if ((mi != i) && (Mj != j)) { a = accFunc(a, i-1, j+1) } - if ((Mi != i) && (mj != j)) { a = accFunc(a, i+1, j-1) } - if ((Mi != i) && (Mj != j)) { a = accFunc(a, i+1, j+1) } - if ((mi != i) ) { a = accFunc(a, i-1, j ) } - if ((Mi != i) ) { a = accFunc(a, i+1, j ) } - if ( (mj != j)) { a = accFunc(a, i , j-1) } - if ( (Mj != j)) { a = accFunc(a, i , j+1) } - return a; -} - -function Lead (required, unknowns) { - this.required = +required; - this.unknowns = unknowns; -} - -solver.get_rules = function (field) { - var w = field.width; - var h = field.height; - - function cell_name (i, j) { - return [i, j].join(';'); - } - - function acc_f (el, cell) { - if (!el.isBomb() && !el.isUnknown()) { - return function (a) { return a } - } - - return function (a, i, j) { - var e = field[i][j]; - if (!e.isNumber() && !e.isEmpty()) { - return a; - } - - var name = cell_name(i, j); - if (!a[name]) { - a[name] = new Lead(e.type, []); - a[name].pos = new Position2D(i, j); - } - - if (el.isBomb()) { - --a[name].required; - } else { - a[name].unknowns.push(cell); - cell.used = true; - } - return a; - } - } - - var unknowns = []; - var leads = {}; - var free_unknowns = []; - field.matrForEach(function (el, i, j) { - if (!el.isUnknown() && !el.isBomb()) { - return - } - var cell = new Position2D(i, j); - cell.used = false; - leads = fold_around(0, 0, h-1, w-1, i, j, leads, - acc_f(el, cell)); - if (el.isUnknown()) { - (cell.used ? unknowns : free_unknowns).push(cell); - } - delete cell.used; - }); - - this.delete_empty_rules(); - - return { - unknowns : unknowns, - leads : leads, - free_unknowns : free_unknowns - } -} - -solver.delete_empty_rules = function (leads) { - foreach_obj(leads, function (lead, key) { - // Check for validity - if ((lead.required > lead.unknowns.length) || - (lead.required < 0)) { - throw { - name : 'unexpected_cell_type', - pos : lead.pos, - message : 'The vicinity of the cell ' + - lead.pos + ' conflicts with its type' - } - } - // Remove the rules that can't provide any new information - if (!lead.required && !lead.unknowns.length) { - delete leads[key]; - } - }); -} +function test () { + var B = create_map(3, function(){return 1}); + B[1] = 2; + + /* AX = B : + * x 1 2 1 x + * x x x x x + */ + var A = create_matrix(3, 8, function(){return false}); + A[0][0] = A[0][1] = A[0][2] = A[0][3] = true; + A[1][2] = A[1][3] = A[1][4] = true; + A[2][3] = A[2][4] = A[2][5] = A[2][6] = true; + + /* CX = B, slightly artificial example: + * 1 _ 1 + * x 2 x + * _ x _ + */ + var C = create_matrix(3, 4, function(){return false}); + C[0][1] = true; + C[1][1] = C[1][2] = C[1][3] = true; + C[2][2] = true; + + return { + solve_ab : solve_for_rules(A, B, 2, 3), + elim_cb : eliminate_obvious_rules(C, B) + }; +} + +function eliminate_obvious_rules (A, B) { + var res = []; + var aWork = copy_matrix(A); + var bWork = B.slice(); + + var vars_set = 0; + var obvious; + do { + obvious = false; + + for (var i = 0; i < aWork.height; ++i) { + var rowCnt = 0; + for (var j = 0; j < aWork.width; ++j) { + rowCnt += aWork[i][j]; + } + if ((bWork[i] > rowCnt) || (bWork[i] < 0)) { + return null; + } + if (bWork[i] && (bWork[i] != rowCnt)) { + continue; + } + + obvious = true; + for (var j = 0; j < aWork.width + vars_set; ++j) { + if (!aWork[i][j-vars_set]) { + continue; + } + res[j] = bWork[i] != 0; + for (var z = 0; z < aWork.height; ++z) { + if (aWork[z][j-vars_set] && res[j]) { + --bWork[z]; + } + } + aWork = copy_matrix_wo_col(aWork, j-vars_set); + ++vars_set; + } + bWork.splice(i, 1); + aWork = copy_matrix_wo_row(aWork, i--); + } + } while (obvious); + + return { + A : aWork, + B : bWork, + res : res + } +} + +/* Find all possible solutions for A * X = B assuming that both a_ij and x_i + * are in {true, false}. + * + * A, B -- matrices to be solved; + * {min|max} -- if a number, sum of all x_i must be {more|less} than the value + * of this parameter. + * */ + +function solve_for_rules (A, B, min, max) { + var min = isInt(min) ? min : -1; + var max = isInt(max) ? max : Infinity; + + if (B.length != A.height) { + throw { + name : 'extended_matrix_incorrect', + text : B + ' can\'t be an extension of ' + A + + ' as their dimensions don\'t match' + } + } + + console.log({A : A, B : B, min : min, max : max}); + + var obvious = eliminate_obvious_rules(A, B); + console.log({After_elimination : obvious}); + if (obvious === null) { + return []; + } + var aWork = obvious.A; + var bWork = obvious.B; + var resTempl = obvious.res; + + var foundMines = 0; + for (var i = 0; i < resTempl.length; ++i) { + foundMines += i in resTempl && resTempl[i]; + } + min -= foundMines; + max -= foundMines; + + if (max < 0 || aWork.width < min) { + return []; + } + + if (aWork.width === 0) { + + for (var i = 0; i < B.length; ++i) { + if (B[i] != 0) { + return []; + } + } + + console.log('Success!'); + return [resTempl]; + } + + var res = []; + + var splitIdx = 0; + + var aWoSplit = copy_matrix_wo_col(aWork, splitIdx); + + /* Assuming the selected variable is `true` */ + { + console.log('TRUE'); + var newB = create_map(bWork.length, function(i) { + return A[i][splitIdx] ? bWork[i] - 1 : bWork[i]; + }); + var r = solve_for_rules (aWoSplit, newB, min - 1, max - 1); + if (r.length > 0) { + res = res.concat(r.map(function(el) { + el.splice(splitIdx, 0, true); return el;})); + } + console.log('OUT_TRUE'); + } + /* Assuming it's `false` */ + { + console.log('FALSE'); + var newB = bWork; + var r = solve_for_rules (aWoSplit, newB, min, max); + if (r.length > 0) { + res = res.concat(r.map(function(el) { + el.splice(splitIdx, 0, false); return el;})); + } + console.log('OUT_FALSE'); + } + + console.log({res : res, resTempl : resTempl}); + res = res.map(function(el) { + for (var i = 0; i < resTempl.length; ++i) { + if (i in resTempl) { + el.splice(i, 0, resTempl[i]); + } + } + return el; + }); + + return res; -solver.guess_cell_types = function(field) { - var h = field.height; - var w = field.width; - - function near_unknown_f (a, i, j) { - var el = field[i][j]; - a.near_unknown = a.near_unknown || el.isUnknown(); - if (el.isBomb()) { - ++a.mines_around; - } - return a - } - - field.matrForEach(function (el, i, j) { - var el = field[i][j]; - if (!el.isFree()) { - return - } - - var info = { - near_unknown : false, - mines_around : 0 - }; - - info = fold_around(0, 0, h-1, w-1, i, j, info, - near_unknown_f); - - field[i][j].type = info.near_unknown ? 'q' : - info.mines_around + ""; - }); - - return field; -} - -solver.calculate = function(swp) { - var field = copy_matrix(swp.field); - var totalUnknown = 0; - - var obvious; - var rules = this.get_rules(field); - do { - obvious = 0; - foreach_obj(rules.leads, function(lead) { - if (lead.required && (lead.required !== - lead.unknowns.length)) - { return } - - var type = lead.required ? 'b' : 'f'; - lead.unknowns.forEach(function (e) { - field[e.row][e.col] = new Cell(type); - }); - obvious += lead.unknowns.length; - }); - rules = this.get_rules(field); - } while (obvious); - - var bombs_found = 0; - - var probs = create_matrix(field.height, field.width, function (i, j) { - var el = field[i][j]; - if (el.isUnknown()) { - return -2; - } else if (el.isBomb()) { - ++bombs_found; - return 100; - } else if (el.isFree()) { - return 0; - } else { - return -1; - } - }); - var res = this.calculate_probabilities(rules, swp.mines - bombs_found); - var norm = this.normalized(res, rules.free_unknowns.length); - var update_probs = function (is_bound) { - return function (val, index) { - var curr_prob = is_bound ? norm.bound[index] : - norm.free; - probs[val.row][val.col] = curr_prob; - if (curr_prob === 0) { - field[val.row][val.col].type = 'f'; - } else if (curr_prob === 100) { - field[val.row][val.col].type = 'b'; - } - } - }; - rules.unknowns.forEach(update_probs(true)); - rules.free_unknowns.forEach(update_probs(false)); - - return { - probs : probs, - new_field : this.guess_cell_types(field) - } -} - -solver.count_mines = function (field) { - var result = 0; - field.matrForEach(function (el) { - if (el.isBomb()) { - ++result; - } - }); - return result; -} - -solver.is_correct = function (rules, mines_left) { - var result = true; - foreach_obj(rules.leads, function(lead) { - var consider = true; - var current = (lead.unknowns.filter(function (val) { - consider = consider && val.traversed; - return val.set - }).length === lead.required); - if (consider) { - result = result && current; - } - }); - return result; } -function combinations (n, k) { - if (n < k) { - return 0; - } - - var i; - var r = 1; - for (i = k + 1; i <= n; ++i) { - r *= i; - } - for (i = 1; i <= (n - k); ++i) { - r /= i; - } - return r; -} - -solver.calculate_probabilities = function (rules, mines_left, index) { - index = index ? index : 0; - var result = { - probs : create_map(rules.unknowns.length, function () { - return 0 - }), - prob_free : 0, - total : 0 - } - - if (index === rules.unknowns.length) { - if ((rules.free_unknowns.length < mines_left) || - (!this.is_correct(rules, mines_left))) - { return result } - - var modifier = combinations(rules.free_unknowns.length, - mines_left); - - var probs = []; - rules.unknowns.forEach(function (val, index) { - if (val.set) { - result.probs[index] += modifier; - } - }); - - result.prob_free += mines_left * modifier; - result.total += modifier; - return result; - } - - var s; - var curr = rules.unknowns[index]; - - if (!this.is_correct(rules, mines_left)) { - return result; - } - - curr.traversed = true; - - if (mines_left) { - curr.set = true; - --mines_left; - s = this.calculate_probabilities(rules, mines_left, - index + 1); - result.total += s.total; - s.probs.forEach(function (val, index) { - result.probs[index] += val; - }); - result.prob_free += s.prob_free; - curr.set = false; - ++mines_left; - } - - s = this.calculate_probabilities(rules, mines_left, - index + 1); - result.total += s.total; - result.prob_free += s.prob_free; - s.probs.forEach(function (val, index) { - result.probs[index] += val; - }); - - curr.traversed = false; - return result; -} - -solver.normalized = function (s, free_unknowns_cnt) { - return { - bound : s.probs.map(function (val) { - return val * 100 / s.total; - }), - free : s.prob_free * 100 / (s.total * free_unknowns_cnt) - } -} diff --git a/util.js b/util.js new file mode 100644 index 0000000..ca76ddc --- /dev/null +++ b/util.js @@ -0,0 +1,65 @@ +function isInt(n) { + return Number(n) === n && n % 1 === 0; +} + +function create_map(n, func) { + var result = []; + var i; + for (i = 0; i < n; ++i) { + result.push(func(i)); + } + return result; +} + +function create_matrix(n, m, func) { + var i, j; + var result = create_map(n, function(i) { + return create_map(m, function (j) { + return func(i, j); + }); + }); + result.width = m; + result.height = n; + result.forEach = function (callback, thisArg) { + var that = thisArg || this; + for (i = 0; i < result.height; ++i) { + for (j = 0; j < result.width; ++j) { + callback(that[i][j], i, j, that); + } + } + }; + return result; +} + +function copy_matrix(matr) { + var i, j; + var result = []; + result.width = matr.width; + result.height = matr.height; + for (i = 0; i < matr.height; ++i) { + result[i] = []; + for (j = 0; j < matr.width; ++j) { + result[i][j] = matr[i][j]; + } + } + result.matrForEach = matr.matrForEach; + return result; +} + +function copy_matrix_wo_col(matr, col) { + return create_matrix(matr.height, matr.width - 1, function(i, j) { + return j < col ? matr[i][j] : matr[i][j+1]; + }); +} + +function copy_matrix_wo_row(matr, row) { + return create_matrix(matr.height - 1, matr.width, function(i, j) { + return i < row ? matr[i][j] : matr[i+1][j]; + }); +} + +function cr_const(n) { + return function() { + return n; + }; +} From 5e9113805e9821558eadab9eae3dffa3b395e393 Mon Sep 17 00:00:00 2001 From: Dmitry Khalansky Date: Thu, 10 Nov 2016 12:59:55 +0300 Subject: [PATCH 02/23] Implemented splitting the graph components If we verify the states of combinations of N boolean variables, it is in general case a problem requiring 2^N iterations. However, if N variables can be split into M independent components of length p(m), m \in M then we require just \sum 2^{p(m)}. If N is divided evenly into M components, for example, then we require just M 2^{N/M} iterations. For N = 100 and M = 5, the naive method requires (2^N) / (M 2^{N/M}) = 1/M 2^{N - N/M} \approx 2.4e23 times more iterations. --- solver.js | 53 ++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 52 insertions(+), 1 deletion(-) diff --git a/solver.js b/solver.js index 4be0286..b9cb8b4 100644 --- a/solver.js +++ b/solver.js @@ -21,9 +21,19 @@ function test () { C[1][1] = C[1][2] = C[1][3] = true; C[2][2] = true; + /* DX = B: + * 1 2 x x 1 + * _ x x x x + */ + var D = create_matrix(3, 6, function(){return false}); + D[0][0] = true; + D[1][0] = D[1][1] = D[1][2] = true; + D[2][3] = D[2][4] = D[2][5] = true; + return { solve_ab : solve_for_rules(A, B, 2, 3), - elim_cb : eliminate_obvious_rules(C, B) + elim_cb : eliminate_obvious_rules(C, B), + split : split_at_compontents(D, B) }; } @@ -75,6 +85,47 @@ function eliminate_obvious_rules (A, B) { } } +function split_at_compontents (A, B) { + var res = []; + var assigned = Object.create(null); + for (var i = 0; i < A.height; ++i) { + if (i in assigned) + continue; + + var comp = []; + var curr = Object.create(null); + for (var j = 0; j < A.width; ++j) { + if (A[i][j]) { + curr[j] = true; + } + } + + for (var k = i; k < A.height; ++k) { + if (k in assigned) + continue; + + for (var j = 0; j < A.width; ++j) { + if (curr[j] && A[k][j]) { + comp.push(k); + assigned[k] = true; + break; + } + } + } + + var a = create_matrix(comp.length, A.width, function (i, j) { + return A[comp[i]][j]; + }); + + var b = create_map(comp.length, function(i) { + return B[comp[i]]; + }); + + res.push({a: a, b: b}); + } + return res; +} + /* Find all possible solutions for A * X = B assuming that both a_ij and x_i * are in {true, false}. * From d160151f658030ebc71736317c0289a7a4ccdead Mon Sep 17 00:00:00 2001 From: Dmitry Khalansky Date: Tue, 15 Nov 2016 15:20:01 +0300 Subject: [PATCH 03/23] Improve unit tests As the logic of the application grows it becomes harder to analyse the results of a test evaluation manually. Thus, the tests now themselves know which results to expect. --- solver.js | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/solver.js b/solver.js index b9cb8b4..29e8091 100644 --- a/solver.js +++ b/solver.js @@ -11,15 +11,28 @@ function test () { A[1][2] = A[1][3] = A[1][4] = true; A[2][3] = A[2][4] = A[2][5] = A[2][6] = true; + var solve_ab = solve_for_rules(A, B, 2, 3); + var solve_ab_cond = solve_ab.toString() === + "false,false,true,false,true,false,false,true," + + "false,false,true,false,true,false,false,false"; + + console.log("Simple test: " + solve_ab_cond); + /* CX = B, slightly artificial example: * 1 _ 1 * x 2 x * _ x _ */ - var C = create_matrix(3, 4, function(){return false}); - C[0][1] = true; - C[1][1] = C[1][2] = C[1][3] = true; - C[2][2] = true; + var C = create_matrix(3, 3, function(){return false}); + C[0][0] = true; + C[1][0] = C[1][1] = C[1][2] = true; + C[2][1] = true; + + var elim_cb = eliminate_obvious_rules(C, B); + var elim_cb_cond = elim_cb.A.height === 0 && elim_cb.B.length === 0 && + elim_cb.res.toString() === "true,true,false"; + + console.log("Elimination of obvious rules: " + elim_cb_cond); /* DX = B: * 1 2 x x 1 From bb37723908e043f670927ae652de535af84c1b96 Mon Sep 17 00:00:00 2001 From: Dmitry Khalansky Date: Tue, 15 Nov 2016 15:22:00 +0300 Subject: [PATCH 04/23] Fix a typo, less redundant unit tests compontents -> components. The unit tests are no longer evaluated twice needlessly. --- solver.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/solver.js b/solver.js index 29e8091..7ff0c26 100644 --- a/solver.js +++ b/solver.js @@ -44,9 +44,9 @@ function test () { D[2][3] = D[2][4] = D[2][5] = true; return { - solve_ab : solve_for_rules(A, B, 2, 3), - elim_cb : eliminate_obvious_rules(C, B), - split : split_at_compontents(D, B) + solve_ab : solve_ab, + elim_cb : elim_cb, + split : split_at_components(D, B) }; } @@ -98,7 +98,7 @@ function eliminate_obvious_rules (A, B) { } } -function split_at_compontents (A, B) { +function split_at_components (A, B) { var res = []; var assigned = Object.create(null); for (var i = 0; i < A.height; ++i) { From 88703c03b57adadefedec1490f70cbf941d59962 Mon Sep 17 00:00:00 2001 From: Dmitry Khalansky Date: Tue, 15 Nov 2016 15:23:53 +0300 Subject: [PATCH 05/23] Index set is returned with graph components After we're done with each component of our graph, we'll have to somehow find correspondence between these arrays and the initial data. So we return a set of indexes of original matrices that are present in each component. The order is preserved, so this is enough. --- solver.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/solver.js b/solver.js index 7ff0c26..d3cf153 100644 --- a/solver.js +++ b/solver.js @@ -134,7 +134,9 @@ function split_at_components (A, B) { return B[comp[i]]; }); - res.push({a: a, b: b}); + var ixs = comp; + + res.push({a: a, b: b, ixs: ixs}); } return res; } From 5b041c2d1e8a3f14610a8bab069fd16e5f8590ed Mon Sep 17 00:00:00 2001 From: Dmitry Khalansky Date: Tue, 15 Nov 2016 15:28:00 +0300 Subject: [PATCH 06/23] More optimal splitting index There may be matrices where the first `x` columns only contain 0. So we skip these columns and only consider those that have values. --- solver.js | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/solver.js b/solver.js index d3cf153..a9c83fb 100644 --- a/solver.js +++ b/solver.js @@ -141,6 +141,16 @@ function split_at_components (A, B) { return res; } +function partition_split_idx (A, B) { + for (var idx = 0; idx < A.width; ++idx) { + for (var r = 0; r < A.height; ++r) { + if (A[r][idx]) + return idx; + } + } + return null; +} + /* Find all possible solutions for A * X = B assuming that both a_ij and x_i * are in {true, false}. * @@ -197,7 +207,7 @@ function solve_for_rules (A, B, min, max) { var res = []; - var splitIdx = 0; + var splitIdx = partition_split_idx(aWork, bWork); var aWoSplit = copy_matrix_wo_col(aWork, splitIdx); From 73f470605feb164cce74b3774f79b3c7bd4104d9 Mon Sep 17 00:00:00 2001 From: Dmitry Khalansky Date: Tue, 15 Nov 2016 16:36:00 +0300 Subject: [PATCH 07/23] Fix indentation Consistently using tabs everywhere in *.js-files. --- classes.js | 40 +++---- sweep.js | 328 ++++++++++++++++++++++++++--------------------------- util.js | 70 ++++++------ 3 files changed, 219 insertions(+), 219 deletions(-) diff --git a/classes.js b/classes.js index 17cb014..922a7da 100644 --- a/classes.js +++ b/classes.js @@ -1,29 +1,29 @@ function Sweep(width, height, mines) { - this.mines = +mines; - this.field = create_matrix(height, width, - function() { return new Cell('q') }); + this.mines = +mines; + this.field = create_matrix(height, width, + function() { return new Cell('q') }); } function Cell(type) { - function is_of_type (type) { - return function () { - return this.type === type - } - } + function is_of_type (type) { + return function () { + return this.type === type + } + } - this.type = type; - this.isUnknown = is_of_type('q'); - this.isEmpty = is_of_type('0'); - this.isFree = is_of_type('f'); - this.isBomb = is_of_type('b'); - this.isNumber = function () { - var eln = parseInt(this.type, 10); - return !isNaN(eln) && (eln !== 0) - }; + this.type = type; + this.isUnknown = is_of_type('q'); + this.isEmpty = is_of_type('0'); + this.isFree = is_of_type('f'); + this.isBomb = is_of_type('b'); + this.isNumber = function () { + var eln = parseInt(this.type, 10); + return !isNaN(eln) && (eln !== 0) + }; } function Position2D(row, col) { - this.row = +row; - this.col = +col; - this.toString = function () { return '(' + row + ', ' + col + ')' }; + this.row = +row; + this.col = +col; + this.toString = function () { return '(' + row + ', ' + col + ')' }; } diff --git a/sweep.js b/sweep.js index e02776c..1a7417a 100644 --- a/sweep.js +++ b/sweep.js @@ -1,210 +1,210 @@ var csweep; function td_id(name, i, j) { - return [name, 'row', i, 'col', j].join('_'); + return [name, 'row', i, 'col', j].join('_'); } function remove_children(node) { - while (node.firstChild) { - node.removeChild(node.firstChild); - } + while (node.firstChild) { + node.removeChild(node.firstChild); + } } function sweep_to_table(swp, table, cellFunc, probs) { - remove_children(table); - for (var i = 0; i < swp.field.height; ++i) { - var newTr = document.createElement('tr'); - for (var j = 0; j < swp.field.width; ++j) { - newTr.appendChild(cellFunc(swp, i, j, probs)); - } - table.appendChild(newTr); - } - return table; + remove_children(table); + for (var i = 0; i < swp.field.height; ++i) { + var newTr = document.createElement('tr'); + for (var j = 0; j < swp.field.width; ++j) { + newTr.appendChild(cellFunc(swp, i, j, probs)); + } + table.appendChild(newTr); + } + return table; } function update_field(probs) { - sweep_to_table(csweep, document.getElementById('sweep_field'), - create_cell, probs); + sweep_to_table(csweep, document.getElementById('sweep_field'), + create_cell, probs); } function update_popup (e) { - var popup = document.getElementById('popup'); - if (!popup) { - return - } - popup.style.left = (e.clientX) + 'px'; - popup.style.top = (e.clientY) + 'px'; + var popup = document.getElementById('popup'); + if (!popup) { + return + } + popup.style.left = (e.clientX) + 'px'; + popup.style.top = (e.clientY) + 'px'; } function create_cell(swp, i, j, probs) { - var callbacks = {}; - if (probs) { - callbacks.mouseover = function (e) { - var prob = probs[i][j]; - if (prob === -1) { - return; - } - var popup = document.createElement('div'); - popup.id = 'popup'; - var text = prob.toPrecision(3) + '%'; - var textNode = document.createTextNode(text); - popup.appendChild(textNode); - document.getElementsByTagName('body' - )[0].appendChild(popup); - update_popup (e); - } - callbacks.mouseout = function (e) { - var popup = document.getElementById('popup'); - if (popup) { - popup.remove(); - } - } - } - var newCell = cell_to_dom(swp.field[i][j], callbacks); - newCell.id = td_id('sweep_field', i, j); - if (probs) { - var color = get_color(probs[i][j]); - newCell.style.backgroundColor = color; - } - pos = new Position2D (i, j); - newCell.addEventListener("click", get_dialog(pos)); - return newCell; + var callbacks = {}; + if (probs) { + callbacks.mouseover = function (e) { + var prob = probs[i][j]; + if (prob === -1) { + return; + } + var popup = document.createElement('div'); + popup.id = 'popup'; + var text = prob.toPrecision(3) + '%'; + var textNode = document.createTextNode(text); + popup.appendChild(textNode); + document.getElementsByTagName('body' + )[0].appendChild(popup); + update_popup (e); + } + callbacks.mouseout = function (e) { + var popup = document.getElementById('popup'); + if (popup) { + popup.remove(); + } + } + } + var newCell = cell_to_dom(swp.field[i][j], callbacks); + newCell.id = td_id('sweep_field', i, j); + if (probs) { + var color = get_color(probs[i][j]); + newCell.style.backgroundColor = color; + } + pos = new Position2D (i, j); + newCell.addEventListener("click", get_dialog(pos)); + return newCell; } function cell_to_dom (cell, callbacks) { - var type = cell.type; - var res = document.createElement ('td'); - res.className += ['cell', type].join('_'); - var text; - if (cell.isBomb()) { - text = '⚑'; - } else if (cell.isUnknown() || cell.isEmpty()) { - text = ''; - } else { - text = type; - } - var textNode = document.createTextNode(text); - var boldText = document.createElement("b"); - boldText.appendChild(textNode); - res.appendChild(boldText); - - for (var name in callbacks) { - if (callbacks.hasOwnProperty(name)) { - res.addEventListener(name, callbacks[name]); - } - } - - return res; + var type = cell.type; + var res = document.createElement ('td'); + res.className += ['cell', type].join('_'); + var text; + if (cell.isBomb()) { + text = '⚑'; + } else if (cell.isUnknown() || cell.isEmpty()) { + text = ''; + } else { + text = type; + } + var textNode = document.createTextNode(text); + var boldText = document.createElement("b"); + boldText.appendChild(textNode); + res.appendChild(boldText); + + for (var name in callbacks) { + if (callbacks.hasOwnProperty(name)) { + res.addEventListener(name, callbacks[name]); + } + } + + return res; } function close_dialog() { - var old_dialog = document.getElementById('dialog'); - if (old_dialog) { - old_dialog.remove(); - } + var old_dialog = document.getElementById('dialog'); + if (old_dialog) { + old_dialog.remove(); + } } function get_dialog(pos) { - return function (e) { - close_dialog(); - var dialog = document.createElement('table'); - dialog.className = 'dialog'; - dialog.id = 'dialog'; - dialog.style['position'] = 'absolute'; - dialog.style['left'] = e.clientX + 'px'; - dialog.style['top'] = e.clientY + 'px'; - var types = [ '1', '2', '3', '4', '5', '6', '7', '8', - 'q', 'b', '0' ]; - function get_click_callback(type) { - return function () { - csweep.field[pos.row][pos.col].type = type; - close_dialog(); - update_field(); - } - } - var width = 4; - for (var i = 0; i < Math.ceil(types.length / width); ++i) { - var row = document.createElement ('tr'); - for (var j = 0; j < width; ++j) { - var type = types[i * width + j]; - if (!type) { - break; - } - var callbacks = { - click : get_click_callback(type) - }; - var cell = cell_to_dom(new Cell(type), - callbacks); - row.appendChild(cell); - } - dialog.appendChild(row); - } - var body = document.getElementsByTagName('body')[0]; - body.appendChild(dialog); - } + return function (e) { + close_dialog(); + var dialog = document.createElement('table'); + dialog.className = 'dialog'; + dialog.id = 'dialog'; + dialog.style['position'] = 'absolute'; + dialog.style['left'] = e.clientX + 'px'; + dialog.style['top'] = e.clientY + 'px'; + var types = [ '1', '2', '3', '4', '5', '6', '7', '8', + 'q', 'b', '0' ]; + function get_click_callback(type) { + return function () { + csweep.field[pos.row][pos.col].type = type; + close_dialog(); + update_field(); + } + } + var width = 4; + for (var i = 0; i < Math.ceil(types.length / width); ++i) { + var row = document.createElement ('tr'); + for (var j = 0; j < width; ++j) { + var type = types[i * width + j]; + if (!type) { + break; + } + var callbacks = { + click : get_click_callback(type) + }; + var cell = cell_to_dom(new Cell(type), + callbacks); + row.appendChild(cell); + } + dialog.appendChild(row); + } + var body = document.getElementsByTagName('body')[0]; + body.appendChild(dialog); + } } function apply_form() { - var width = document.getElementById('field_width').value; - var height = document.getElementById('field_height').value; - var mines = document.getElementById('mines_count').value; - csweep = new Sweep(width, height, mines); - update_field(); - return false; + var width = document.getElementById('field_width').value; + var height = document.getElementById('field_height').value; + var mines = document.getElementById('mines_count').value; + csweep = new Sweep(width, height, mines); + update_field(); + return false; } function restore_form() { - document.getElementById('field_width' ).value = csweep.field.width; - document.getElementById('field_height').value = csweep.field.height; - document.getElementById('mines_count' ).value = csweep.mines; + document.getElementById('field_width' ).value = csweep.field.width; + document.getElementById('field_height').value = csweep.field.height; + document.getElementById('mines_count' ).value = csweep.mines; } function set_initial_page() { - csweep = new Sweep(4, 4, 6); - restore_form(); - apply_form(); - document.addEventListener('mousemove', update_popup); - document.addEventListener('click', function(e) { - var target = e.target; - while (target !== null) { - if (/sweep_field_/.test(target.id)) { - return; - } - target = target.parentElement; - } - close_dialog(); - }); + csweep = new Sweep(4, 4, 6); + restore_form(); + apply_form(); + document.addEventListener('mousemove', update_popup); + document.addEventListener('click', function(e) { + var target = e.target; + while (target !== null) { + if (/sweep_field_/.test(target.id)) { + return; + } + target = target.parentElement; + } + close_dialog(); + }); } function solve() { - var probs = solver.solve(csweep); - update_field(probs); + var probs = solver.solve(csweep); + update_field(probs); } function to_hex(c) { - var hex = Math.floor(c).toString(16); - return hex.length == 1 ? "0" + hex : hex; + var hex = Math.floor(c).toString(16); + return hex.length == 1 ? "0" + hex : hex; } function get_color(prob) { - var red, green, blue; - switch (prob) { - case 100 : - red = 255; - green = 0; - break; - case 0 : - red = 0; - green = 255; - break; - case -1 : - return undefined - default : - red = 255 * prob / 100; - green = 255 * (100 - prob) / 200; - } - var red = to_hex(red); - var green = to_hex(green); - var blue = to_hex(0); - return "#" + red + green + blue; + var red, green, blue; + switch (prob) { + case 100 : + red = 255; + green = 0; + break; + case 0 : + red = 0; + green = 255; + break; + case -1 : + return undefined + default : + red = 255 * prob / 100; + green = 255 * (100 - prob) / 200; + } + var red = to_hex(red); + var green = to_hex(green); + var blue = to_hex(0); + return "#" + red + green + blue; } diff --git a/util.js b/util.js index ca76ddc..8a87377 100644 --- a/util.js +++ b/util.js @@ -3,47 +3,47 @@ function isInt(n) { } function create_map(n, func) { - var result = []; - var i; - for (i = 0; i < n; ++i) { - result.push(func(i)); - } - return result; + var result = []; + var i; + for (i = 0; i < n; ++i) { + result.push(func(i)); + } + return result; } function create_matrix(n, m, func) { - var i, j; - var result = create_map(n, function(i) { - return create_map(m, function (j) { - return func(i, j); - }); - }); - result.width = m; - result.height = n; - result.forEach = function (callback, thisArg) { - var that = thisArg || this; - for (i = 0; i < result.height; ++i) { - for (j = 0; j < result.width; ++j) { - callback(that[i][j], i, j, that); - } - } - }; - return result; + var i, j; + var result = create_map(n, function(i) { + return create_map(m, function (j) { + return func(i, j); + }); + }); + result.width = m; + result.height = n; + result.forEach = function (callback, thisArg) { + var that = thisArg || this; + for (i = 0; i < result.height; ++i) { + for (j = 0; j < result.width; ++j) { + callback(that[i][j], i, j, that); + } + } + }; + return result; } function copy_matrix(matr) { - var i, j; - var result = []; - result.width = matr.width; - result.height = matr.height; - for (i = 0; i < matr.height; ++i) { - result[i] = []; - for (j = 0; j < matr.width; ++j) { - result[i][j] = matr[i][j]; - } - } - result.matrForEach = matr.matrForEach; - return result; + var i, j; + var result = []; + result.width = matr.width; + result.height = matr.height; + for (i = 0; i < matr.height; ++i) { + result[i] = []; + for (j = 0; j < matr.width; ++j) { + result[i][j] = matr[i][j]; + } + } + result.matrForEach = matr.matrForEach; + return result; } function copy_matrix_wo_col(matr, col) { From f5207cea1f6ab20e5bcfc0507768807b06ed9dad Mon Sep 17 00:00:00 2001 From: Dmitry Khalansky Date: Tue, 15 Nov 2016 18:37:20 +0300 Subject: [PATCH 08/23] Implement a function that calls the solver Solver is called once the green button is pressed. Its results aren't yet displayed to user in any helpful way but they're written to console. It should allow for more rapid manual testing with examples. --- sweep.js | 46 ++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 44 insertions(+), 2 deletions(-) diff --git a/sweep.js b/sweep.js index 1a7417a..6b49944 100644 --- a/sweep.js +++ b/sweep.js @@ -177,8 +177,50 @@ function set_initial_page() { } function solve() { - var probs = solver.solve(csweep); - update_field(probs); + var number_idxs = []; + var bomb_idxs = []; + var free_idxs = []; + csweep.field.forEach(function (c, i, j) { + var idx = i * csweep.field.width + j + if (c.isNumber() || c.isEmpty()) { + number_idxs.push(idx) + } else if (c.isUnknown()) { + free_idxs.push(idx) + } else if (c.isBomb()) { + bomb_idxs.push(idx); + } + }); + + var w = Number(csweep.field.width); + var d = [0, 1, 2, w, w + 2, 2 * w, 2 * w + 1, 2 * w + 2]; + + var A = create_matrix(number_idxs.length, free_idxs.length, + function () { return false; }); + var B = create_map(number_idxs.length, function (idx) { + var nidx = number_idxs[idx]; + return Number(csweep.field[ + Math.floor(nidx / w)][nidx % w].type); }); + + for (var i = 0; i < number_idxs.length; ++i) { + var nidx = number_idxs[i]; + for (var j = 0; j < free_idxs.length; ++j) { + if (d.indexOf(nidx + w + 1 - free_idxs[j]) !== -1) { + A[i][j] = true; + } + } + for (var j = 0; j < bomb_idxs.length; ++j) { + if (d.indexOf(nidx + w + 1 - bomb_idxs[j]) !== -1) { + --B[i]; + } + } + } + + var mines = csweep.mines - bomb_idxs.length; + var res = solve_for_rules(A, B, mines, mines); + + console.log(res); + + update_field(); } function to_hex(c) { From 968d8034ecc0bcb0a84a6f740f82c88df75526a1 Mon Sep 17 00:00:00 2001 From: Dmitry Khalansky Date: Tue, 15 Nov 2016 19:18:32 +0300 Subject: [PATCH 09/23] Implement displaying of results of calculation Now the field shows with color the estimated probabilities of mine location in each specific cell. --- sweep.js | 31 ++++++++++++++++++++++++++----- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/sweep.js b/sweep.js index 6b49944..b29fbe3 100644 --- a/sweep.js +++ b/sweep.js @@ -46,7 +46,7 @@ function create_cell(swp, i, j, probs) { } var popup = document.createElement('div'); popup.id = 'popup'; - var text = prob.toPrecision(3) + '%'; + var text = (prob * 100).toPrecision(3) + '%'; var textNode = document.createTextNode(text); popup.appendChild(textNode); document.getElementsByTagName('body' @@ -218,9 +218,30 @@ function solve() { var mines = csweep.mines - bomb_idxs.length; var res = solve_for_rules(A, B, mines, mines); - console.log(res); + if (res) { + var probs = create_matrix(csweep.field.height, w, + function() { return 0; }); - update_field(); + for (var i = 0; i < bomb_idxs.length; ++i) { + var bidx = bomb_idxs[i]; + probs[Math.floor(bidx / w)][bidx % w] = -1; + } + + for (var i = 0; i < number_idxs.length; ++i) { + var nidx = number_idxs[i]; + probs[Math.floor(nidx / w)][nidx % w] = -1; + } + + for (var i = 0; i < free_idxs.length; ++i) { + var fidx = free_idxs[i]; + for (var j = 0; j < res.length; ++j) { + probs[Math.floor(fidx / w)][fidx % w] + += res[j][i]; + } + probs[Math.floor(fidx / w)][fidx % w] /= res.length; + } + update_field(probs); + } } function to_hex(c) { @@ -242,8 +263,8 @@ function get_color(prob) { case -1 : return undefined default : - red = 255 * prob / 100; - green = 255 * (100 - prob) / 200; + red = 255 * prob; + green = 255 * (1 - prob); } var red = to_hex(red); var green = to_hex(green); From d2992b72deca36ffb708b3a04fbe1098fa069904 Mon Sep 17 00:00:00 2001 From: Dmitry Khalansky Date: Tue, 15 Nov 2016 19:40:24 +0300 Subject: [PATCH 10/23] Fix operation that determines if cells are near It used to perform some arithmetics that considered the last cell of line M a neighbor of the first cell of line M + 1. Fixed this. --- sweep.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/sweep.js b/sweep.js index b29fbe3..0d46cca 100644 --- a/sweep.js +++ b/sweep.js @@ -192,7 +192,6 @@ function solve() { }); var w = Number(csweep.field.width); - var d = [0, 1, 2, w, w + 2, 2 * w, 2 * w + 1, 2 * w + 2]; var A = create_matrix(number_idxs.length, free_idxs.length, function () { return false; }); @@ -201,15 +200,20 @@ function solve() { return Number(csweep.field[ Math.floor(nidx / w)][nidx % w].type); }); + var near = function (i, j) { + return Math.abs(Math.floor(i / w) - Math.floor(j / w)) <= 1 && + Math.abs((i % w) - (j % w)) <= 1 + }; + for (var i = 0; i < number_idxs.length; ++i) { var nidx = number_idxs[i]; for (var j = 0; j < free_idxs.length; ++j) { - if (d.indexOf(nidx + w + 1 - free_idxs[j]) !== -1) { + if (near(nidx, free_idxs[j])) { A[i][j] = true; } } for (var j = 0; j < bomb_idxs.length; ++j) { - if (d.indexOf(nidx + w + 1 - bomb_idxs[j]) !== -1) { + if (near(nidx, bomb_idxs[j])) { --B[i]; } } From 7431d5aff5a4b6aa267ddd87014e3aff62cb826e Mon Sep 17 00:00:00 2001 From: Dmitry Khalansky Date: Wed, 16 Nov 2016 17:46:01 +0300 Subject: [PATCH 11/23] Multiple fixes to algorithm As soon as it became possible to test the algorithm interactively, a multitude of problems emerged. We can hope that now they're fixed: for small examples at least they give the correct results. --- solver.js | 39 +++++++++++++++++++++++---------------- 1 file changed, 23 insertions(+), 16 deletions(-) diff --git a/solver.js b/solver.js index a9c83fb..6b5ff49 100644 --- a/solver.js +++ b/solver.js @@ -55,7 +55,8 @@ function eliminate_obvious_rules (A, B) { var aWork = copy_matrix(A); var bWork = B.slice(); - var vars_set = 0; + var idxs = create_map(aWork.width, function(i) { return i }); + var obvious; do { obvious = false; @@ -68,23 +69,28 @@ function eliminate_obvious_rules (A, B) { if ((bWork[i] > rowCnt) || (bWork[i] < 0)) { return null; } - if (bWork[i] && (bWork[i] != rowCnt)) { + if (bWork[i] && (bWork[i] !== rowCnt)) { continue; } obvious = true; - for (var j = 0; j < aWork.width + vars_set; ++j) { - if (!aWork[i][j-vars_set]) { + for (var j = 0; j < aWork.width; ++j){ + if (!aWork[i][j]) { continue; } - res[j] = bWork[i] != 0; - for (var z = 0; z < aWork.height; ++z) { - if (aWork[z][j-vars_set] && res[j]) { - --bWork[z]; + res[idxs[j]] = bWork[i] !== 0; + if (res[idxs[j]]) { + for (var z = 0; z < aWork.height; ++z) { + if (aWork[z][j]) { + --bWork[z]; + } } } - aWork = copy_matrix_wo_col(aWork, j-vars_set); - ++vars_set; + aWork = copy_matrix_wo_col(aWork, j); + idxs = create_map(aWork.width, function(i) { + return idxs[i + (i >= j)]; + }); + --j; } bWork.splice(i, 1); aWork = copy_matrix_wo_row(aWork, i--); @@ -195,8 +201,8 @@ function solve_for_rules (A, B, min, max) { if (aWork.width === 0) { - for (var i = 0; i < B.length; ++i) { - if (B[i] != 0) { + for (var i = 0; i < bWork.length; ++i) { + if (bWork[i] !== 0) { return []; } } @@ -213,10 +219,10 @@ function solve_for_rules (A, B, min, max) { /* Assuming the selected variable is `true` */ { - console.log('TRUE'); var newB = create_map(bWork.length, function(i) { - return A[i][splitIdx] ? bWork[i] - 1 : bWork[i]; + return aWork[i][splitIdx] ? bWork[i] - 1 : bWork[i]; }); + console.log('TRUE', {newB: newB}, splitIdx); var r = solve_for_rules (aWoSplit, newB, min - 1, max - 1); if (r.length > 0) { res = res.concat(r.map(function(el) { @@ -224,10 +230,11 @@ function solve_for_rules (A, B, min, max) { } console.log('OUT_TRUE'); } + console.log({type: "A", res : res, resTempl : resTempl}); /* Assuming it's `false` */ { - console.log('FALSE'); var newB = bWork; + console.log('FALSE', {newB: newB}); var r = solve_for_rules (aWoSplit, newB, min, max); if (r.length > 0) { res = res.concat(r.map(function(el) { @@ -236,7 +243,7 @@ function solve_for_rules (A, B, min, max) { console.log('OUT_FALSE'); } - console.log({res : res, resTempl : resTempl}); + console.log({type: "B", res : res, resTempl : resTempl}); res = res.map(function(el) { for (var i = 0; i < resTempl.length; ++i) { if (i in resTempl) { From c840a5bd40bfbd6473fea0bce1fe706b00cf3fba Mon Sep 17 00:00:00 2001 From: Dmitry Khalansky Date: Thu, 17 Nov 2016 13:00:42 +0300 Subject: [PATCH 12/23] Enhance unit tests for algorithm Added new tests that cover the problems we've had to fix which weren't detected by the previous set of tests. Performed some refactoring to remove boilerplate code from unit tests. --- solver.js | 61 +++++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 53 insertions(+), 8 deletions(-) diff --git a/solver.js b/solver.js index 6b5ff49..1b5d6ec 100644 --- a/solver.js +++ b/solver.js @@ -1,9 +1,25 @@ function test () { - var B = create_map(3, function(){return 1}); - B[1] = 2; + + function display(name, results) { + console.log(name + ": " + results.map( + function(c) { if (c) { + return "." + } else { + return "x" + }}).join('') + ); + } + + function check_results(solution, correct) { + return solution.map(function(x) { + x.toString() + }).sort().toString() === correct.map(function(x) { + x.toString() + }).sort().toString(); + } /* AX = B : - * x 1 2 1 x + * x 1 2 1 x x * x x x x x */ var A = create_matrix(3, 8, function(){return false}); @@ -11,12 +27,41 @@ function test () { A[1][2] = A[1][3] = A[1][4] = true; A[2][3] = A[2][4] = A[2][5] = A[2][6] = true; + var B = create_map(3, function(){return 1}); + B[1] = 2; + var solve_ab = solve_for_rules(A, B, 2, 3); - var solve_ab_cond = solve_ab.toString() === - "false,false,true,false,true,false,false,true," + - "false,false,true,false,true,false,false,false"; + var solve_ab_cond = check_results(solve_ab, + [[false, false, true, false, true, false, false, true], + [false, false, true, false, true, false, false, false]] + ); + + /* CX = D : + * 2 x x + * x x 2 + */ + var C = create_matrix(2, 4, function(){return true}); + C[0][1] = false; + C[1][2] = false; + + var D = create_map(2, function(){return 2}); + + var solve_cd = solve_for_rules(C, D, 3, 3); + var solve_cd_cond = check_results(solve_cd, + [[true, true, true, false], + [false, true, true, true]]); + + /* EX = F : + * x 1 x + */ + var E = create_matrix(1, 2, function(){return true}); + var F = create_map(1, function(){return 1}); + + var solve_ef = solve_for_rules(E, F, 1, 1); + var solve_ef_cond = check_results(solve_ef, + [[true, false], [false, true]]); - console.log("Simple test: " + solve_ab_cond); + display("Simple tests", [solve_ab_cond, solve_cd_cond, solve_ef_cond]); /* CX = B, slightly artificial example: * 1 _ 1 @@ -32,7 +77,7 @@ function test () { var elim_cb_cond = elim_cb.A.height === 0 && elim_cb.B.length === 0 && elim_cb.res.toString() === "true,true,false"; - console.log("Elimination of obvious rules: " + elim_cb_cond); + display("Elimination of obvious rules", [elim_cb_cond]); /* DX = B: * 1 2 x x 1 From 4d365a845d1ca9cbbff00d2f8690d5dba2ce1ee2 Mon Sep 17 00:00:00 2001 From: Dmitry Khalansky Date: Tue, 22 Nov 2016 14:14:39 +0300 Subject: [PATCH 13/23] Start implementing generation of fields We need a more complex auto-testing framework which could generate many examples. It would allow us to cover the algorithm more fully as well as to create ground for future development of algorithm for automatical solution of the fields, not simple estimation of probabilities. --- generator.js | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 generator.js diff --git a/generator.js b/generator.js new file mode 100644 index 0000000..2c09c8a --- /dev/null +++ b/generator.js @@ -0,0 +1,16 @@ +function generate (width, height, mines, seed) { + if (mines > width * height || width < 0 || height < 0 || !seed || + seed >= (1 <<< 31) - 1) { + return null; + } + + var prime = (1 <<< 31) - 1; + var field = create_matrix(height, width, function() { return 0; }); + + for (var i = 0; i < mines; ++i) { + var idx = (i + seed) * prime % width * height; + field[Math.floor(idx / width)][idx % width] = true; + } + + return field; +} From 79f3a4bbcd7d3ce1b7d233e3da3970ee8d61d809 Mon Sep 17 00:00:00 2001 From: Dmitry Khalansky Date: Tue, 22 Nov 2016 17:19:46 +0300 Subject: [PATCH 14/23] Implement better field generator This one presumably can generate all the possible layouts depending on the seed instead of a small particular set of them. --- generator.js | 19 +++++++++++++------ index.html | 1 + 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/generator.js b/generator.js index 2c09c8a..70851b4 100644 --- a/generator.js +++ b/generator.js @@ -1,15 +1,22 @@ function generate (width, height, mines, seed) { - if (mines > width * height || width < 0 || height < 0 || !seed || - seed >= (1 <<< 31) - 1) { + if (mines > elems || width <= 0 || height <= 0) { return null; } + var elems = width * height; - var prime = (1 <<< 31) - 1; - var field = create_matrix(height, width, function() { return 0; }); + var coeffs_arr = [elems - mines]; + for (var i = 1; i <= mines; ++i) { + coeffs_arr.push(coeffs_arr[i-1] * (elems - mines + i)); + } + + var field = create_matrix(height, width, function() { return false; }); - for (var i = 0; i < mines; ++i) { - var idx = (i + seed) * prime % width * height; + var rest = create_map(elems, function(i) { return i; }); + for (var i = mines - 1; i >= 0; --i) { + var r_idx = Math.floor(seed / coeffs_arr[i]) % rest.length; + var idx = rest.splice(r_idx, 1)[0]; field[Math.floor(idx / width)][idx % width] = true; + seed %= coeffs_arr[i]; } return field; diff --git a/index.html b/index.html index 17f9ff1..9aff4aa 100644 --- a/index.html +++ b/index.html @@ -8,6 +8,7 @@ + From e183fc146137a4ecf621900af0786b058b8f1460 Mon Sep 17 00:00:00 2001 From: Dmitry Khalansky Date: Wed, 23 Nov 2016 18:22:37 +0300 Subject: [PATCH 15/23] Add more utility functions Implemented memoised factorial function. Implemented a function that returns a permutation of a given array with an index. --- util.js | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/util.js b/util.js index 8a87377..80b9b4b 100644 --- a/util.js +++ b/util.js @@ -2,6 +2,27 @@ function isInt(n) { return Number(n) === n && n % 1 === 0; } +var fact_arr = [1, 1]; +function factorial(n) { + for (var i = 2; i <= n; ++i) { + fact_arr[i] = i * fact_arr[i-1]; + } + return fact_arr[n]; +} + +function permutation(arr, n) { + var perm = []; + var rest = arr.slice(); + for (var i = 0; i < arr.length; ++i) { + var f = factorial(arr.length - i - 1); + var pos = Math.floor(n / f); + n = n % f; + perm.push(rest.splice(pos, 1)[0]); + } + + return perm; +} + function create_map(n, func) { var result = []; var i; From d41a9f9970e9732a4495944029e5e7baececd484 Mon Sep 17 00:00:00 2001 From: Dmitry Khalansky Date: Mon, 12 Dec 2016 17:59:41 +0300 Subject: [PATCH 16/23] Add combination computation and merge functions Both functions manipulate arrays, the first one computes the permutations of various lengths, the second one merges the second array into the missing values in the first one. --- solver.js | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/solver.js b/solver.js index 1b5d6ec..1ec2afc 100644 --- a/solver.js +++ b/solver.js @@ -202,6 +202,47 @@ function partition_split_idx (A, B) { return null; } +function combinations (n, kmin, kmax) { + if (kmax < 0 || n < kmin) { + return []; + } + + if (n === 0) { + return [[]]; + } + + if (n === 1) { + var res = []; + if (kmax > 0) { + res.push([1]); + } + if (kmin <= 0) { + res.push([0]); + } + console.log(res); + return res; + } + + var combsPlus = combinations (n-1, kmin-1, kmax-1); + var combsMin = combinations (n-1, kmin, kmax); + + combsPlus.map(function (el) { el.splice(0, 0, 1); return el; }); + combsMin.map( function (el) { el.splice(0, 0, 0); return el; }); + return combsPlus.concat(combsMin); +} + +function merge (arr1, arr2) { + var s = arr1.slice(); + var lp = 0; + for (var i = 0; i < arr2.length; ++i) { + while (lp in s) { + ++lp; + } + s[lp] = arr2[i]; + } + return s; +} + /* Find all possible solutions for A * X = B assuming that both a_ij and x_i * are in {true, false}. * From 7bd364c9d23a2021506bd3c91a2eade5faa4a5f0 Mon Sep 17 00:00:00 2001 From: Dmitry Khalansky Date: Mon, 12 Dec 2016 18:14:00 +0300 Subject: [PATCH 17/23] More general traversal of possibilities First steps have been made to allow mines to be not only present or not present but also have a range of possible values. It's useful because it shall allow us to use symmetry to reason about groups of mines at once. --- solver.js | 35 ++++++++++++----------------------- 1 file changed, 12 insertions(+), 23 deletions(-) diff --git a/solver.js b/solver.js index 1ec2afc..46f5da4 100644 --- a/solver.js +++ b/solver.js @@ -285,7 +285,7 @@ function solve_for_rules (A, B, min, max) { return []; } - if (aWork.width === 0) { + if (aWork.width === 0 || aWork.height === 0) { for (var i = 0; i < bWork.length; ++i) { if (bWork[i] !== 0) { @@ -294,42 +294,31 @@ function solve_for_rules (A, B, min, max) { } console.log('Success!'); - return [resTempl]; + var combs = combinations(aWork.width, min, max); + return combs.map(function(el) { return merge(resTempl, el); }); } var res = []; var splitIdx = partition_split_idx(aWork, bWork); + var maxAtIdx = Math.max.apply(null, create_map(aWork.height, + function(j) { return aWork[j][splitIdx]; })); var aWoSplit = copy_matrix_wo_col(aWork, splitIdx); - /* Assuming the selected variable is `true` */ - { - var newB = create_map(bWork.length, function(i) { - return aWork[i][splitIdx] ? bWork[i] - 1 : bWork[i]; + for (var i = maxAtIdx; i >= 0; --i) { + var newB = create_map(bWork.length, function(j) { + return bWork[j] - (aWork[j][splitIdx] ? i : 0); }); - console.log('TRUE', {newB: newB}, splitIdx); - var r = solve_for_rules (aWoSplit, newB, min - 1, max - 1); + console.log({i: i, newB: newB, splitIdx: splitIdx}); + var r = solve_for_rules (aWoSplit, newB, min - i, max - i); if (r.length > 0) { res = res.concat(r.map(function(el) { - el.splice(splitIdx, 0, true); return el;})); + el.splice(splitIdx, 0, i); return el;})); } - console.log('OUT_TRUE'); - } - console.log({type: "A", res : res, resTempl : resTempl}); - /* Assuming it's `false` */ - { - var newB = bWork; - console.log('FALSE', {newB: newB}); - var r = solve_for_rules (aWoSplit, newB, min, max); - if (r.length > 0) { - res = res.concat(r.map(function(el) { - el.splice(splitIdx, 0, false); return el;})); - } - console.log('OUT_FALSE'); + console.log({Out: i, res: res}); } - console.log({type: "B", res : res, resTempl : resTempl}); res = res.map(function(el) { for (var i = 0; i < resTempl.length; ++i) { if (i in resTempl) { From c5f96c48f30760f483e9b0fbc630a687c221210d Mon Sep 17 00:00:00 2001 From: Dmitry Khalansky Date: Tue, 13 Dec 2016 11:30:03 +0300 Subject: [PATCH 18/23] Move array merging and combination listing to util These functions aren't really specific to the problem at hand, so we may as well take them out. --- solver.js | 46 +++------------------------------------------- util.js | 41 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 43 deletions(-) diff --git a/solver.js b/solver.js index 46f5da4..b81269f 100644 --- a/solver.js +++ b/solver.js @@ -202,47 +202,6 @@ function partition_split_idx (A, B) { return null; } -function combinations (n, kmin, kmax) { - if (kmax < 0 || n < kmin) { - return []; - } - - if (n === 0) { - return [[]]; - } - - if (n === 1) { - var res = []; - if (kmax > 0) { - res.push([1]); - } - if (kmin <= 0) { - res.push([0]); - } - console.log(res); - return res; - } - - var combsPlus = combinations (n-1, kmin-1, kmax-1); - var combsMin = combinations (n-1, kmin, kmax); - - combsPlus.map(function (el) { el.splice(0, 0, 1); return el; }); - combsMin.map( function (el) { el.splice(0, 0, 0); return el; }); - return combsPlus.concat(combsMin); -} - -function merge (arr1, arr2) { - var s = arr1.slice(); - var lp = 0; - for (var i = 0; i < arr2.length; ++i) { - while (lp in s) { - ++lp; - } - s[lp] = arr2[i]; - } - return s; -} - /* Find all possible solutions for A * X = B assuming that both a_ij and x_i * are in {true, false}. * @@ -294,8 +253,9 @@ function solve_for_rules (A, B, min, max) { } console.log('Success!'); - var combs = combinations(aWork.width, min, max); - return combs.map(function(el) { return merge(resTempl, el); }); + var combs = combinations_arr(aWork.width, min, max); + return combs.map(function(el) { + return merge_in(resTempl, el); }); } var res = []; diff --git a/util.js b/util.js index 80b9b4b..f3dbec4 100644 --- a/util.js +++ b/util.js @@ -84,3 +84,44 @@ function cr_const(n) { return n; }; } + +function combinations_arr (n, kmin, kmax) { + if (kmax < 0 || n < kmin) { + return []; + } + + if (n === 0) { + return [[]]; + } + + if (n === 1) { + var res = []; + if (kmax > 0) { + res.push([1]); + } + if (kmin <= 0) { + res.push([0]); + } + console.log(res); + return res; + } + + var combsPlus = combinations (n-1, kmin-1, kmax-1); + var combsMin = combinations (n-1, kmin, kmax); + + combsPlus.map(function (el) { el.splice(0, 0, 1); return el; }); + combsMin.map( function (el) { el.splice(0, 0, 0); return el; }); + return combsPlus.concat(combsMin); +} + +function merge_in (arr1, arr2) { + var s = arr1.slice(); + var lp = 0; + for (var i = 0; i < arr2.length; ++i) { + while (lp in s) { + ++lp; + } + s[lp] = arr2[i]; + } + return s; +} From 9ba3f841504f1fe3343cb439c66d8ccd017056e6 Mon Sep 17 00:00:00 2001 From: Dmitry Khalansky Date: Tue, 13 Dec 2016 11:33:50 +0300 Subject: [PATCH 19/23] solver is an object now We've put the solving functions into an object so that we can set parameters for solutions. --- solver.js | 29 ++++++++++++++++------------- sweep.js | 3 ++- 2 files changed, 18 insertions(+), 14 deletions(-) diff --git a/solver.js b/solver.js index b81269f..497b991 100644 --- a/solver.js +++ b/solver.js @@ -1,4 +1,6 @@ -function test () { +solver = { } + +solver.test = function test () { function display(name, results) { console.log(name + ": " + results.map( @@ -30,7 +32,7 @@ function test () { var B = create_map(3, function(){return 1}); B[1] = 2; - var solve_ab = solve_for_rules(A, B, 2, 3); + var solve_ab = this.solve_for_rules(A, B, 2, 3); var solve_ab_cond = check_results(solve_ab, [[false, false, true, false, true, false, false, true], [false, false, true, false, true, false, false, false]] @@ -46,7 +48,7 @@ function test () { var D = create_map(2, function(){return 2}); - var solve_cd = solve_for_rules(C, D, 3, 3); + var solve_cd = this.solve_for_rules(C, D, 3, 3); var solve_cd_cond = check_results(solve_cd, [[true, true, true, false], [false, true, true, true]]); @@ -57,7 +59,7 @@ function test () { var E = create_matrix(1, 2, function(){return true}); var F = create_map(1, function(){return 1}); - var solve_ef = solve_for_rules(E, F, 1, 1); + var solve_ef = this.solve_for_rules(E, F, 1, 1); var solve_ef_cond = check_results(solve_ef, [[true, false], [false, true]]); @@ -73,7 +75,7 @@ function test () { C[1][0] = C[1][1] = C[1][2] = true; C[2][1] = true; - var elim_cb = eliminate_obvious_rules(C, B); + var elim_cb = this.eliminate_obvious_rules(C, B); var elim_cb_cond = elim_cb.A.height === 0 && elim_cb.B.length === 0 && elim_cb.res.toString() === "true,true,false"; @@ -91,11 +93,11 @@ function test () { return { solve_ab : solve_ab, elim_cb : elim_cb, - split : split_at_components(D, B) + split : this.split_at_components(D, B) }; } -function eliminate_obvious_rules (A, B) { +solver.eliminate_obvious_rules = function eliminate_obvious_rules (A, B) { var res = []; var aWork = copy_matrix(A); var bWork = B.slice(); @@ -149,7 +151,7 @@ function eliminate_obvious_rules (A, B) { } } -function split_at_components (A, B) { +solver.split_at_components = function split_at_components (A, B) { var res = []; var assigned = Object.create(null); for (var i = 0; i < A.height; ++i) { @@ -192,7 +194,7 @@ function split_at_components (A, B) { return res; } -function partition_split_idx (A, B) { +solver.partition_split_idx = function partition_split_idx (A, B) { for (var idx = 0; idx < A.width; ++idx) { for (var r = 0; r < A.height; ++r) { if (A[r][idx]) @@ -210,7 +212,7 @@ function partition_split_idx (A, B) { * of this parameter. * */ -function solve_for_rules (A, B, min, max) { +solver.solve_for_rules = function solve_for_rules (A, B, min, max) { var min = isInt(min) ? min : -1; var max = isInt(max) ? max : Infinity; @@ -224,7 +226,7 @@ function solve_for_rules (A, B, min, max) { console.log({A : A, B : B, min : min, max : max}); - var obvious = eliminate_obvious_rules(A, B); + var obvious = this.eliminate_obvious_rules(A, B); console.log({After_elimination : obvious}); if (obvious === null) { return []; @@ -239,6 +241,7 @@ function solve_for_rules (A, B, min, max) { } min -= foundMines; max -= foundMines; + console.log({min: min, max: max}); if (max < 0 || aWork.width < min) { return []; @@ -260,7 +263,7 @@ function solve_for_rules (A, B, min, max) { var res = []; - var splitIdx = partition_split_idx(aWork, bWork); + var splitIdx = this.partition_split_idx(aWork, bWork); var maxAtIdx = Math.max.apply(null, create_map(aWork.height, function(j) { return aWork[j][splitIdx]; })); @@ -271,7 +274,7 @@ function solve_for_rules (A, B, min, max) { return bWork[j] - (aWork[j][splitIdx] ? i : 0); }); console.log({i: i, newB: newB, splitIdx: splitIdx}); - var r = solve_for_rules (aWoSplit, newB, min - i, max - i); + var r = this.solve_for_rules (aWoSplit, newB, min - i, max - i); if (r.length > 0) { res = res.concat(r.map(function(el) { el.splice(splitIdx, 0, i); return el;})); diff --git a/sweep.js b/sweep.js index 0d46cca..20da41a 100644 --- a/sweep.js +++ b/sweep.js @@ -220,7 +220,8 @@ function solve() { } var mines = csweep.mines - bomb_idxs.length; - var res = solve_for_rules(A, B, mines, mines); + + var res = solver.solve_for_rules(A, B, mines, mines); if (res) { var probs = create_matrix(csweep.field.height, w, From c61a06789dd921e8f3382e60fc43ef17a74a1545 Mon Sep 17 00:00:00 2001 From: Dmitry Khalansky Date: Tue, 13 Dec 2016 11:48:16 +0300 Subject: [PATCH 20/23] Fix errors made while moving `combinations_arr` The function didn't work since it called itself with the wrong name. It also included debugging logging that's no longer needed. --- util.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/util.js b/util.js index f3dbec4..db82807 100644 --- a/util.js +++ b/util.js @@ -102,12 +102,11 @@ function combinations_arr (n, kmin, kmax) { if (kmin <= 0) { res.push([0]); } - console.log(res); return res; } - var combsPlus = combinations (n-1, kmin-1, kmax-1); - var combsMin = combinations (n-1, kmin, kmax); + var combsPlus = combinations_arr (n-1, kmin-1, kmax-1); + var combsMin = combinations_arr (n-1, kmin, kmax); combsPlus.map(function (el) { el.splice(0, 0, 1); return el; }); combsMin.map( function (el) { el.splice(0, 0, 0); return el; }); From 9cd5d10839ded9c92b45fce911552b723f2fef3c Mon Sep 17 00:00:00 2001 From: Dmitry Khalansky Date: Tue, 13 Dec 2016 11:50:46 +0300 Subject: [PATCH 21/23] Make debug logging conditional Debug logging was a major resource hog, and why bother with it when everything works? And when it doesn't work, the problems are usually highlighted by unit testing. --- solver.js | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/solver.js b/solver.js index 497b991..5118fd7 100644 --- a/solver.js +++ b/solver.js @@ -1,4 +1,11 @@ -solver = { } +solver = { + debug: false, + log: function () { + if (this.debug) { + console.log.apply(console, arguments); + } + } +} solver.test = function test () { @@ -90,10 +97,12 @@ solver.test = function test () { D[1][0] = D[1][1] = D[1][2] = true; D[2][3] = D[2][4] = D[2][5] = true; + var split_db = this.split_at_components(D, B); + return { solve_ab : solve_ab, elim_cb : elim_cb, - split : this.split_at_components(D, B) + split : split_db }; } @@ -224,10 +233,10 @@ solver.solve_for_rules = function solve_for_rules (A, B, min, max) { } } - console.log({A : A, B : B, min : min, max : max}); + this.log({A : A, B : B, min : min, max : max}); var obvious = this.eliminate_obvious_rules(A, B); - console.log({After_elimination : obvious}); + this.log({After_elimination : obvious}); if (obvious === null) { return []; } @@ -241,7 +250,7 @@ solver.solve_for_rules = function solve_for_rules (A, B, min, max) { } min -= foundMines; max -= foundMines; - console.log({min: min, max: max}); + this.log({min: min, max: max}); if (max < 0 || aWork.width < min) { return []; @@ -255,7 +264,7 @@ solver.solve_for_rules = function solve_for_rules (A, B, min, max) { } } - console.log('Success!'); + this.log('Success!'); var combs = combinations_arr(aWork.width, min, max); return combs.map(function(el) { return merge_in(resTempl, el); }); @@ -273,13 +282,13 @@ solver.solve_for_rules = function solve_for_rules (A, B, min, max) { var newB = create_map(bWork.length, function(j) { return bWork[j] - (aWork[j][splitIdx] ? i : 0); }); - console.log({i: i, newB: newB, splitIdx: splitIdx}); + this.log({i: i, newB: newB, splitIdx: splitIdx}); var r = this.solve_for_rules (aWoSplit, newB, min - i, max - i); if (r.length > 0) { res = res.concat(r.map(function(el) { el.splice(splitIdx, 0, i); return el;})); } - console.log({Out: i, res: res}); + this.log({Out: i, res: res}); } res = res.map(function(el) { From aa79cc6ec2e6db42691e411023255f6949bc9c33 Mon Sep 17 00:00:00 2001 From: Dmitry Khalansky Date: Tue, 13 Dec 2016 12:08:46 +0300 Subject: [PATCH 22/23] Better colors Now red appears and green disappears faster than as a linear function of probability. --- sweep.js | 24 ++++-------------------- 1 file changed, 4 insertions(+), 20 deletions(-) diff --git a/sweep.js b/sweep.js index 20da41a..f24187a 100644 --- a/sweep.js +++ b/sweep.js @@ -255,24 +255,8 @@ function to_hex(c) { } function get_color(prob) { - var red, green, blue; - switch (prob) { - case 100 : - red = 255; - green = 0; - break; - case 0 : - red = 0; - green = 255; - break; - case -1 : - return undefined - default : - red = 255 * prob; - green = 255 * (1 - prob); - } - var red = to_hex(red); - var green = to_hex(green); - var blue = to_hex(0); - return "#" + red + green + blue; + return prob === -1 ? undefined : ("#" + + to_hex(255 * Math.pow(prob, 1.1)) + + to_hex(255 * (1 - Math.pow(prob, 0.4))) + + to_hex(0)); } From c75a55103889f06821c167d38501f814e6417943 Mon Sep 17 00:00:00 2001 From: Dmitry Khalansky Date: Wed, 14 Dec 2016 16:16:48 +0300 Subject: [PATCH 23/23] Fix tests No reason to trust unit tests if they aren't working. And it turns out they doesn't. Some errors have surfaced that needed the code to be updated. --- solver.js | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/solver.js b/solver.js index 5118fd7..cc10e33 100644 --- a/solver.js +++ b/solver.js @@ -21,9 +21,9 @@ solver.test = function test () { function check_results(solution, correct) { return solution.map(function(x) { - x.toString() + return x.toString() }).sort().toString() === correct.map(function(x) { - x.toString() + return x.toString() }).sort().toString(); } @@ -41,8 +41,8 @@ solver.test = function test () { var solve_ab = this.solve_for_rules(A, B, 2, 3); var solve_ab_cond = check_results(solve_ab, - [[false, false, true, false, true, false, false, true], - [false, false, true, false, true, false, false, false]] + [[0, 0, 1, 0, 1, 0, 0, 1], + [0, 0, 1, 0, 1, 0, 0, 0]] ); /* CX = D : @@ -57,8 +57,8 @@ solver.test = function test () { var solve_cd = this.solve_for_rules(C, D, 3, 3); var solve_cd_cond = check_results(solve_cd, - [[true, true, true, false], - [false, true, true, true]]); + [[1, 1, 1, 0], + [0, 1, 1, 1]]); /* EX = F : * x 1 x @@ -68,7 +68,7 @@ solver.test = function test () { var solve_ef = this.solve_for_rules(E, F, 1, 1); var solve_ef_cond = check_results(solve_ef, - [[true, false], [false, true]]); + [[1, 0], [0, 1]]); display("Simple tests", [solve_ab_cond, solve_cd_cond, solve_ef_cond]); @@ -84,7 +84,7 @@ solver.test = function test () { var elim_cb = this.eliminate_obvious_rules(C, B); var elim_cb_cond = elim_cb.A.height === 0 && elim_cb.B.length === 0 && - elim_cb.res.toString() === "true,true,false"; + elim_cb.res.toString() === "1,1,0"; display("Elimination of obvious rules", [elim_cb_cond]); @@ -134,7 +134,7 @@ solver.eliminate_obvious_rules = function eliminate_obvious_rules (A, B) { if (!aWork[i][j]) { continue; } - res[idxs[j]] = bWork[i] !== 0; + res[idxs[j]] = (bWork[i] !== 0) * aWork[i][j]; if (res[idxs[j]]) { for (var z = 0; z < aWork.height; ++z) { if (aWork[z][j]) {