KoL Detective Helper

Helps with solving the detective school puzzles in Kingdom of Loathing

// ==UserScript==
// @name           KoL Detective Helper
// @namespace      kol.interface.unfinished
// @description    Helps with solving the detective school puzzles in Kingdom of Loathing
// @include        https://*kingdomofloathing.com/wham.php*
// @grant          GM_setValue
// @grant          GM_getValue
// @version        1.2
// ==/UserScript==

// Version 1.2
// - highlight areas on hover over questions
// Version 1.1
// - include possible responses in lists of people and jobs, so they can be recognized in answers prior to visiting
// Version 1.0.1
// - highlight active row better
// Version 1.0

var titleKey;
var places;
var person;
var people;
var jobs;
var personPlaceJob = {};
var personSays = {};

function getTitle() {
	var t = document.evaluate( '//b[contains(.,"Who killed ")]', document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null);
	if (t.singleNodeValue) {
        var title = t.singleNodeValue.innerHTML;
        // console.log('title is '+title);
        titleKey = title;
	    var tt = document.evaluate( '//td[contains(.,"You have been on this case for ")]', document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null);
	    if (tt.singleNodeValue) {
            var re = tt.singleNodeValue.innerHTML.match(/You have been on this case for ([0-9][0-9]*) minute/);
            if (re) {
                var mins = re[1];
                // console.log('minutes is '+mins);
                if (mins!=0) 
                    getTruthAndLies();
            }
        }
    }
}

function gatherLocations() {
    var rc = [];
	var locs = document.evaluate( '//a[contains(@href,"wham.php?visit=")]', document, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE,null);
    for (var i=0;i<locs.snapshotLength;i++) {
        var loc = locs.snapshotItem(i);
        var aloc = loc.firstChild.firstChild.innerHTML;
        rc[rc.length] = aloc;
        // console.log('location: '+aloc);
    }
    return rc;
}

function gatherPeople() {
    var rc = [];

    for (var p in personPlaceJob) {
        rc[rc.length] = p;
    }

    return rc;
}

function gatherJobs() {
    var rc = [];

    for (var p in personPlaceJob) {
        rc[rc.length] = personPlaceJob[p].job;
    }

    return rc;
}

function highlightHover(e) {
    var tgt = this.getAttribute('tgt');
    var tgtnode = document.evaluate( '//a[@href="wham.php?visit='+tgt+'"]', document, null, XPathResult.FIRST_ORDERED_NODE_TYPE,null);
    if (tgtnode.singleNodeValue) {
        tgtnode.singleNodeValue.setAttribute('style','color:blue;');
        tgtnode.singleNodeValue.firstChild.setAttribute('size','3');
    }
}

function unhighlightHover(e) {
    var tgt = this.getAttribute('tgt');
    var tgtnode = document.evaluate( '//a[@href="wham.php?visit='+tgt+'"]', document, null, XPathResult.FIRST_ORDERED_NODE_TYPE,null);
    if (tgtnode.singleNodeValue) {
        tgtnode.singleNodeValue.setAttribute('style','');
        tgtnode.singleNodeValue.firstChild.setAttribute('size','2');
    }
}

function gatherMorePeople(people) {
    var re = /^[A-Z][a-z][A-Za-z]* [A-Z][a-z][A-Za-z]*(\s[A-Z][a-z][A-Za-z]*)?$/;
    var rea = /^wham\.php\?ask=([1-9])/;
    var asks = document.evaluate( '//a[contains(@href,"wham.php?ask=")]', document, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE,null);
    for (var i=0;i<asks.snapshotLength;i++) {
        var a = asks.snapshotItem(i);
        var subj = a.innerHTML;
        if (subj.match(re)) {
            if (people.indexOf(subj)<0) {
                people[people.length] = subj;
            }
            //console.log('Extra person: '+subj);
        }
        var m = a.getAttribute('href').match(rea);
        if (m) {
            a.setAttribute('tgt',m[1]);
            a.addEventListener('mouseover',highlightHover,false);
            a.addEventListener('mouseout',unhighlightHover,false);
        }
    }
}

function gatherMoreJobs(jobs) {
    var re = /^the victim's ([-a-z ]*)$/;
    var rea = /^wham\.php\?ask=rel&w=([1-9])/;
    var asks = document.evaluate( '//a[contains(@href,"wham.php?ask=rel")]', document, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE,null);
    for (var i=0;i<asks.snapshotLength;i++) {
        var a = asks.snapshotItem(i);
        var subj = a.innerHTML;
        var w = subj.match(re);
        if (w) {
            if (jobs.indexOf(w[1])<0) {
                jobs[jobs.length] = w[1];
                //console.log('Extra job: '+w[1]);
            }
        }
        var m = a.getAttribute('href').match(rea);
        if (m) {
            a.setAttribute('tgt',m[1]);
            a.addEventListener('mouseover',highlightHover,false);
            a.addEventListener('mouseout',unhighlightHover,false);
        }
    }
}

// -------------------------------------------------

function gatherPerson() {
	var p = document.evaluate( '//input[@value="Accuse!"]', document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null);
	if (p.singleNodeValue) {
	    p = document.evaluate( './/b', p.singleNodeValue.parentNode.parentNode, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null);
        if (p.singleNodeValue) {
            person = p.singleNodeValue.innerHTML;
            var role = String(p.singleNodeValue.nextSibling.nextSibling.nextSibling.nodeValue).trim();
            var loc = String(p.singleNodeValue.nextSibling.nextSibling.nextSibling.nextSibling.innerHTML).trim().replace(")","").replace("(","");
            //console.log(person+', '+role+' is at '+loc);

            var peep = personPlaceJob[person];
            if (peep) {
                peep.job = role;
                peep.place = loc;
            } else {
                personPlaceJob[person] = {
                    job:role,
                    place:loc
                };
            }
            if (people.indexOf(person)<0)
                people[people.length] = person;
            if (jobs.indexOf(role)<0)
                jobs[jobs.length] = role;
            //console.log('Person: '+person);
        } 
    } 
}

function gatherAnswer(person) {
    if (!person) {
        return;
    }
 	var a = document.evaluate( '//td[@colspan="2" and @width="500"]', document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null);
 	if (a.singleNodeValue) {
        var t = a.singleNodeValue.innerHTML;
        var p,l,j;

        var accused = checkAccused(t);
        //console.log('accused? ' + accused);

        for (var i in people) {
            var re = new RegExp('\\b'+people[i]+'\\b');
            if (re.test(t)) {
                p = people[i];
                //console.log('Talking about person '+p);
            }
        }

        for (var i in places) {
            var re = new RegExp('\\b'+places[i]+'\\b');
            if (re.test(t)) {
                if (!l || l.length<places[i].length) {
                    l = places[i];
                }
                //console.log('Talking about location '+l);
            }
        }

        var ts = t.replace(/I don't know how s*he got stuck with [a-z -]* like me, either\./,'');
        // console.log('looking for jobs');
        for (var i in jobs) {
            var re = new RegExp('\\b'+jobs[i]+'\\b');
            var w = ts.search(re);
            if (w>=3 && ts.substr(w-3,3).toLowerCase()!="ex-") {
                if (!j || j.length<jobs[i].length) {
                    j = jobs[i];
                }
                // console.log('Talking about job '+j);
            }
        }

        var map = personSays[person];
        if (!map) {
            map = new Object();
            personSays[person] = map;
        }
        if (p && l) {
            //console.log(p + ' -> ' + l);
            map[p+'.place'] = l;
        }
        if (p && j) {
            //console.log(p + ' -> ' + j);
            map[p+'.job'] = j;
        }
        if (l && j) {
            //console.log(l + ' -> ' + j);
            map[l] = j;
        }
        if (accused) {
            if (p) map.accused = p;
            else if (j) map.accused = j;
            else map.accused = 'n/a';
        }
    } 
}

function checkAccused(text) {
    var re = /("Do you have any thoughts on who did it\?" you ask.|"Help me out here," you say\. "You know all the people in this house. Who's the most likely killer\?"|"Who do you think is the most likely to be the killer\?" you ask\.|"Who do you think the killer is\?" you ask.|"Does anyone else here seem particularly suspicious to you\?" you ask\.|"If you had to guess," you ask, "who would you say did it\?")/;

    return re.test(text);
}

// -------------------------------------------------

function saveTruthAndLies() {
    GM_setValue('truth',JSON.stringify(personPlaceJob));
    GM_setValue('lies',JSON.stringify(personSays));
    // var i = 1;
    // // console.log('Truth:');
    // for (var p in personPlaceJob) {
    //     console.log(i+'. '+p+': '+personPlaceJob[p].job+', '+personPlaceJob[p].place);
    //     i = i+1;
    // }
    // // console.log('Lies:');
    // i = 1;
    // for (var p in personSays) {
    //     var ps = personSays[p];
    //     var s = '';
    //     for (var rel in ps) {
    //         if (s)
    //             s += ', ';
    //         s += rel+':'+ps[rel];
    //     }
    //     console.log(i+'. '+p+' says '+s);
    //     i = i+1;
    // }
}

function getTruthAndLies() {
    var t = GM_getValue('truth','');
    if (t) {
        personPlaceJob = JSON.parse(t);
    }
    var lies = GM_getValue('lies','');
    if (lies) {
        personSays = JSON.parse(lies);
    }
}

function abbreviateName(name) {
    var re = /^([A-Z])[a-zA-Z]* ([A-Z])[a-zA-Z]*(\s?([A-Z]))?/;
    var m = name.match(re);
    if (m) {
        if (m[4])
            return m[1]+m[2]+m[4];
        return m[1]+m[2];
    }
    return name;
}

function display() {
    var tbl = document.createElement('table');
    tbl.setAttribute('style','width:95%;font-size:9pt;border-collapse:collapse;border:1px solid black;');
    tbl.setAttribute('cellpadding','3');
    tbl.setAttribute('cellspacing','2');

    // headers
    var c,r = tbl.insertRow();
    r.setAttribute('style','border:1px solid black;');
    c = r.insertCell();
    c.appendChild(document.createTextNode('Perp'));

    for (var ppl in people) {
        var name = abbreviateName(people[ppl]);
        c = r.insertCell();
        c.appendChild(document.createTextNode(name));
        // c = r.insertCell();
        // c.appendChild(document.createTextNode(name));
    }
    for (var loc in places) {
        c = r.insertCell();
        c.appendChild(document.createTextNode(places[loc]));
    }
    c = r.insertCell();
    c.appendChild(document.createTextNode('Acc'));

    for (var p in personSays) {
        r = tbl.insertRow();
        var rattrib = "border:1px solid black;";
        
        c = r.insertCell();
        if (p==person) {
            var b = document.createElement('b');
            b.appendChild(document.createTextNode(p));
            c.appendChild(b);
            rattrib = rattrib + "background-color:rgb(250,230,230);";
        } else {
            c.appendChild(document.createTextNode(p));
        }
        r.setAttribute('style',rattrib);
        c.appendChild(document.createElement('br'));
        if (personPlaceJob[p] && personPlaceJob[p].place) {
            var b = document.createElement('div');
            b.setAttribute('style','font-size:smaller;');
            if (personPlaceJob[p].job) {
                b.appendChild(document.createTextNode(personPlaceJob[p].job));
                b.appendChild(document.createElement('br'));
            }
            b.appendChild(document.createTextNode(personPlaceJob[p].place));
            c.appendChild(b);
        }
        
        for (var ppl in people) {
            c = r.insertCell();
            var place = personSays[p][people[ppl]+'.place'];
            if (place) {
                var d = document.createElement('div');
                // check truth
                if (personPlaceJob[people[ppl]] && personPlaceJob[people[ppl]].place) {
                    // we know this association
                    if (personPlaceJob[people[ppl]].place==place) {
                        d.setAttribute('style','color:green;border-bottom:1px solid black;');
                    } else {
                        d.setAttribute('style','color:red;border-bottom:1px solid black;');
                    }
                } else {
                    d.setAttribute('style','color:gray;border-bottom:1px solid black;');
                }
                d.appendChild(document.createTextNode(place));
                c.appendChild(d);
            } else {
                c.appendChild(document.createElement('br'));
            }

            //c = r.insertCell();
            //c.appendChild(document.createElement('br'));
            var job = personSays[p][people[ppl]+'.job'];
            if (job) {
                var d = document.createElement('div');
                // check truth
                if (personPlaceJob[people[ppl]] && personPlaceJob[people[ppl]].job) {
                    if (personPlaceJob[people[ppl]] && personPlaceJob[people[ppl]].job==job) {
                        d.setAttribute('style','color:green;border-top:1px solid black;');
                    } else {
                        d.setAttribute('style','color:red;border-top:1px solid black;');
                    }
                } else {
                    d.setAttribute('style','color:gray;border-top:1px solid black;');
                }
                d.appendChild(document.createTextNode(job));
                c.appendChild(d);
            } else {
                c.appendChild(document.createElement('br'));
            }
        }
        for (var loc in places) {
            c = r.insertCell();
            var job = personSays[p][places[loc]];
            if (job) {
                var d = document.createElement('div');
                d.setAttribute('style','color:gray;');
                // check truth
                for (var others in personPlaceJob) {
                    var other = personPlaceJob[others];
                    if (other.job==job && other.place) {
                        if (other.place==places[loc]) {
                            d.setAttribute('style','color:green;');
                        } else {
                            d.setAttribute('style','color:red;');
                        }
                        break;
                    } else if (other.place==places[loc] && other.job) {
                        if (other.job==job) {
                            d.setAttribute('style','color:green;');
                        } else {
                            d.setAttribute('style','color:red;');
                        }
                        break;
                    }
                }
                d.appendChild(document.createTextNode(job));
                c.appendChild(d);
            }
        }
        c = r.insertCell();
        if (personSays[p].accused) {
            c.appendChild(document.createTextNode(personSays[p].accused));
        }
    }
    document.body.appendChild(tbl);
}
    
// -------------------------------------------------

getTitle();
places = gatherLocations();
people = gatherPeople();
jobs = gatherJobs();
gatherPerson();
gatherMorePeople(people);
gatherMoreJobs(jobs);
gatherAnswer(person);
saveTruthAndLies();
display();