ThomasR/nonogram-solver

View on GitHub
src/Strategy.js

Summary

Maintainability
C
1 day
Test Coverage
const util = require("./util");
const guessAndConquer = require('./guessAndConquer');

const debugMode = require('commander').debug;

/**
 * Strategy for solving a puzzle by applying line solvers repeatedly
 */
class Strategy {
  /**
   * @param {Array} solvers List of line solvers sorted by speed
   * @param {boolean} randomize `false` to run trial and error in order. Defaults to `true`.
   *     In practice, using random guessing mostly yields faster results.
   */
  constructor(solvers, randomize = true) {
    this.solvers = solvers;
    this.randomize = randomize;
  }

  /**
   * Solve the puzzle.
   * @param {Puzzle} puzzle The puzzle to solve
   * @param {boolean} withTrialAndError `false` to stop without trial and error. Defaults to `true`.
   */
  solve(puzzle, withTrialAndError = true) {
    if (debugMode) {
      var start = Date.now();
      var statistics = Array(this.solvers.length).fill(0);
      var solutionSequence = [];
    }

    // keep tracks of visited lines
    this.visited = {
      rows: Array(puzzle.height).fill(0).map(() => new Uint8Array(this.solvers.length)),
      columns: Array(puzzle.width).fill(0).map(() => new Uint8Array(this.solvers.length))
    };

    // repeatedly run all solvers on puzzle
    let progress;
    do {
      let snapshot = puzzle.snapshot;
      progress = false;
      this.solvers.forEach((solver, i) => {
        if (progress) {
          return;
        }
        // run one line solver on the whole puzzle
        this.solveOnce(puzzle, solver, i, solutionSequence);
        progress = puzzle.snapshot.toString() !== snapshot.toString();
        if (debugMode) {
          statistics[i]++;
        }
      });
    } while(progress);

    // no solution found… trial and error now
    if (withTrialAndError && !puzzle.isFinished) {
      if (debugMode) {
        console.log('must start guessing');
      }
      let deepResult = guessAndConquer(this, puzzle);
      if (deepResult) {
        puzzle.import(deepResult);
      }
    }

    if (debugMode) {
      console.log(`Solution sequence: [${solutionSequence.join(',')}]`);
      console.log(`Time elapsed: ${Date.now() - start}ms`);
      console.log(`Runs (on puzzle) per solver: ${JSON.stringify(statistics)}`);
    }
  }

  /**
   * @private
   * Run one solver on the puzzle
   * @param {Puzzle} puzzle The puzzle to solve
   * @param {Solver} solver The solver to use
   * @param {number} solverIndex The solver's index in `this.solvers`
   * @param {Array} solutionSequence Array of strings for statistics in debug mode
   */
  solveOnce(puzzle, solver, solverIndex, solutionSequence) {
    // If we're dealing with a slow solver, we want to skip as soon as one line is partially solved
    let skipEarly = solver.speed === 'slow';
    let skip = false;

    // Optimize iteration order
    let optimizeOrder = (lines, hints) => {
      // remove already solved lines
      let unsolvedLines = lines.reduce((result, line, index) => {
        let zeros = line.reduce((count, x) => count + (x === 0 ? 1 : 0), 0);
        if (!zeros) {
          return result;
        }
        result.push({line, index, zeros});
        return result;
      }, []);

      // sort by estimated computation effort
      if (skipEarly) {
        unsolvedLines.forEach(lineMeta => {
          let {index, zeros} = lineMeta;
          let hintSum = util.hintSum(hints[index]);
          let estimate = zeros < hintSum ? 0 : Math.pow(zeros - hintSum, hints[index].length);
          lineMeta.estimate = estimate;
        });
        unsolvedLines.sort(({estimate: left}, {estimate: right}) => left - right);
      }
      return unsolvedLines;
    };

    // the actual execution
    let run = (lines, hints, onRow) => {
      let visited = onRow ?
        {current: this.visited.rows, other: this.visited.columns} :
        {current: this.visited.columns, other: this.visited.rows};
      let rearrangedLines = optimizeOrder(lines, hints);
      rearrangedLines.forEach(({line, index: i, estimate}) => {
        if (skip || visited.current[i][solverIndex]) {
          return;
        }
        if (debugMode) {
          console.log(`Running solver ${solverIndex} on ${onRow ? 'row' : 'column'} ${i}`, JSON.stringify(line.slice()), hints[i]);
          if (estimate) {
            console.log(`Estimated effort: ${estimate}`);
          }
        }
        visited.current[i][solverIndex] = 1;
        // First, trim unnecessary information from the line
        let [trimmedLine, trimmedHints, trimInfo] = util.trimLine(line, hints[i]);
        if (debugMode) {
          var start = Date.now();
        }
        // solver run
        let newLine = solver(trimmedLine, trimmedHints);

        if (debugMode) {
          let end = Date.now();
          if (end - start > 100) {
            console.log(`Long run: ${end - start}ms`);
          }
        }

        // now, restore the trimmed line and analyze the result
        let hasChanged = false;
        let changedLines = [];
        if (newLine) { // the solver may return null to indicate no progress
          newLine = util.restoreLine(newLine, trimInfo);
          line.forEach((el, i) => {
            // What has changed?
            if (el !== newLine[i]) {
              line[i] = newLine[i];
              // These perpendicular lines must be revisited
              visited.other[i].fill(0);
              if (debugMode) {
                changedLines.push(i);
              }
            }
          });
          hasChanged = changedLines.length > 0;
          skip = hasChanged && skipEarly;
        }

        if (!debugMode) {
          util.spinner.spin();
        } else if (hasChanged) {
          console.log(`found ${newLine}`);
          console.log(puzzle);
          console.log(`Must revisit ${onRow ? 'column' : 'row'}${changedLines.length > 1 ? 's' : ''} ${changedLines.join(',')}`);
          solutionSequence.push(`(${solverIndex})${onRow ? 'r' : 'c'}${i}[${changedLines.join(',')}]`);
        }
      });
    };

    // run on rows
    run(puzzle.rows, puzzle.rowHints, true);
    if (skip) {
      return;
    }
    // …and then on columns
    run(puzzle.columns, puzzle.columnHints);
  }
}

module.exports = Strategy;