Skip to content

Instantly share code, notes, and snippets.

@soypunk
Last active November 30, 2020 23:38
Embed
What would you like to do?
Implementation of Ray Otus's "The Oracle" in Javascript. (Interesting bits starting around line 263 or so.)

How do you use this?

let stabilityValue = 6;
let possibilityValue = "Somewhat";
let TO = new TheOracle(stabilityValue, possibilityValue);

Stability Values & Labels

TO.stabilities;
TO.stabilityLabels;

Possibility Labels

TO.possibilityLabels

Set the Stability

Pass an integer that is one of the possible TO.stabilities values

TO.stability = 6;

Set the Possibility

Pass a string that is one of the possible TO.possibilityLabels values

TO.possibility = "Somewhat";

Set the Scene

TO.sceneSetup();

This returns an object:

  • throw: an integer representing the result of a 1d6 dice roll
  • stability: an integer representing the stabiltiy you've set
  • sceneSetup: "as expected" | "twisted"

There's a toString() method on this object that will provide a default bit of text you can use:

(Stability: 6) as expected

Ask a Question

TO.askQuestion();

This returns an object:

  • throw: an object, representing the 3d6 dice roll
    • diceRolls: an array of dices roll (length of 3)
    • total: the total value of the three dice
  • event: false OR a string representing the result of the event() method.
  • stability: an integer representing the stabiltiy you've set
  • possibility: a string representing the possibility you've set
  • outcome: "Yes" | "Yes, but" | "Yes, and" | "No" | "No, but" | "No, and"

There's a toString() method on this object that will provide a default bit of text you can use:

(Stability: 6, Possibility: Somewhat) Yes

/*
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;
}
};
function randomNumberGenerator(t){this.m=2147483648,this.a=1103515245,this.c=12345,this.state=t||Math.floor(Math.random()*(this.m-1))}function xmur3(t){for(var e=0,i=1779033703^t.length;e<t.length;e++)i=(i=Math.imul(i^t.charCodeAt(e),3432918353))<<13|i>>>19;return function(){return i=Math.imul(i^i>>>16,2246822507),i=Math.imul(i^i>>>13,3266489909),(i^=i>>>16)>>>0}}function uuidv4(){return"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g,function(t){var e=16*Math.random()|0;return("x"==t?e:3&e|8).toString(16)})}randomNumberGenerator.prototype.nextInt=function(){return this.state=(this.a*this.state+this.c)%this.m,this.state},randomNumberGenerator.prototype.nextFloat=function(){return this.nextInt()/(this.m-1)},randomNumberGenerator.prototype.nextRange=function(t,e){var i=e-t,r=this.nextInt()/this.m;return t+Math.floor(r*i)},randomNumberGenerator.prototype.choice=function(t){return t[this.nextRange(0,t.length)]};const RANDSEED=xmur3(uuidv4()),RNG=new randomNumberGenerator(RANDSEED()),add=(t,e)=>t+e,numberAsc=(t,e)=>t-e,numberDesc=(t,e)=>e-t,hasDuplicates=function(t){return new Set(t).size!==t.length},arrayToSentence=function(t){let e=[...t];if(1==e.length)return e[0];{let i=e.pop(),r=", and ";return 2==t.length&&(r=" and "),e.join(", ")+r+i}},capitalize=function(t){return t.charAt(0).toUpperCase()+t.slice(1)},getRandom=function(t,e=1){let i=new Array(e),r=t.length,n=new Array(r);if(e>r)throw new RangeError("getRandom: more elements taken than available");for(;e--;){let o=Math.floor(Math.random()*r);i[e]=t[o in n?n[o]:o],n[o]=--r in n?n[r]:r}return i};function getOccurrence(t,e){var i=0;return t.forEach(t=>t===e&&i++),i}function onlyUnique(t,e,i){return i.indexOf(t)===e}const getClosestKey=function(t,e,i){if(t.hasOwnProperty(e))return e;let r=Object.keys(t);r.sort(function(t,e){return t-e});for(var n,o=0;o<r.length;o++){if(Number(r[o])>e)return n===i?i:+n;n=r[o]}return+r[o-1]},reverseMapping=t=>Object.keys(t).reduce((e,i)=>Object.assign(e,{[t[i]]:(e[t[i]]||[]).concat(i)}),{}),find_duplicate_in_array=function(t){const e={},i=[];t.forEach(t=>{e[t]||(e[t]=0),e[t]+=1});for(const t in e)e[t]>=2&&i.push(t);return i};function randomUnweightedArrayItem(t){return t[Math.floor(Math.random()*t.length)]}function randomWeightedArrayItem(t){let e=1;for(let i=0;i<t.length;++i)e+=t[i][1];const i=Math.floor(Math.random()*e);e=0;for(let r=0;r<t.length;++r)if((e+=t[r][1])>=i)return t[r][0]}function isNumeric(t){return"string"==typeof t&&(!isNaN(t)&&!isNaN(parseFloat(t)))}function isOdd(t){return t%2}const Dice={roll:function(t){return RNG.choice(Array.from(Array(t),(t,e)=>e+1))},d6:function(){return this.roll(6)},twoD6:function(){let t={};return t.diceRolls=[this.roll(6),this.roll(6)],t.total=t.diceRolls.reduce(add),t},threeD6:function(){let t={};return t.diceRolls=[this.roll(6),this.roll(6),this.roll(6)],t.total=t.diceRolls.reduce(add),t},dropHighest:function(t){return t.sort(numberAsc),t.pop(),t},dropLowest:function(t){return t.sort(numberDesc),t.pop(),t},dm:function(t){return Math.floor(t/3-2)}},TheOracle=class{constructor(t=6,e="Somewhat"){this._stability=t,this.stabilities=[1,2,3,4,5,6,7,8,9,10],this.stabilityLabels=[...this.stabilities].map(t=>t.toString()),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=e}get stability(){return this._stability}set stability(t){try{t=Number(t)}catch(t){alert(JSON.stringify(t))}this._stability=t}get possibility(){return this._possibility}set possibility(t){this._possibility=t}stabilityCutoff(t){let e=0;return 5==t?e=1:t>5&&(e=2),e}sceneSetup(){let t={};return t.throw=Dice.d6(),t.stability=this.stability,t.sceneState="as expected",t.throw>=this.stability&&(t.sceneState="twisted"),t.toString=function(){return`(Stability: ${this.stability}) ${this.sceneState}`},t}event(){return{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"}[Dice.twoD6().total]}askQuestion(){let t={},e=this.stabilityCutoff(this.stability);return t.stability=this.stability,t.possibility=this.possibility,t.throw=Dice.threeD6(),t.target=this.oracleMap[this.possibility][e],t.outcome="No",t.exceptional=!1,t.event=!1,t.throw.total>=t.target&&(t.outcome="Yes"),t.throw.diceRolls[0]==t.throw.diceRolls[1]&&(t.exceptional=!0,isOdd(t.throw.diceRolls[0])?t.outcome+=", but":t.outcome+=", and"),isOdd(t.throw.diceRolls[0])&&isOdd(t.throw.diceRolls[1])&&isOdd(t.throw.diceRolls[0])&&(t.event=this.event()),t.toString=function(){let e=`(Stability: ${this.stability}, Possibility: ${this.possibility}) ${this.outcome}`;return t.event&&(e+=`. Event: ${this.event}`),e},t}};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment