January 01, 2005

The Ramble Chronicles: Randomness

One thing you need in most games is a source of randomness. In Tcl, random numbers are provided by the expr command's rand() function; but rand() can be made considerably more convenient by some judicious wrapping. Again, this is a Tcl-oriented essay rather than a game design essay; if the details don't interest you, pass on by.

The rand() function returns a floating-point value between 0 and 1, which is handy if you're working with raw probability figures. For many applications, though, what you're really looking for is an integer in some range. Here's a function that returns a random integer between $min and $max, inclusive:

proc rand {min max} {
    # Determine the number of possible values.
    set numValues [expr {$max - $min + 1}]

    # Get a random integer from 0 to numValues - 1
    set randInt [expr {int($numValues*rand())}]

    # Return a number from min to max
    return [expr {$min + $randInt}]
}

# Roll a six-sided die
puts "You rolled [rand 1 6]"

rand covers many of the cases I'm interested in; however, there's a particular application of rand that deserves further elaboration: picking an element randomly from a list. For example, suppose your game includes a creature that says something different every time the player talks to it. Provide the creature with a list of statements, and pick one randomly using pickfrom:

proc pickfrom {list} {
    set i [rand 0 [expr {[llength $list] - 1}]]
    set result [lindex $list $i]
    return $result
}

puts "The parrot said [pickfrom {
    {Polly wants a cracker!}
    {Shiver me timbers!}
    {Squawk!}
    {The treasure is hidden under the captain's pillow!}
}]" 

Sometimes you want to remove the selected element from the list, so that it can't be picked again. I use this a lot when randomly placing features on a newly created map. First I build up a list of open tiles, i.e., spots where I can legally put a chest, or a monster, or the player. Then I use takefrom to randomly choose tiles from the list. Because chosen tiles are removed from the list, I don't need to worry about putting two things in the same spot. Here's what takefrom looks like. Note that instead of passing a list, I'm passing a variable that contains a list; that's necessary so that takefrom can update the variable's value.

proc takefrom {listvar} {
    upvar $listvar list
    set i [rand 0 [expr {[llength $list] - 1}]]
    set result [lindex $list $i]
    set list [lreplace $list $i $i]
    return $result
}

set openlist [getopentiles $map]

set location [takefrom openlist]

puts "The player starts at $location!"

Between them, these three commands provide almost all of the randomness I need for Ramble...so far, anyway.

Posted by Will Duquette at January 1, 2005 01:25 PM