R00t Success Chance

Calculates success chance for r00t field actions.

// ==UserScript==
// @name           R00t Success Chance
// @description    Calculates success chance for r00t field actions.
// @namespace      http://userscripts.org/users/dtkarlsson
// @include        http*://*animecubed.com/billy/bvs/villagefields*
// @include        http*://*animecubedgaming.com/billy/bvs/villagefields*
// @licence        MIT; http://www.opensource.org/licenses/mit-license.php
// @copyright      2009, Daniel Karlsson
// @version        0.12.0
// @history        0.12.0 New domain - animecubedgaming.com - Channel28
// @history        0.11.0 Now https compatible (Updated by Channel28)
// @history        0.10.0 Added rough estimate of max difficulty
// @history        0.9.1 Text was supposed to be white, now it is
// @grant          none
// ==/UserScript==

var jutsuTypes = {
	gen: "Genjutsu",
	nin: "Ninjutsu",
	tai: "Taijutsu",
	dou: "Doujutsu"
}
var basicJutsuTypes = {
	gen: "Genjutsu",
	nin: "Ninjutsu",
	tai: "Taijutsu"
}

function Ability()
{
	this.lvl = 0;
	this.str = 0;
	this.rng = 0;
	this.suc = 0;
}

function Ninja()
{
	this.gen = new Ability();
	this.nin = new Ability();
	this.tai = new Ability();
	this.dou = new Ability();
}

function Challenge()
{
	this.dif = 0;
	this.suc = 0;
}

function Mission()
{
	this.gen = new Challenge();
	this.nin = new Challenge();
	this.tai = new Challenge();
	this.dou = new Challenge();
	this.order = [];
	this.crank = 0;
}

// Math stuff below

function binomialCoefficient(n, k)
{
	// n!/[k!(n-k)!]
	if (k > n || k < 0)
		return 0;
	k = Math.max(k, n - k);
	var i = 1;
	var j = k + 1;
	var c = 1;
	// i = 1 ... n-k => (n-k)!
	// j = k+1 ... n => n!/k!
	while (j <= n)
		c *= j++ / i++;
	return c;
}

function binomdist(k, n, p, cumulative)
{
	// Cumulative distribution, k or less successes in n trials
	if (cumulative) {
		var sum = 0;
		for (var i = 0; i <= k; i++)
			sum += binomdist(i, n, p, false);
		return sum;
	}
	// Exactly k successes in n trials with probability p
	return binomialCoefficient(n, k) * Math.pow(p, k) * Math.pow(1 - p, n - k);
}

function successChance(challenge, ability, crank)
{
	if (!crank)
		crank = 0;

	// Successful rolls required
	var req = (challenge.suc + crank)- ability.suc;
	if (req > ability.lvl)
		return 0;
	else if (req <= 0)
		return 1;

	if ((challenge.dif + crank) > ability.rng)
		return 0;

	// Success probability per roll
	var prob = (ability.str + ability.rng - (challenge.dif + crank) + 1) / ability.rng;

	if (prob >= 1)
		return 1;

	return Math.min(0.9999, 1 - binomdist(req - 1, ability.lvl, prob, true));
}

function expectedSuccesses(challenge, ability, crank)
{
	if (!crank)
		crank = 0;

	if ((challenge.dif + crank) > ability.rng)
		return 0;

	// Success probability per roll
	var prob = (ability.str + ability.rng - (challenge.dif + crank) + 1) / ability.rng;
	
	return ability.suc + Math.min(1, prob) * ability.lvl;
}

function percent(n)
{
	if (n == 1.0)
		return "100%";
	else if (n == 0.0)
		return "0%";
	var p = Math.round(n * 1000) / 10;
	if (p > 99.9)
		return ">99.9%";
	else if (p < 0.1)
		return "<0.1%";
	return p + "%";
}

// Parsing

function parseAbilities()
{
	var ninja = new Ninja();
	var tables = document.evaluate("//table[count(descendant::table)=4]//table",
		document, null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null);
	
	for (var i = 0; i < tables.snapshotLength; i++) {
		var ab = new Ability();
		var type = "";
		
		var lines = tables.snapshotItem(i).textContent.split(/\n/);
		for (var l in lines) {
			var m;
			
			m = lines[l].match(/Suc: (\d+)( \(\+(\d+)\))?/);
			if (m) {
				ab.suc += parseInt(m[1]);
				if (m[3])
					ab.suc += parseInt(m[3]);
				continue;
			}
			
			m = lines[l].match(/Str: (\d+)( \(\+(\d+)\))?/);
			if (m) {
				ab.str += parseInt(m[1]);
				if (m[3])
					ab.str += parseInt(m[3]);
				continue;
			}
			
			m = lines[l].match(/Rng: (\d+)( \(\+(\d+)\))?/);
			if (m) {
				ab.rng += parseInt(m[1]);
				if (m[3])
					ab.rng += parseInt(m[3]);
				continue;
			}
			
			m = lines[l].match(/Lvl: (\d+)( \(\+(\d+)\))?/);
			if (m) {
				ab.lvl += parseInt(m[1]);
				if (m[3])
					ab.lvl += parseInt(m[3]);
				continue;
			}
			
			m = lines[l].match(/(\w{3}): (\d+)/);
			if (m) {
				type = m[1].toLowerCase();
				ab.lvl += parseInt(m[2]);
				continue;
			}
		}
		ninja[type] = ab;
	}

	return ninja;
}

function parseMission()
{
	var tables = document.evaluate("//table[count(descendant::table)=0]",
		document, null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null);
	
	var node = null;
	var mission = new Mission();
	for (var i = 0; i < tables.snapshotLength; i++) {
		var txt = tables.snapshotItem(i).textContent;
		if (/virus attack/i.test(txt)) {
			var lines = txt.split(/\n/);
			// Gen  D: 15  S: 15
			for (var l in lines) {
				var line = lines[l].replace(/\s+/g, " ");
				var m = line.match(/(\w{3}) D: (\d+) S: (\d+)/);
				if (m) {
					var type = m[1].toLowerCase();
					mission[type].dif = parseInt(m[2]);
					mission[type].suc = parseInt(m[3]);
					node = tables.snapshotItem(i);
				}
			}
			break;
		}
	}
	
	if (node)
		return {mission: mission, node: node};
}

var ninja = parseAbilities();
var missionNode = parseMission();

if (missionNode) {
	var mission = missionNode.mission;
	var node = missionNode.node;
	
	var p = 1;
	for (var t in jutsuTypes) {
		p *= successChance(mission[t], ninja[t]);
	}
	
	var tr = document.createElement("tr");
	var td = document.createElement("td");
	td.textContent = "Success chance: " + percent(p);
	td.style.color = "white";
	tr.appendChild(td);
	node.firstChild.appendChild(tr);
}

// Rough estimate of average field difficulty
function fieldChallenge(diff)
{
	diff = Math.min(230, diff);
	diff = Math.max(0, diff);
	var c = new Challenge();
	c.dif = Math.round(0.13 * diff + 15);
	c.suc = Math.round(0.11 * diff + 15);
	return c;
}

// Calculate max difficulty a ninja can reliably handle
var diff, p;
for (diff = 230; diff >= 0; diff -= 5) {
	p = 1;
	for (var t in basicJutsuTypes)
		p = Math.min(p, successChance(fieldChallenge(diff), ninja[t]));
	if (p > 0.95)
		break;
}
var form = document.getElementsByName("field")[0];
var div = document.createElement("div");
div.textContent = "Recommended difficulty (estimate): " + diff + " (" + percent(p) + ")";
form.parentNode.insertBefore(div, form);