Random functions - ThePix/QuestJS GitHub Wiki

There are many occasions when it is useful to have a random number, whether it is the combination of a safe or used to determine if the player stuck the dragon with the glowing sword of Felduuk.

random.int(n1, n2)

Returns a random number from 0 to n1 if n2 is omitted, or from n1 to n2, inclusive. To simulate a normal six-sided dice, then you would do this:

random.int(1, 6)

random.chance(percentile)

Returns true percentile out of 100 times, false otherwise. This example will be true 20% of the time.

random.chance(20)

random.fromArray(arr, remove)

Returns a random element from the array, or null if it is empty. If remove is true, the selected element is removed from the array, preventing it being selected a second time.

random.shuffle(arr)

Returns the given array, in random order using the Fisher-Yates algorithm.

random.dice(dice)

Returns a random number based on the standard RPG dice notation. For example "2d6+3" means roll two six-sided dice and add three. Returns the number if sent a number.

  • It can cope with complex strings such as "2d8-3d6".
  • You can take the highest or lowest dice; for example "3d6h2" will roll three six-sided dice, and just use the highest 2, while "3D6L" will use the lowest one.
  • You can "explode" dice; "3d6e" will roll again for every 6, and "3d6e5" will roll again for every 5 and over. If you use "E", rather than "e", rolling high on your bonus dice will allow a further roll, so potentially exploding dice can give very high numbers!
  • You can re-roll dice; "3d6r" will re-roll any 1, and 3d6r2 will re-roll any 2 and under.
  • You can count successes or failures; "3d6C4" will count the number of rolls that were 4 or over, while "3d6c4" will count the number of rolls that were 4 or under. You must specify the qualifier - "3d6c" will cause an error.
  • You can also specify unusual dice, i.e., not a sequence from one to n, by separating each value with a colon, so "d1:5:6" rolls a three sided die, with 1, 5 and 6 on the sides. You can only use natural numbers - whole numbers from zero and above. For exploding dice, you must specify the number to be qualify, so "3d1:5:6e6" is okay, but "3d1:5:6e" is not.
  • It will cope with any number of parts, so "-19+2d1:5:6-d4" will be fine.
  • The rolls for the last call of random.dice are stored in random.diceLog.

If you want to do a range of numbers that do not start from zero, add or subtract the result. For example, if you want to roll from -1 to 1, do "d3-2".

Pass true as a second parameter and instead of a random result, it will give you the average expected value. This can be useful if you want to gauge what to expect.

random.dice("-19+2d1:5:6-d4", true)
-> -13.5

random.prime(number or array)

Loads up a buffer with the given number or array of numbers. The random.int function will grab the first number from the buffer and return that instead of a random number, if there is anything in the buffer. Note that the other random functions all use random.int, so you can use random.prime to force any of them to return a certain value (be aware that some may use it several times). Note that there is no checking, so random.int(4) could return 7 or even the string "seven" if that is what you put in the buffer. It is up to you to ensure the numbers you prime the buffer with make sense.

Note that calling this function resets the buffer, effectively removing any existing numbers in the buffer first.

This is most useful when testing, as you know in advance what the random number will be.

Seeding the random number generator

Use random.seed to seed the random number generator (which can be done in settings.js or elsewhere as required). It takes a string as an argument. Whenever you seed the random number generator with the same string, you will get the same sequence of numbers.

random.seed('butternut')

Note that the built-in random number generator does not allow seeding, so when you call this function Quest will start to use its own version, and the distribution is not guaranteed to be properly random. It is certainly not good enough for cryptography, but probably good enough for interactive fiction.

You can go back to the original random number generator be calling random.seed with no parameters.

random.seed()

Why would you want to do this? You might have a location that is randomly generator, but you want it the same every time. Perhaps a galaxy-spanning game where the player can visit randomly-generated star systems, but you want each star system to always be the same. Seed the random number generator with the name of the star, and then create it randomly. Every time the player arrives it it will have the same seed so the random elements will always be the same.

Note that this produces the same sequence each time. If you modify the code to get a new random number early in the sequence, all later random numbers will get changed. Let us suppose you have a version 2 of your galaxy game, and in this version you additionally randomly determine the star type before doing anything else. All your randomly generated planets will be different because they now use the next random number is the sequence. If this is important, you need to modify your game so new random numbers are only taken from the end of the sequence.

A Random Maze

By way of illustrating random numbers, here is the code to create a random maze. At one time mazes were popular in interactive fiction, but that is no longer the case, so I would not recommend including this in your game unless you can give it some twist that makes it actually fun to navigate.

In settings.js, we seed the random number generator, set the size and turn on mapping.

settings.size = 10
random.seed('butternut')
settings.libraries.push('node-map')

I am using a random seed so the map will be the same every time the map is played. If you want it to be different each time, there are some special considerations regarding exits as they are not saved. That is outside the scope of this article, but see here.

In data.js, we have a couple of rooms, one at the start and one at the end of the maze. The one at the end has to be named "maze_10_9" because the exit to it will be created by the next bit of code. Note that here and later we are using Link rather than Exit - Quest can then sort out the return exits.

createRoom("lounge", {
  desc:"The lounge is boring... but there is a maze to the northeast.",
  northeast:new Link('maze_0_0')
})

createRoom("maze_10_9", {
  desc:"You escaped the maze!",
})

This code generates 100 rooms, in two loops. The i variable is how far east the room is, and j how far north it is. A room is created with a name that includes i and j, so it is unique and predictable. We then can give the room exits to the north and east, exits in the other directions will be determined by the rooms in that direction. There is a 10% chance of an exit in both directions. Otherwise, it is 50/50 east or north, though we need to take account of whether this is the edge of the maze.

for (let i = 0; i < settings.size; i++) {
  for (let j = 0; j < settings.size; j++) {
    const r = createRoom("maze_" + i + "_" + j, {
      desc:"You are in a maze of twisty little passages, all alike.",
    })
    if (random.chance(10) && i < (settings.size - 1) && j < (settings.size - 1)) {
      r.east = new Link("maze_" + (i + 1) + "_" + j)
      r.north = new Link("maze_" + i + "_" + (j + 1))
    }
    else if ((random.chance(50) && i < (settings.size - 1)) || j === settings.size - 1) {
      r.east = new Link("maze_" + (i + 1) + "_" + j)
    }
    else {
      r.north = new Link("maze_" + i + "_" + (j + 1))
    }
  }
}

We can add some variety using the text processor. We need to use the textProcessor function directly so the description gets set in stone when the room is created. The text processor uses the same random functions, so also will be constant for a given seed.

for (let i = 0; i < settings.size; i++) {
  for (let j = 0; j < settings.size; j++) {
    const r = createRoom("maze_" + i + "_" + j, {
      desc:processText("you are in a maze of {random:twisty:winding:snaking:meandering} {random:little passages:dark tunnels}, all alike."),
    })
    if (random.chance(10) && i < (settings.size - 1) && j < (settings.size - 1)) {
      r.east = new Link("maze_" + (i + 1) + "_" + j)
      r.north = new Link("maze_" + i + "_" + (j + 1))
    }
    else if ((random.chance(50) && i < (settings.size - 1)) || j === settings.size - 1) {
      r.east = new Link("maze_" + (i + 1) + "_" + j)
    }
    else {
      r.north = new Link("maze_" + i + "_" + (j + 1))
    }
    if (random.chance(10)) {
      switch(random.int(2)) {
        case 0: r.desc += ' There is a clock on the wall.'; break;
        case 1: r.desc += ' There is a curious stain on the floor.'; break;
        case 2: r.desc += ' It smells of elderberries in here.'; break;
      }
    }
  }
}

Hmm, what if the player tries to examine the clock or smell what is in the room? The code for these things can become long fast, but the basic principles are not complicated. We will break out each random feature into its own function. To ensure items have unique names, the room name is appended. The clock is stopped at a random time - the minutes is randomly from 10 to 59 so we do not need to add a zero for single digit numbers.

for (let i = 0; i < settings.size; i++) {
  for (let j = 0; j < settings.size; j++) {
    const r = createRoom("maze_" + i + "_" + j, {
      desc:processText("you are in a maze of {random:twisty:winding:snaking:meandering} {random:little passages:dark tunnels}, all alike."),
    })
    if (random.chance(10) && i < (settings.size - 1) && j < (settings.size - 1)) {
      r.east = new Link("maze_" + (i + 1) + "_" + j)
      r.north = new Link("maze_" + i + "_" + (j + 1))
    }
    else if ((random.chance(50) && i < (settings.size - 1)) || j === settings.size - 1) {
      r.east = new Link("maze_" + (i + 1) + "_" + j)
    }
    else {
      r.north = new Link("maze_" + i + "_" + (j + 1))
    }
    if (random.chance(10)) {
      switch(random.int(2)) {
        case 0: randomClock(r); break;
        case 1: randomStain(r); break;
        case 2: randomSmell(r); break;
      }
    }
  }
}


function randomClock(r) {
  r.desc += ' There is a clock on the wall.'
  createItem('clock_' + r.name, {
    alias:'clock',
    loc:r.name,
    scenery:true,
    examine:'It seems to have stopped at ' + random.int(1, 12) + ':' + random.int(10, 59) + '.',
  })
}

function randomStain(r) {
  r.desc += ' There is a curious stain on the floor.'
  createItem('stain_' + r.name, {
    alias:'curious stain',
    loc:r.name,
    scenery:true,
    examine:'Is that blood?',
  })
}

function randomSmell(r) {
  r.desc += ' It smells of elderberries in here.'
  r.smell = 'Definitely elderberries. And not in a good way.'
}

Actually, a slicker way would be an array of functions, and to pick one of them at random. We need the functions before the loop now so JavaScript can find them (so how could it find the functions previously?).

const randomFeatures = [
  function(r) {
    r.desc += ' There is a clock on the wall.'
    createItem('clock_' + r.name, {
      alias:'clock',
      loc:r.name,
      scenery:true,
      examine:'It seems to have stopped at ' + random.int(1, 12) + ':' + random.int(10, 59) + '.',
    })
  },

  function(r) {
    r.desc += ' There is a curious stain on the floor.'
    createItem('stain_' + r.name, {
      alias:'curious stain',
      loc:r.name,
      scenery:true,
      examine:'Is that blood?',
    })
  },

  function(r) {
    r.desc += ' It smells of elderberries in here.'
    r.smell = 'Definitely elderberries. And not in a good way.'
  },
]

for (let i = 0; i < settings.size; i++) {
  for (let j = 0; j < settings.size; j++) {
    const r = createRoom("maze_" + i + "_" + j, {
      desc:processText("you are in a maze of {random:twisty:winding:snaking:meandering} {random:little passages:dark tunnels}, all alike."),
    })
    if (random.chance(10) && i < (settings.size - 1) && j < (settings.size - 1)) {
      r.east = new Link("maze_" + (i + 1) + "_" + j)
      r.north = new Link("maze_" + i + "_" + (j + 1))
    }
    else if ((random.chance(50) && i < (settings.size - 1)) || j === settings.size - 1) {
      r.east = new Link("maze_" + (i + 1) + "_" + j)
    }
    else {
      r.north = new Link("maze_" + i + "_" + (j + 1))
    }
    if (random.chance(10)) {
      randomFeatures[random.int(0, randomFeatures.length - 1)](r)
    }
  }
}

The line almost at the bottom is where the magic is done.

      randomFeatures[random.int(0, randomFeatures.length - 1)](r)

It might be worth breaking down. First look at this bit:

                     random.int(0, randomFeatures.length - 1)

We are getting a random integer from zero to one less than the length of the array. It is a good idea to set it up like this, rather than the actual number, so that if you add more entries to the list it will just work without you having to remember to update anything.

So we have the index of the function, now we get the function:

      randomFeatures[random.int(0, randomFeatures.length - 1)]

Once we have the function, we can call it, sending it the room as a parameter.

      randomFeatures[random.int(0, randomFeatures.length - 1)](r)