Greasy Fork is available in English.

picross

picross solver

// ==UserScript==
// @name         picross
// @namespace    http://du9l.com/
// @version      0.1
// @description  picross solver
// @author       Du9L
// @match        http://liouh.com/picross2/
// @grant        none
// ==/UserScript==

(function() {

"use strict";

let $ = window.jQuery;

const STATE_UNKNOWN = 'UNKNOWN', STATE_ON = 'ON', STATE_OFF = 'OFF';
const TASK_ROW = 'ROW', TASK_COL = 'COL';

let loadPuzzle = function () {
    let new_puzzle = {
        rows: 0,
        cols: 0,
        row_keys: [],
        col_keys: [],
    };
    let all_rows = $('#puzzle tbody > tr');
    new_puzzle.rows = all_rows.length - 1;
    // load row keys
    for (let r = 1; r < all_rows.length; ++r) {
        let key_cell = $(all_rows[r]).find('.key').first();
        let keys = [];
        $(key_cell).find('strong').each( (index, element) => {
            keys.push(parseInt($(element).text()));
        });
        new_puzzle.row_keys.push(keys);
    }
    let all_cols = $($('#puzzle tbody > tr').first()).find('td');
    new_puzzle.cols = all_cols.length - 1;
    // load col keys
    for (let c = 1; c < all_cols.length; ++c) {
        let key_cell = all_cols[c];
        let keys = [];
        $(key_cell).find('strong').each( (index, element) => {
            keys.push(parseInt($(element).text()));
        });
        new_puzzle.col_keys.push(keys);
    }

    return new_puzzle;
}

// search function: 
// [valid solution] {kth cell: [possible states]} | [invalid solution] null = 
//   search ( keys since X, cells since Y )
let searchFunction = function (keys_since, cells_since, keys, initial_state) {
    // edge: no more keys to fulfill, set rest cells to OFF
    if (keys_since >= keys.length) {
        let ret = {};
        for (let cell = cells_since; cell < initial_state.length; ++cell) {
            if (initial_state[cell] == STATE_UNKNOWN) {
                ret[cell] = [STATE_OFF];
            } else if (initial_state[cell] == STATE_ON) {
                return null;
            }
        }
        return ret;
    }
    // edge: no more cells to use, return invalid
    if (cells_since >= initial_state.length) {
        return null;
    }
    // if current cell is OFF, skip to next ON or UNKNOWN cell
    let state = initial_state[cells_since];
    let ret = null;
    if (state == STATE_OFF || state == STATE_UNKNOWN) {
        let start_point = cells_since;
        while (++start_point < initial_state.length) {
            if (initial_state[start_point] != STATE_OFF) {
                ret = searchFunction(keys_since, start_point, keys, initial_state);
                if (ret !== null && state == STATE_UNKNOWN) {
                    ret[cells_since] = [STATE_OFF];
                }
                break;
            }
        }
    }
    // if current cell is ON, try to fill the rest of current key
    if (state == STATE_ON || state == STATE_UNKNOWN) {
        let current_key = keys[keys_since];
        if (cells_since + current_key <= initial_state.length) {
            // check if ON is possible
            let valid = true;
            for (let cell = cells_since; cell < cells_since + current_key; ++cell) {
                if (initial_state[cell] == STATE_OFF) {
                    valid = false;
                    break;
                }
            }
            if (valid && cells_since + current_key < initial_state.length && 
                    initial_state[cells_since + current_key] == STATE_ON) {
                valid = false;
            }
            if (valid) {
                let start_point = cells_since + current_key + 1;
                if (start_point > initial_state.length) {
                    -- start_point;
                }
                let sub_result = searchFunction(keys_since+1, start_point, keys, initial_state);
                if (sub_result !== null) {
                    if (ret === null) {
                        ret = {};
                    }
                    // set current key cells
                    for (let cell = cells_since; cell < cells_since + current_key + 1; ++cell) {
                        if (cell >= initial_state.length || initial_state[cell] != STATE_UNKNOWN) {
                            continue;
                        }
                        let new_value = (cell == cells_since + current_key) ? STATE_OFF : STATE_ON;
                        if (cell in ret) {
                            if (!ret[cell].includes(new_value)) {
                                ret[cell].push(new_value)
                            }
                        } else {
                            ret[cell] = [new_value];
                        }
                    }
                    // merge with the sub-result
                    for (const cell in sub_result) {
                        if (!sub_result.hasOwnProperty(cell)) {
                            continue;
                        }
                        if (cell in ret) {
                            for (const val of sub_result[cell]) {
                                if (!ret[cell].includes(val)) {
                                    ret[cell].push(val)
                                }
                            }
                        } else {
                            ret[cell] = sub_result[cell];
                        }
                    }
                }
            }
        }
    }
    return ret;
}
// end of search function

let solvePuzzle = function (puzzle) {
    // initial state
    let state = [];
    for (let r = 0; r < puzzle.rows; ++r) {
        let row_state = [];
        for (let c = 0; c < puzzle.cols; ++c) {
            row_state.push(STATE_UNKNOWN);
        }
        state.push(row_state);
    }
    // initial queue
    let task_set = new Set();
    let row_tasks = [], col_tasks = [];
    for (let r = 0; r < puzzle.rows; ++r) {
        let task = [TASK_ROW, r];
        row_tasks.push(task);
        task_set.add(task);
    }
    for (let c = 0; c < puzzle.cols; ++c) {
        let task = [TASK_COL, c];
        col_tasks.push(task);
        task_set.add(task);
    }
    // start searching
    let solution = [];
    while (task_set.size != 0) {
        let new_tasks = new Set();

        // run each task
        for (const task of task_set.values()) {
            let task_type = task[0], row_or_col = task[1];
            console.info('Run task', task_type, row_or_col);
            // task data
            let initial_state, keys;
            if (task_type == TASK_ROW) {
                initial_state = state[row_or_col].slice();
                keys = puzzle.row_keys[row_or_col];
            } else {
                initial_state = [];
                for (const row_state of state) {
                    initial_state.push(row_state[row_or_col]);
                }
                keys = puzzle.col_keys[row_or_col];
            }

            // start searching
            let ret = searchFunction(0, 0, keys, initial_state);
            if (ret === null) {
                console.error("A search returned null!!", task_type, row_or_col);
            } else {
                // check for cells with only one solution
                for (let cell in ret) {
                    if (!ret.hasOwnProperty(cell)) {
                        continue;
                    }
                    const val = ret[cell];
                    cell = parseInt(cell);
                    if (val.length == 0) {
                        console.error("Search function returned empty array!!", val);
                    }
                    else if (val.length == 1) {
                        let row, col;
                        if (task_type == TASK_ROW) {
                            if (state[row_or_col][cell] != STATE_UNKNOWN) {
                                throw "Search function returned known cell";
                            }
                            state[row_or_col][cell] = val[0];
                            new_tasks.add(col_tasks[cell]);
                            row = row_or_col; col = cell;
                        } else {
                            if (state[cell][row_or_col] != STATE_UNKNOWN) {
                                throw "Search function returned known cell";
                            }
                            state[cell][row_or_col] = val[0];
                            new_tasks.add(row_tasks[cell]);
                            row = cell; col = row_or_col;
                        }
                        console.info("Found solution!!", row, col, val[0]);
                        solution.push([row, col, val[0]]);
                    }
                }
            }
        }

        // check if it's all solved
        let solved = true;
        for (const row_state of state) {
            for (const col_value of row_state) {
                if (col_value == STATE_UNKNOWN) {
                    solved = false;
                    break;
                }
            }
            if (!solved) {
                break;
            }
        }
        if (solved) {
            console.info('all solved!!');
            break;
        }

        // append new tasks
        console.info('====== next round ======')
        task_set.clear();
        for (const new_task of new_tasks.values()) {
            console.info('task:', new_task);
            task_set.add(new_task);
        }
    }

    return solution;
}

let applySolution = function (solution) {
    let _clicker = function (step_data) {
        let obj = $($('#puzzle tbody > tr')[step_data[0] + 1]).find('td')[step_data[1] + 1];
        let _which = (step_data[2] == STATE_ON) ? 1 : 3;
        $(obj).trigger(
            {type: 'mousedown', target: obj, which: _which}
        ).trigger(
            {type: 'mouseup', target: obj, which: _which}
        );
    }
    for (let step = 0; step < solution.length; step++) {
        const step_data = solution[step];
        setTimeout(_clicker.bind(undefined, step_data), (step + 1) * 100);
    }
}

let addSolveButton = function () {
    let container = $('.controls .control-group').first();
    $('<div class="custom" />').appendTo(container);
    let button = $('<button>Auto solve</button>');
    button.mouseup(function() {
        let puzzle = loadPuzzle();
        console.info(puzzle);
        let solution = solvePuzzle(puzzle);
        applySolution(solution);
        let solvedPercent = solution.length * 100 / (puzzle.rows * puzzle.cols)
        $('<span />').text(
            'Solved: ' + (solvedPercent.toFixed(2) + "%")
        ).appendTo('#solverText').delay(2000).fadeOut();
    });
    button.appendTo(container);
    $('<div id="solverText" />').appendTo(container);
}

window.addEventListener('load', addSolveButton, false);

})();