Week 1 · The Vault · Night 4 of 31
Night 4 · A Random Terror
📺 “Every good horror host has a segment where the audience never knows what’s coming. Tonight, neither will your program. We’re going to teach it to pick a monster at random, and, more importantly, to be honest about how random it really is.”
Tonight you’ll learn
math/rand: random numbers and why they need a seed- Functions that return two values
- Passing a random source into a function instead of hiding it inside
- The
randomcommand
Where we are: list prints three monsters.
Randomness, and the trick behind it
Computers can’t do random. What math/rand gives you is a very long, very scrambled sequence of numbers that looks random, starting from a seed. Same seed, same sequence, every time. That sounds like a flaw and it’s actually a gift: on Night 26 we’ll use a fixed seed to make the whole program replay exactly, which is how you record demos and reproduce bugs.
For a genuine surprise, seed with something that changes: the current time in nanoseconds.
A function that returns two things
Add to the bottom of internal/monsters/monsters.go:
// RandomFact picks a random monster and one of its facts using r.
func RandomFact(r *rand.Rand) (Monster, string) {
m := monsters[r.Intn(len(monsters))]
return m, m.Facts[r.Intn(len(m.Facts))]
}
and import the package at the top of the file, under the package line:
import "math/rand"
Read the function’s signature slowly: it takes r, a pointer to a random source, and returns two values, a Monster and a string. Multiple return values are everywhere in Go; value, err is the most common pair, and you’ll write hundreds of them.
r.Intn(n) gives a whole number from 0 up to but not including n. len(monsters) is the number of monsters, so monsters[r.Intn(len(monsters))] is a random one, and the same trick picks a fact.
Why take r as a parameter instead of making one inside? Because whoever calls RandomFact then controls the randomness. A test can pass a source with a known seed and check the exact answer. The demo recorder can pass a fixed seed. The command passes a clock-seeded one. The function itself doesn’t need to know or care.
The random command
Create cmd/random.go:
package cmd
import (
"fmt"
"math/rand"
"time"
"github.com/hungovercoders/terminal-of-terror/internal/monsters"
"github.com/spf13/cobra"
)
var randomCmd = &cobra.Command{
Use: "random",
Short: "Get a random monster fact",
Run: func(cmd *cobra.Command, args []string) {
r := rand.New(rand.NewSource(time.Now().UnixNano()))
m, fact := monsters.RandomFact(r)
fmt.Println("🎃 Random Terror Fact 🎃")
fmt.Println()
fmt.Println(m.Name)
fmt.Println(fact)
},
}
func init() {
rootCmd.AddCommand(randomCmd)
}
rand.NewSource(seed) makes the scrambled sequence; rand.New(source) wraps it in the handy methods like Intn. time.Now().UnixNano() is the seed: nanoseconds since 1970, different on every run.
m, fact := monsters.RandomFact(r) receives both return values at once.
Run it
go run . random
go run . random
🎃 Random Terror Fact 🎃
Frankenstein's Monster
The creature is never named in the novel; Frankenstein is his creator
Run it a few times; the fact changes. Now change the seed to a constant, rand.NewSource(13), and run it again. Same fact every time. Put the clock back afterwards.
Try it
- Print the monster’s
Originon a third line. - Import order: Go groups standard-library imports first, then a blank line, then everything else.
gofmtsorts each group alphabetically but won’t move things between groups; the blank line is yours to add. Get it wrong on purpose and see how it looks. go vet ./...checks every package in the module for common mistakes (the...means “and everything below”). Run it now. It should say nothing, which is good news.
💀 Terrifying fact
A slice is three numbers: a pointer to some storage, a length and a capacity. Handing a slice to a function copies those three numbers, not the elements. So GetAllMonsters() returns a view of the vault’s own storage, and anything that sorts or overwrites the returned slice changes the vault for everyone. It’s fast and it’s a trap. Last night’s homework sorted the vault by accident. When that matters, copy first: out := append([]Monster{}, monsters...).
🕯️ Before dawn
Give random a --monster flag so random --monster dracula picks a random fact from Dracula only. In init, before AddCommand, add:
randomCmd.Flags().StringVar(&which, "monster", "", "Only facts about this monster")
with var which string declared above. In Run, if which isn’t empty, loop over GetAllMonsters() for a monster whose name matches (strings.EqualFold ignores case) and pick from its facts. Print a helpful message if there’s no such monster. On Night 8 this becomes a proper argument with fuzzy matching.
📺 “Chance has entered the building. Tomorrow night the monsters leave the code entirely and move somewhere they can be edited without a compiler. Coffins, essentially.”