Possible new TagTime universal ping algorithm

If I did this right then here’s a graph with the amount of time you spend answering pings on the x-axis and the amount of time that each algorithm (blue = continuous, orange = discrete with 1-second granularity) will tell you you spent:

And here’s the same thing but with 0.1-second granularity:

(The probability of getting pung in s seconds after the last ping is 1-exp(-s/g) in the continuous case and 1-(1-d/g)^floor(s/d) in the discrete case, where d is the granularity, eg, 1 for seconds or 1/10 for deciseconds, and g is the mean gap in seconds.)

So I guess we can call that justification for using deciseconds in the new algorithm (if we actually want a new algorithm). Since I got seriously nerdsniped, I implemented it in Javascript. The RNG below has a period of 109 years in deciseconds. And it does the fancy fast-forwarding.

First, constants:

const GAP = 45*60       // Average gap between pings, in seconds

const URPING = 15338120000 // Birth of the new TagTime algorithm in deciseconds
const SEED = 20180809     // Initial state of the random number generator (RNG)
const IA = 3125        // (5^5) Multiplier used for LCG RNG
const IM = 34359738337 // (2^35-31) Modulus used for RNG (assumes 36-bit ints)

// Each decisecond has probability 1/(GAP*10) of being a ping, where GAP is the 
// average time between pings in seconds. The LCG random number generator
// returns a random integer uniformly in {1,...,IM-1} so LCG/IM is a U(0,1) 
// random variable and there's a ping iff 
//   LCG/IM < 1/(GAP*10) => LCG < IM/(GAP*10)
// so we define a constant for the RHS to compare to.
const THRESH = IM/(GAP*10)

Global variables:

let state = SEED // Global variable that's the state of the RNG
let lastping = URPING // Global var with decisecond of last ping

Random number generator, with fast-forwarding:

// Return a random integer in [1,IM-1]; changes the RNG state.
// wikipedia.org/wiki/Linear_congruential_generator
function lcg() { return state = IA * state % IM }

// Compute a*b % m when a*b might overflow
function multmod(a, b, m) {
  if (b === 0)   { return 0 }
  if (b%2 === 0) { return multmod(a*2%m, b/2, m) % m }
  else           { return (a + multmod(a, b-1, m)) % m }
}

// Efficiently compute a^n % m -- wikipedia.org/wiki/Modular_exponentiation
function powmod(a, n, m) {
  if (n === 0)   { return 1 }
  let x
  if (n%2 === 0) { x = powmod(a, n/2, m); return multmod(x, x, m) }
  else           { x = powmod(a, n-1, m); return multmod(a, x, m) }
}

// Fast-forward the RNG by doing i steps at once
function powlcg(i) { return state = multmod(powmod(IA, i, IM), state, IM) }

And finally, the function to get the time of the next ping, from any starting time:

// Current unixtime in deciseconds
function decinow() { return round(Date.now()/100) }

// Given unixtime in deciseconds, t, start checking at t+1 and return the first
// time we come to with a ping, which becomes the new lastping time.
function nextping(t) {
  if (t < lastping) { return 0 }            // going back in time not allowed!
  if (t > lastping) { powlcg(t-lastping) }  // fast-forward the RNG up thru t
  lastping = t+1
  while (lcg() >= THRESH) { lastping += 1 }
  return lastping
}

For example, nextping(decinow()) efficiently gives the next upcoming ping time (in deciseconds) and then you just keep calling nextping with the previous ping time to get the next.

Feel free to play with the code on Glitch.

Btw, I realized that we can’t use the nifty fast-forwarding in the original algorithm because we don’t know the exact number of times the the RNG will have been called in the past.

1 Like