JavaRush /Courses /Kotlin SELF /Random — generating random Int and Double values

Random — generating random Int and Double values

Kotlin SELF
Level 6, Lesson 4
Available

1. Random numbers in programs

Random numbers are a great way to make a program less “wooden.” When everything is predictable in advance, you quickly hit a ceiling: the game is always the same, tests are boring, and a simulation doesn’t feel like real life. Random lets you add variation: generate a dice roll, a random discount, an event probability, loot boxes, a “random” scenario. And yes, this is the exact moment when the computer suddenly pretends it’s not a deterministic machine, but a little bit of a wizard.

It’s important to agree on two things right away. First, in programming, “randomness” usually means pseudorandomness: the numbers look random, but inside it’s a clever algorithm. Second, you need boundary rules, otherwise you’ll constantly catch an “off by 1” error (the most annoying kind, because your brain insists: “but everything is correct!”).

kotlin.random.Random: the source of “randomness”

In Kotlin, the standard pseudorandom number generator is in the kotlin.random package. Typically, you use the Random object and call methods like nextInt() or nextDouble() on it.

Let’s start with the simplest idea: Random is like a “box of balls.” You ask: “give me a ball with a number,” and it hands you the next one. But to make your life easier, the methods have contracts: which numbers can come up and which cannot.

Mini example: just get “some” integer.

import kotlin.random.Random

fun main() {
    val x = Random.nextInt()
    println("x = $x") // x = (some integer, different each run)
}

Here it’s important not to expect “nice-looking” values. It can be negative, large, or small. If you need a specific range (and you almost always do), you set bounds.

2. Random.nextInt(from, until): the key rule about bounds

When you want a number “between A and B,” you typically use: Random.nextInt(from, until)

And here’s the key rule: from is included, and until is not included.

That is, the result lies in the range: from .. (until - 1)

This rule isn’t a “whim of Kotlin’s developers,” but a very practical convention: it’s convenient when working with ranges, sizes, and indices (later this will feel especially nice). In Kotlin documentation examples, you can see it in the classic “roll a die until you get 6”: it uses Random.nextInt(1, 7), and 7 is chosen specifically so that 6 is reachable.

Let’s lock this in with a small table (so it’s easier to remember):

Call Possible results
Random.nextInt(0, 10)
0..9
Random.nextInt(1, 7)
1..6
Random.nextInt(-3, 3)
-3..2

Notice that the last row is especially “tricky”: if you expected -3..3, you’ve already fallen into the classic trap.

3. The “+1” error: why a die is the best teacher

Every topic in programming has its own hidden pitfall. Loops have an infinite while, strings have a forgotten trailing space, null brings sadness and philosophy. For Random, the symbol is a d6 die (1..6). It perfectly illustrates the boundary problem.

Now we’ll build a small piece of a console app (let it be a training program called “LuckyCafe”: it will give the customer a random discount/bonus, simply so we have something to evolve).

First, let’s implement a “dice roll” correctly:

import kotlin.random.Random

fun main() {
    val dice = Random.nextInt(from = 1, until = 7)
    println("Rolled: $dice") // Rolled: (1..6)
}

Why is until = 7? Because the upper bound is not included, so this gives us 1..6.

And now the incorrect variant that beginners very often write (and sometimes not only beginners, but then they pretend “it was intended”):

import kotlin.random.Random

fun main() {
    val dice = Random.nextInt(from = 1, until = 6)
    println("Rolled: $dice") // Rolled: (1..5) — 6 will NEVER happen
}

This is not “6 comes up rarely.” It’s “never.” This kind of bug can live in a project for weeks, because it feels plausible: “well, yeah, 6 just hasn’t come up yet.” So a useful habit is: if you want an “inclusive” range, say the formula in your head: until = max + 1.

4. Double randomness

Random.nextDouble(): a random number from 0 to 1

If nextInt() is a “random integer,” then nextDouble() is most often thought of as a “random fraction.” By default:

Random.nextDouble() returns a floating-point number in the range from 0.0 to 1.0 (1.0, as a rule, is not considered guaranteed to be reachable — but for everyday reasoning, think “almost up to 1.0”).

Practically, it’s convenient as a “probability” or a “coefficient.” For example: “with 20% probability, we give a bonus” or “we randomly tweak the price/weight a little.”

Example: generate a "luck coefficient" as a number from 0.0 to 1.0:

import kotlin.random.Random

fun main() {
    val luck = Random.nextDouble()
    println("Luck = $luck") // 	Luck = 0.483920...
}

Almost always it will be a long fraction. That’s normal: the computer doesn’t have to care about pretty output — that’s our job.

Scaling a random fraction: making “0..1” useful

A 0..1 fraction isn’t always convenient by itself. More often you want a “random number in a range,” for example a discount of 5%..25% or a random price 100.0..150.0.

There are two basic approaches, and both are useful. We’ll start with the most straightforward “mathematical” one: take a random number and stretch it to the desired range.

The formula is: value = min + Random.nextDouble() * (max - min)

Example: a random discount from 5% to 25% (i.e., 0.05..0.25 as the range):

import kotlin.random.Random

fun main() {
    val min = 0.05
    val max = 0.25
    val discount = min + Random.nextDouble() * (max - min)

    println("Discount = $discount") 	// Discount = 0.1372...
}

Yes, it’s a long fraction again. Humans prefer percentages, so let’s add percentage output.

Random + rounding for output: making the result human-friendly

Random values almost always come out “ugly”: 0.1372049182. That’s a great number for a mathematician and a terrible one for a user. So the normal pattern is: generated → scaled → rounded for output.

We won’t dive into new rounding rules, but we’ll show a typical trick: rounding to 1 digit after the decimal point using round.

import kotlin.math.round
import kotlin.random.Random

fun main() {
    val discount = 0.05 + Random.nextDouble() * 0.20 // 0.05..0.25
    val percent = discount * 100.0

    val prettyPercent = round(percent * 10.0) / 10.0
    println("Discount = $prettyPercent%") // Discount = 13.7%
}

And now our “LuckyCafe” starts to look like a program rather than a measurement log from a spaceship.

5. Mini example: “LuckyCafe” — a bonus for an order

Now we’ll assemble a small project that uses both nextInt(...) and nextDouble(), and, most importantly, the key bounds rule. The scenario is: we print a “coffee shop” receipt, and then we randomly determine the bonus of the day: either a discount, or a free syrup, or “sorry, no bonuses today” (a coffee shop must also be able to say “no,” otherwise it’ll go broke).

Randomly choosing a case via nextInt(0, 3)

When you need to choose one of several options, it’s convenient to generate an index number: 0, 1, 2. And here the until rule matters again: if there are 3 options, then until must be 3, because the result will be 0..2.

import kotlin.random.Random

fun main() {
    val bonusCase = Random.nextInt(from = 0, until = 3)

    println("bonusCase = $bonusCase") // bonusCase = 0/1/2
}

Here you can already hang logic on if/else (we can do that). Yes, later there will be when, but for now we’re playing with the dice we already got in previous lessons.

Turning the case number into text via if/else

Now let’s produce our first output:

import kotlin.random.Random

fun main() {
    val bonusCase = Random.nextInt(from = 0, until = 3)

    val message = if (bonusCase == 0) {
        "Bonus: discount on the order"
    } else if (bonusCase == 1) {
        "Bonus: free syrup"
    } else {
        "Bonus: no bonuses today (but you’re still awesome)"
    }

    println(message)
}

Note: we intentionally use 0..2, because it’s more convenient than 1..3. Indices and “counting from zero” are annoying at first, then they start to feel natural.

If a discount is selected — generate it via nextDouble()

Now let’s make the discount not just text, but a number. Suppose the discount is from 5% to 20% (that’s the range 0.05..0.20).

import kotlin.math.round
import kotlin.random.Random

fun main() {
    val bonusCase = Random.nextInt(from = 0, until = 3)

    if (bonusCase == 0) {
        val discount = 0.05 + Random.nextDouble() * 0.15
        val percent = round(discount * 1000.0) / 10.0
        println("Bonus: discount $percent%") // Bonus: discount 12.3%
    } else if (bonusCase == 1) {
        println("Bonus: free syrup")
    } else {
        println("Bonus: no bonuses today")
    }
}

We rounded the percentage to 1 digit after the decimal point. You can also round to an integer — it depends on the task. The key thing here is to see the chain: nextDouble() → scaling → rounding.

Mini-game inside: “roll a die until you get a 6”

Sometimes randomness is used not for bonuses, but for repeated attempts until an event happens. In official Kotlin examples, they often show the idea “roll a die until a six” using do-while and Random.nextInt(1, 7).

We’ll add this as a “promo”: the barista rolls a die, and if it’s 6 — we give an extra bonus.

import kotlin.random.Random

fun main() {
    var roll: Int

    do {
        roll = Random.nextInt(1, 7)
        println("Barista rolled: $roll") // Barista rolled: 2
    } while (roll != 6)

    println("It’s a 6! Adding a cookie to the order!") // It’s a 6! ...
}

6. until cheat sheet

With Random.nextInt(from, until) you almost always pause at the same place: “is the upper bound included?” So it’s helpful to keep a “cheat-sheet formula” in mind and verify any generation against it.

If you need:

  • from a to b inclusive, you write nextInt(a, b + 1).
  • from 0 to (n - 1) (for example, an index), you write nextInt(0, n).

That’s why people often say: “until is like size.” If you have 10 options, until = 10, and you’ll get 0..9.

By the way, the same thinking shows up in other parts of the standard library: many operations rely on the same “upper bound is excluded” principle, because it makes it easier not to go out of bounds.

7. Typical mistakes when working with Random

Mistake #1: mix up the bounds and get “never happens.”
The most common trap is to write Random.nextInt(1, 6) and expect 1..6. But by contract, the upper bound is not included, so you get 1..5. This breaks statistics, game mechanics, and your faith in the fairness of the world. The cure is one habit: if you need “up to b inclusive,” use until = b + 1. In the dice example, it’s easy to see: Random.nextInt(1, 7) yields 1..6.

Mistake #2: expect “pretty” numbers from nextDouble().
Random.nextDouble() almost always produces a long fraction, and that’s normal. The mistake is to print it to the user “as is” and then wonder why the program looks weird. The right style: either convert to percentages, or round to the required number of digits (as we did using round).

Mistake #3: mix “probability” and “percent” without converting.
A very common confusion: you get p = Random.nextDouble(), decide it’s “percent,” and print “discount 0.42%.” In fact, that’s 42% as a fraction (0.42), and for percent you need to multiply by 100. And conversely: if you store percent as 42 and then use it as a probability, your logic falls apart. Make an agreement with yourself: either you store a fraction (0.0..1.0) or percent (0..100), and each time you convert explicitly.

Mistake #4: think Random is “magical true randomness.”
In learning tasks it’s almost never important, but it’s useful to remember: Random produces pseudorandom numbers. This means it’s perfect for games, simulations, and educational examples, but for cryptography and security you need other approaches (but that’s not today’s topic — just keep it in mind so you don’t build a “bank password generator” on nextInt()).

Mistake #5: create a range via a “complicated formula” when there’s a simpler way.
Beginners sometimes write something like a + Random.nextInt() % (b - a) and get negative numbers, a weird distribution, and lots of pain. If you need a range, use nextInt(from, until) and live peacefully: it’s designed specifically so you don’t reinvent the wheel (and then fix the wheel because it has square tires).

Mistake #6: forget that until is “like size,” and break selection from options.
When you choose one option out of three (0, 1, 2), you need nextInt(0, 3). If you write nextInt(0, 2), the third option will never occur. In a small example it’s obvious right away, but in a large app it can show up as “why do users never get bonus #3,” and you’ll spend half a day figuring out that you simply cut off a branch of randomness.

1
Task
Kotlin SELF, level 6, lesson 4
Locked
Dice roll
Dice roll
1
Task
Kotlin SELF, level 6, lesson 4
Locked
Bonus of the Day
Bonus of the Day
1
Task
Kotlin SELF, level 6, lesson 4
Locked
Showcase discount
Showcase discount
1
Task
Kotlin SELF, level 6, lesson 4
Locked
Guess the Number
Guess the Number
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION