const LETTERS = "ABCDEFGHIJKLMNOPQRST";
const ROWS = [
     [...("ABCDE")],
     [...("FGHIJ")],
     [...("KLMNO")],
     [...("PQRST")]
];

class Marple{
    constructor(clues){
        this.clues = []
        for(let clue of clues){
            if(clue[1] === '^'){
                this.clues.push(["same", clue[0], clue[2]]);
            } else if(clue[0] == clue[2]){
                this.clues.push(["adjacent", clue[0], clue[1]]);
            } else if(clue[1] === '<'){
                this.clues.push(["left", clue[0], clue[2]])
            } else {
                this.clues.push(["between", clue[0], clue[1], clue[2]])
            }
        }
        this.pos = {};
        for(let letter of LETTERS){
            this.pos[letter] = new Set([...Array(5).keys()].map(i => i));
        }
    }

    updateSame(a, b){
        let updated = false;
        let commons = [...this.pos[a]].filter(ele => this.pos[b].has(ele));
        if(this.pos[a].size > commons.length){
            this.pos[a] = new Set(commons);
            updated = true;
        }
        if(this.pos[b].size > commons.length){
            this.pos[b] = new Set(commons);
            updated = true;
        }
        return updated;
    }

    updateAdjacent(a, b){
        let updated = false;
        let ca = [...this.pos[a]].filter(ele => this.pos[b].has(ele - 1) || this.pos[b].has(ele + 1));
        if(this.pos[a].size > ca.length){
            this.pos[a] = new Set(ca);
            updated = true;
        }
        let cb = [...this.pos[b]].filter(ele => this.pos[a].has(ele - 1) || this.pos[a].has(ele + 1));
        if(this.pos[b].size > cb.length){
            this.pos[b] = new Set(cb);
            updated = true;
        }
    }

    updateLeft(a, b){
        let updated = false;
        let [m, M] = [Math.min(...this.pos[a]), Math.max(...this.pos[b])];
        let ca = [...this.pos[a]].filter(ele => ele < M);
        let cb = [...this.pos[b]].filter(ele => ele > m);
        if(this.pos[a].size > ca.length){
            this.pos[a] = new Set(ca);
            updated = true;
        }
        if(this.pos[b].size > cb.length){
            this.pos[b] = new Set(cb);
            updated = true;
        }   
        return updated;        
    }

    updateBetween(a, b, c){
        let updated = false;
        let cb = [];
        for(let eb of this.pos[b]){
            let found = false;
            for(let ea of this.pos[a]){
                for(let ec of this.pos[c]){                    
                    if((ea < eb && eb < ec) || (ec < eb && eb < ea)){
                        found = true;
                        break;
                    }                    
                }
                if(found) break;
            }
            if(found){
                cb.push(eb)
            }
        }
        if(cb.length < this.pos[b].size){
            this.pos[b] = new Set(cb);
            updated = true;                
        }
        let ca = [];
        for(let ea of this.pos[a]){
            let found = false;
            for(let eb of this.pos[b]){
                for(let ec of this.pos[c]){
                    if((ea < eb && eb < ec) || (ec < eb && eb < ea)){
                        found = true;
                        break;
                    }                    
                }
                if(found) break;
            }                
            if(found){
                ca.push(ea);
            }
        }
       if(ca.length < this.pos[a].size){
            this.pos[a] = new Set(ca);
            updated = true;                
        }
        let cc = [];
        for(let ec of this.pos[c]){
            let found = false;
            for(let ea of this.pos[a]){
                for(let eb of this.pos[b]){
                    if((ea < eb && eb < ec) || (ec < eb && eb < ea)){  
                        found = true;
                        break;
                    }                              
                }
                if(found)  break;
            }   
            if(found){
                cc.push(ec);
            }
    }
    if(cc.length < this.pos[c].size){
        this.pos[c] = new Set(cc);
        updated = true;                
    }
    return updated;
    }

    updateRow(row){
        let updated = false;
        for(let letter of row){
            if(this.pos[letter].size === 1){
                let ele = [...this.pos[letter]].pop()
                for(let l of row){
                    if(l === letter) continue;                    
                    if(this.pos[l].has(ele)){
                        this.pos[l].delete(ele);
                        updated = true;
                    }
                }
            }
        }
        for(let v = 0; v < 5; v++){
            // Candidate letters that have position v
            let cv = [];
            for(let letter of row){
                if(this.pos[letter].has(v)){
                    cv.push(letter);
                }
            }
            // Only one letter has position v
            if(cv.length == 1){
                let l = cv[0];
                if(this.pos[l].size > 1){
                    this.pos[l] = new Set([v]);
                    updated = true;
                    for(let other of row){
                        if(other == l) continue;
                        this.pos[other].delete(v);
                    }
                }
            }
        }
        return updated;
    }

    solve(){
        let updated = true;
        while(updated){
            updated = false;
            let [same, adjacent, left, between] = [false, false, false, false];
            for(let [type, ...o] of this.clues){                
                switch(type){
                    case "same":{
                        let [a, b] = o;
                        same = this.updateSame(a, b);
                        if(same) updated = true;
                        break;
                    }
                    case "adjacent":{
                        let [a, b] = o;
                        adjacent = this.updateAdjacent(a, b);
                        if(adjacent) updated = true;
                        break;
                    }
                    case "left":{
                        let [a, b] = o;
                        left = this.updateLeft(a, b);
                        if(left) updated = true;
                        break;
                    }
                    case "between":{
                        let [a, b, c] = o;
                        between = this.updateBetween(a, b, c);
                        if(between) updated = true;
                        break;
                    }
                }
            }
            for(let row of ROWS){
                let ru = this.updateRow(row);
                if(ru) updated = true;
            }
        }   
        let ans = [...Array(20).keys()].fill(' ');
        for(let i = 0; i < 4; i++){
            let row = ROWS[i];
            for(let letter of row){
                ans[5 * i + [...this.pos[letter]].pop()] = letter;
            }
        }
        return ans.join('');
    }
}


function solve(clues) {
    let marple = new Marple(clues);
    return marple.solve();
}

let tests = [
    // ["MRT", "ABH", "LKO", "OKP", "JIM", "OPE", "GDO", "RAQ", "J^A", "M^P", "A<Q", "D<K", "OQO"],
["HJL","POA","DHJ","KMA","DRG","PHD","AMQ","H<F","M<K","F<E","M<I","T<E","CPC","SOS"]
    // ["PBJ", "KDO", "DHG", "AOR", "INM", "EMB", "GTD", "O^T", "P<Q", "T<P", "A<L", "P<F", "RIR", "IDI"],
    // ["EMJ", "DJO", "AMN", "ADC", "CIL", "END", "GQS", "SAB", "Q<B", "RPR", "SAS", "FNF", "NPN", "SCS"],
    // ["LCI", "CQH", "NOF", "AEC", "APG", "NGL", "EQB", "F^P", "M^S", "E<J", "B<F", "T<P", "F<A", "P<K", "S<Q", "LCL"]
];

for(let test of tests){
    let marple = new Marple(test);
    let res = marple.solve();
    console.log("res :", res);
}

Embed on website

To embed this program on your website, copy the following code and paste it into your website's HTML: