|
/* |
|
esversion: 6 |
|
*/ |
|
|
|
/* |
|
* Implements "The Oracle" by Ray Otus in Javascript |
|
* The Oracle: https://rayotus.itch.io/oracle |
|
* JavaScript source: |
|
* JavaScript author: shawn@medero.net |
|
*/ |
|
|
|
/* |
|
* Utilities functions/classes/etc |
|
*/ |
|
|
|
/* |
|
* RANDOM NUMBER GENERATOR |
|
*/ |
|
|
|
function randomNumberGenerator(seed) { |
|
// LCG using GCC's constants |
|
this.m = 0x80000000; // 2**31; |
|
this.a = 1103515245; |
|
this.c = 12345; |
|
|
|
this.state = seed ? seed : Math.floor(Math.random() * (this.m - 1)); |
|
} |
|
randomNumberGenerator.prototype.nextInt = function() { |
|
this.state = (this.a * this.state + this.c) % this.m; |
|
return this.state; |
|
}; |
|
randomNumberGenerator.prototype.nextFloat = function() { |
|
// returns in range [0,1] |
|
return this.nextInt() / (this.m - 1); |
|
}; |
|
randomNumberGenerator.prototype.nextRange = function(start, end) { |
|
// returns in range [start, end): including start, excluding end |
|
// can't modulu nextInt because of weak randomness in lower bits |
|
var rangeSize = end - start; |
|
var randomUnder1 = this.nextInt() / this.m; |
|
return start + Math.floor(randomUnder1 * rangeSize); |
|
}; |
|
randomNumberGenerator.prototype.choice = function(array) { |
|
return array[this.nextRange(0, array.length)]; |
|
}; |
|
|
|
function xmur3(str) { |
|
for (var i = 0, h = 1779033703 ^ str.length; i < str.length; i++) |
|
h = Math.imul(h ^ str.charCodeAt(i), 3432918353), |
|
h = h << 13 | h >>> 19; |
|
return function() { |
|
h = Math.imul(h ^ h >>> 16, 2246822507); |
|
h = Math.imul(h ^ h >>> 13, 3266489909); |
|
return (h ^= h >>> 16) >>> 0; |
|
}; |
|
} |
|
|
|
function uuidv4() { |
|
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { |
|
var r = Math.random() * 16 | 0, |
|
v = c == 'x' ? r : (r & 0x3 | 0x8); |
|
return v.toString(16); |
|
}); |
|
} |
|
|
|
const RANDSEED = xmur3(uuidv4()); |
|
const RNG = new randomNumberGenerator(RANDSEED()); |
|
|
|
/* |
|
* UTILITIES |
|
*/ |
|
|
|
const add = (a, b) => a + b; |
|
const numberAsc = (a, b) => a - b; |
|
const numberDesc = (a, b) => b - a; |
|
|
|
const hasDuplicates = function(arr) { |
|
return new Set(arr).size !== arr.length; |
|
}; |
|
|
|
const arrayToSentence = function(arr) { |
|
let tempArr = [...arr]; |
|
if (tempArr.length == 1) { |
|
return tempArr[0]; |
|
} else { |
|
let last = tempArr.pop(); |
|
let andStr = ", and "; |
|
if (arr.length == 2) { |
|
andStr = " and "; |
|
} |
|
return tempArr.join(', ') + andStr + last; |
|
} |
|
}; |
|
|
|
const capitalize = function(stringToCap) { |
|
return stringToCap.charAt(0).toUpperCase() + stringToCap.slice(1); |
|
}; |
|
|
|
const getRandom = function(arr, n = 1) { |
|
let result = new Array(n); |
|
let len = arr.length; |
|
let taken = new Array(len); |
|
if (n > len) { |
|
throw new RangeError("getRandom: more elements taken than available"); |
|
} |
|
while (n--) { |
|
let x = Math.floor(Math.random() * len); |
|
result[n] = arr[x in taken ? taken[x] : x]; |
|
taken[x] = --len in taken ? taken[len] : len; |
|
} |
|
return result; |
|
}; |
|
|
|
|
|
function getOccurrence(array, value) { |
|
var count = 0; |
|
array.forEach((v) => (v === value && count++)); |
|
return count; |
|
} |
|
|
|
function onlyUnique(value, index, self) { |
|
return self.indexOf(value) === index; |
|
} |
|
|
|
const getClosestKey = function(arr, target, u) { |
|
if (arr.hasOwnProperty(target)) { |
|
return target; |
|
} |
|
let keys = Object.keys(arr); |
|
keys.sort(function(a, b) { |
|
return a - b; |
|
}); |
|
|
|
for (var i = 0, prev; i < keys.length; i++) { |
|
if (Number(keys[i]) > target) { |
|
return prev === u ? u : +prev; |
|
} |
|
prev = keys[i]; |
|
} |
|
return +keys[i - 1]; |
|
}; |
|
|
|
const reverseMapping = o => Object.keys(o).reduce((r, k) => |
|
Object.assign(r, { |
|
[o[k]]: (r[o[k]] || []).concat(k) |
|
}), {}); |
|
|
|
const find_duplicate_in_array = function(arra1) { |
|
const object = {}; |
|
const result = []; |
|
|
|
arra1.forEach(item => { |
|
if (!object[item]) { |
|
object[item] = 0; |
|
} |
|
object[item] += 1; |
|
}); |
|
|
|
for (const prop in object) { |
|
if (object[prop] >= 2) { |
|
result.push(prop); |
|
} |
|
} |
|
|
|
return result; |
|
}; |
|
|
|
function randomUnweightedArrayItem(data) { |
|
return data[Math.floor(Math.random() * data.length)]; |
|
} |
|
|
|
function randomWeightedArrayItem(data) { |
|
// First, we loop the main dataset to count up the total weight. We're starting the counter at one because the upper boundary of Math.random() is exclusive. |
|
let total = 1; |
|
for (let i = 0; i < data.length; ++i) { |
|
total += data[i][1]; |
|
} |
|
|
|
// Total in hand, we can now pick a random value akin to our |
|
// random index from before. |
|
const threshold = Math.floor(Math.random() * total); |
|
|
|
// Now we just need to loop through the main data one more time |
|
// until we discover which value would live within this |
|
// particular threshold. We need to keep a running count of |
|
// weights as we go, so let's just reuse the "total" variable |
|
// since it was already declared. |
|
total = 0; |
|
for (let i = 0; i < data.length; ++i) { |
|
// Add the weight to our running total. |
|
total += data[i][1]; |
|
|
|
// If this value falls within the threshold, we're done! |
|
if (total >= threshold) { |
|
return data[i][0]; |
|
} |
|
} |
|
} |
|
|
|
function isNumeric(str) { |
|
if (typeof str != "string") return false // we only process strings! |
|
return !isNaN(str) && // use type coercion to parse the _entirety_ of the string (`parseFloat` alone does not do this)... |
|
!isNaN(parseFloat(str)) // ...and ensure strings of whitespace fail |
|
} |
|
|
|
function isOdd(num) { return num % 2;} |
|
|
|
/* |
|
* WORLDS STUPIDEST DICE ROLLER |
|
*/ |
|
|
|
const Dice = { |
|
"roll": function(sides) { |
|
return RNG.choice(Array.from(Array(sides), (_, i) => i + 1)); |
|
}, |
|
"d6": function() { |
|
return this.roll(6); |
|
}, |
|
"twoD6": function() { |
|
let result = {}; |
|
result.diceRolls = [this.roll(6),this.roll(6)]; |
|
result.total = result.diceRolls.reduce(add); |
|
return result; |
|
}, |
|
"threeD6": function() { |
|
let result = {}; |
|
result.diceRolls = [this.roll(6),this.roll(6),this.roll(6)]; |
|
result.total = result.diceRolls.reduce(add); |
|
return result; |
|
}, |
|
"dropHighest": function(arr) { |
|
arr.sort(numberAsc); |
|
arr.pop(); |
|
return arr; |
|
}, |
|
"dropLowest": function(arr) { |
|
arr.sort(numberDesc); |
|
arr.pop(); |
|
return arr; |
|
}, |
|
"dm": function(num) { |
|
return Math.floor((num / 3) - 2); |
|
} |
|
}; |
|
|
|
/* |
|
* THE ORACLE is based on Ray Otus's "The Oracle": |
|
* https://rayotus.itch.io/oracle |
|
* |
|
* The Oracle by Ray Otus is licensed under a Creative Commons Attribution 4.0 International License. |
|
* |
|
* The JavaScript library version of The Oracle is similarily licensed. |
|
*/ |
|
const TheOracle = class { |
|
constructor(stability=6,possibility="Somewhat") { |
|
this._stability = stability; |
|
this.stabilities = [1,2,3,4,5,6,7,8,9,10]; |
|
this.stabilityLabels = [...this.stabilities].map(x => x.toString()); |
|
/* |
|
what is this? |
|
stability cut-offs |
|
possibility label: [<=4, 5, >=6] |
|
*/ |
|
this.oracleMap = { |
|
"Certain": [7,6,5], |
|
"Very likely": [9,8,7], |
|
"Likely": [10,10,9], |
|
"Somewhat": [11,11,11], |
|
"Not very": [12,12,12], |
|
"Unlikely": [13,13,14], |
|
"No way": [14,15,16], |
|
"Impossible": [16,17,18] |
|
}; |
|
this.possibilityLabels = Object.keys(this.oracleMap); |
|
this._possibility = possibility; |
|
} |
|
get stability() { |
|
return this._stability; |
|
} |
|
set stability(num) { |
|
try { |
|
num = Number(num); |
|
} catch (error) { |
|
alert(JSON.stringify(error)); |
|
} |
|
this._stability = num; |
|
} |
|
get possibility() { |
|
return this._possibility; |
|
} |
|
set possibility(possibility) { |
|
this._possibility = possibility; |
|
} |
|
stabilityCutoff(stabilityNum) { |
|
let cutoffIndex = 0; // <= 4 |
|
if (stabilityNum == 5) { |
|
cutoffIndex = 1; |
|
} else if (stabilityNum > 5) { |
|
cutoffIndex = 2; |
|
} |
|
return cutoffIndex; |
|
} |
|
sceneSetup() { |
|
let result = {}; |
|
result.throw = Dice.d6(); |
|
result.stability = this.stability; |
|
result.sceneState = "as expected"; |
|
if (result.throw >= this.stability) { |
|
result.sceneState = "twisted"; |
|
} |
|
result.toString = function() { |
|
return `(Stability: ${this.stability}) ${this.sceneState}`; |
|
}; |
|
return result; |
|
} |
|
|
|
event() { |
|
const eventsTable = { |
|
"2": "Resolve a thread", |
|
"3": "Introduce a new character", |
|
"4": "A good thing happens to a non- viewpoint character", |
|
"5": "A good thing happens to viewpoint character", |
|
"6": "Reveal something about/ advance a thread", |
|
"7": "A non-viewpoint character takes action", |
|
"8": "A bad thing happens to viewpoint character", |
|
"9": "A bad thing happens to non- viewpoint character", |
|
"10": "A notable, but unimportant thing occurs", |
|
"11": "Something important happens \"off screen\"", |
|
"12": "Complicate a Thread" |
|
}; |
|
let roll = Dice.twoD6(); |
|
return eventsTable[roll.total]; |
|
} |
|
|
|
askQuestion() { |
|
let result = {}; |
|
let stabilityCutoffIndex = this.stabilityCutoff(this.stability); |
|
result.stability = this.stability; |
|
result.possibility = this.possibility; |
|
result.throw = Dice.threeD6(); |
|
result.target = this.oracleMap[this.possibility][stabilityCutoffIndex]; |
|
result.outcome = "No"; |
|
result.exceptional = false; |
|
result.event = false; |
|
if (result.throw.total >= result.target) { |
|
result.outcome = "Yes"; |
|
} |
|
if (result.throw.diceRolls[0] == result.throw.diceRolls[1]) { |
|
result.exceptional = true; |
|
/* |
|
* this is my drift on Ray's drift |
|
* Ray's rules say to add a "but" or "and" |
|
* but in my play those two words are not |
|
* interchangable. "and" magnifies a result |
|
* a "but" provides a "twist" on the result. |
|
*/ |
|
if (isOdd(result.throw.diceRolls[0])) { |
|
result.outcome += ", but"; |
|
} else { |
|
result.outcome += ", and"; |
|
} |
|
} |
|
if (isOdd(result.throw.diceRolls[0]) && isOdd(result.throw.diceRolls[1]) && isOdd(result.throw.diceRolls[0])) { |
|
result.event = this.event(); |
|
} |
|
result.toString = function() { |
|
let str = `(Stability: ${this.stability}, Possibility: ${this.possibility}) ${this.outcome}`; |
|
if (result.event) { |
|
str += `. Event: ${this.event}`; |
|
} |
|
return str; |
|
}; |
|
return result; |
|
} |
|
}; |