collapse / src / lib / grid.ts
grid.ts
Raw
import cell from "./cell";
import { vector, V } from "./vector";

const RULE_INCOMPATIBLE = {
    0: [ 2, 3 ],
    1: [ 3, 4 ],
    2: [ 0, 4 ],
    3: [ 0, 1 ],
    4: [ 1, 2]
};
const RULE_WEIGHTS = {
    0: 9,
    1: 6,
    2: 8,
    3: 6,
    4: 6
};
const RULE_SMOOTHNESS = 20;

function weighted_pick(arr, nearby = null){
    let new_arr = [];
    for(let i of arr){
        for(let m=0;m<RULE_WEIGHTS[i];m++){
            new_arr.push(i);
        }
    }
    if(nearby != null) {
        for(let i of Object.keys(nearby)){
            for(let z=0;z<RULE_SMOOTHNESS;z++){
                for(let m=0;m<RULE_WEIGHTS[i];m++){
                    new_arr.push(i);
                }
            }
        }
    }
    return rnd_pick(new_arr);
}

function rnd_pick(arr){
    return arr[Math.floor(Math.random() * arr.length)];
}

export class grid {
    width: number;
    height: number;
    cells: Array<Array<cell>>;
    iter(fn: (x: number, y: number) => void){
        for(let y=0;y<this.height;y++){
            for(let x=0;x<this.width;x++){
                fn(x, y);
            }
        }
    }
    getCell(p: vector){
        let c: cell = undefined;
        try {
            c = this.cells[p.y][p.x];
        } catch (error) {
            
        }
        return c;
    }
    getCells() {
        let out: Array<cell> = [];
        this.iter( (x, y) => {
            out.push( this.getCell( new V(x, y) ) );
        } );
        return out;
    }
    setCell(p: vector, c: cell) {
        if(p.x < 0 || p.y < 0){
            console.warn("SC || position ", p, " too low");
            return
        }
        if(p.x > this.width || p.y > this.height){
            console.warn("SC || position ", p, " too high");
            return
        }
        let cellsCopy = this.cells.slice();
        let group = cellsCopy[p.y].slice();
        group[p.x] = c;
        cellsCopy[p.y] = group;
        this.cells = cellsCopy;
    }
    modifyCell(p: vector, props: Object) {
        this.setCell( p, {
            ...this.getCell(p), ...props,
            isCollapsed: function (): boolean {
                return this.possible.length < 2;
            }
        } );
    }
    getCollapsedNeighbourValues( c: cell ){
        let out = [];
        for(let nv of c.neighbours){
            if(nv){
                let n = this.getCell(nv);
                if(n){
                    if(n.isCollapsed()){
                        out.push(n.possible[0]);
                    }
                }
            }
        }
        return out;
    }
    rule( c: cell ) {
        let possible = c.possible.slice();
        for(let c2v of c.neighbours.filter(n=>n!=null)) {
            let c2 = this.getCell(c2v);
            if(c2 && c2.isCollapsed()) {
                for(let p of c.possible){
                    if(possible.includes(p)){
                        let incompatible = RULE_INCOMPATIBLE[ c2.possible[0] ];
                        try {
                            possible = possible.filter( x=>!incompatible.includes(x) );
                        } catch (error) {
                            console.log(incompatible);
                        }
                    }
                }
            }
        }
        return possible;
    }
    evaluate(){
        for(let cell of this.getUncollapsed()) {
            let p = new V(cell.x, cell.y);
            let c = this.getCell( p );
            this.modifyCell( p, { possible: this.rule(c) } );
        }
    }
    propagate(c: cell, recursionLimit = 0) {
        if(recursionLimit > 10 || c.isCollapsed()){
            return;
        }
        recursionLimit++;
        let newPossible = this.rule(c);
        if(newPossible.length != c.possible.length){
            this.modifyCell( new V(c.x, c.y), { possible: newPossible } );
            for(let nv of c.neighbours.filter(n=>n!=null)){
                let n = this.getCell(nv);
                if(n && !n.isCollapsed()){
                    this.propagate( n, recursionLimit);
                }
            }
        }
        return;
    }
    getUncollapsed(){
        return this.getCells().filter( x=>x.possible.length>1 );
    }
    collapse( p: vector ) {
        let cell = this.getCell( p );
        let neighbour_values = this.getCollapsedNeighbourValues( cell );
        let neighbour_weights = {};
        for(let val of neighbour_values){
            neighbour_weights[val] = neighbour_weights[val] ? neighbour_weights[val] + 1 : 1;
        }
        this.modifyCell( p, { possible: [weighted_pick(cell.possible, neighbour_weights)] } );
    }
    getCellEntropy( p: vector ) {

    }
    solve(){
        this.evaluate();
        let lowestEntropy = Number.MAX_VALUE;
        let lowestEntropyCell: cell = null;
        let uncollapsed = this.getUncollapsed();
        for(let cell of uncollapsed) {
            let entropy = cell.possible.length;
            if(entropy < lowestEntropy && entropy > 0){
                lowestEntropy = entropy;
                lowestEntropyCell = cell;
            }
        }
        if(lowestEntropyCell){
            this.collapse( new V(lowestEntropyCell.x, lowestEntropyCell.y) );
        }
    }
    curatedsolve(){
        if(this.getUncollapsed().length < 1) {
            return true;
        }
        this.solve();
        return false;
    }
    solveall() {
        for(;;){
            if(this.curatedsolve()){
                break;
            }
        }
    }
    constructor(w: number, h: number){
        this.width = w;
        this.height = h;
        this.cells = new Array(h).fill( new Array(w).fill( null ) );
        let index = 0;
        // Create Cells
        this.iter( (x,y) => {
            let position = new vector(x, y);
            let c = new cell( index, x, y );
            c.x = x;
            c.y = y;
            this.setCell(position, c);
            index++;
        } );
        // Assign cell neighbours
        this.iter( (x,y) => {
            let position = new vector(x, y);
            let c = this.getCell( position );
            let neighbours = [
                V.add(position, new V(0, 1) ), // UP
                V.add(position, new V(1, 1) ), // UP-RIGHT
                V.add(position, new V(1, 0) ),
                V.add(position, new V(1,-1) ),
                V.add(position, new V(0,-1) ),
                V.add(position, new V(-1,-1) ),
                V.add(position, new V(-1,0) ),
                V.add(position, new V(-1,1) ),
            ];
            c.neighbours = neighbours;
            this.setCell(position, c);
        } );
        // this.modifyCell( new V(0,0), { possible: [0], collapsed: true } );
        // this.modifyCell( new V(1,0), { possible: [1], collapsed: true } );
        // this.modifyCell( new V(2,0), { possible: [2], collapsed: true } );
    }
}
export default grid;