Skip to content

Instantly share code, notes, and snippets.

@soypunk
Last active November 30, 2020 23:38
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save soypunk/c99fef7b51154dbf2efa428a4f2bf3ef to your computer and use it in GitHub Desktop.
Save soypunk/c99fef7b51154dbf2efa428a4f2bf3ef to your computer and use it in GitHub Desktop.
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