JavaRush /Курсы /Kotlin SELF /Генерация секрета через Random: границы диапазона и ошибк...

Генерация секрета через Random: границы диапазона и ошибка «на 1»

Kotlin SELF
15 уровень , 1 лекция
Открыта

1. Зачем вообще нужен Random, если можно «просто загадать 42»

Если честно, «загадать 42» — отличный план… пока вы не попробуете сыграть второй раз. В мини‑проекте важно, чтобы игра была честной и каждый запуск действительно создавал новый секрет, иначе пользователь очень быстро поймёт закономерность (особенно если он программист, а программисты закономерности чувствуют кожей).

В Kotlin для случайных чисел есть стандартный механизм kotlin.random.Random. Мы будем использовать его, чтобы сгенерировать секретное число в заданном диапазоне. И тут начинается самое интересное: диапазон мы обычно формулируем «по‑человечески» как 1..100 (включая 100), а генератор часто работает «по‑компьютерному» — с верхней границей, которая не включается. Из-за этого появляется классическая ошибка «на 1», когда число 100 никогда не выпадает — и игра становится чуть-чуть нечестной, а значит, чуть-чуть вредной для вашей репутации.

Мини‑набросок (пока без деталей границ):

import kotlin.random.Random

fun main() {
    val min = 1
    val max = 100

    val secret = Random.nextInt(from = min, until = max) // пока подозрительно!
    println("Секрет сгенерирован") // Секрет сгенерирован
}

Это компилируется и работает… но max тут может никогда не появиться. В следующем разделе разберёмся почему.

2. from и until: почему верхняя граница не включается

Случайные числа — один из тех моментов, где язык и библиотека стараются быть максимально точными в формулировках. В Kotlin у Random.nextInt(from, until) контракт такой: число генерируется от from включительно до until не включительно. То есть from входит в возможные значения, а until — нет.

На человеческом языке это означает: «дай мне любое число из полуинтервала [from, until)». А на игровом языке означает: «если я хочу диапазон 1..100, то until должен быть 101».

Давайте сделаем маленькую таблицу‑переводчик, потому что мозг новичка (и мозг опытного разработчика в пятницу вечером) одинаково любит ошибаться в границах.

Как мы говорим Как мы записываем в Kotlin-диапазоне Что нужно для Random.nextInt(from, until)
«от 1 до 100 включительно»
1..100
from = 1, until = 101
«от 0 до 9 включительно»
0..9
from = 0, until = 10
«от 10 до 20 включительно»
10..20
from = 10, until = 21

Вот почему «ошибка на 1» так популярна: у человека в голове часто живёт «оба конца включены», а у функции контракт другой. И функция не виновата: она честно написала в документации, что until не включается.

Чтобы это закрепить, посмотрим на иллюстрацию:

Хотим: 1..5 (включая 5)

1   2   3   4   5
^               ^
from            max (включительно)

А Random.nextInt(from, until) работает так:

from=1, until=6

1   2   3   4   5   6
^                   ^
включается          НЕ включается

Именно поэтому мы почти всегда пишем until = max + 1, когда по правилам игры max должен быть доступен.

3. Правильная генерация секрета для диапазона min..max

Теперь соберём корректную формулу. У нас есть настройки игры:

  • min — минимальное возможное число секрета,
  • max — максимальное возможное число секрета,
  • и договорённость: оба значения входят в диапазон.

Тогда генерация секрета должна быть такой:

import kotlin.random.Random

fun main() {
    val min = 1
    val max = 100

    val secret = Random.nextInt(from = min, until = max + 1)
    println("Секрет сгенерирован") // Секрет сгенерирован
}

Здесь max + 1 — это не «магия», а перевод с «включительно» на «until (не включительно)». Именно так мы заставляем генератор иметь шанс выдать 100.

Ещё один важный момент: Random.nextInt(from, until) бросит IllegalArgumentException, если from >= until. То есть если вы перепутали границы или сделали диапазон пустым — программа упадёт (и это нормально: это ошибка программиста, а не «неудача судьбы»).

Поэтому мы потихоньку подходим к мысли: генерацию секрета лучше прятать в отдельную функцию, где можно централизованно поставить require и не размазывать проверки по всему main.

4. Функция generateSecret(min, max): меньше дублей, больше гарантий

Когда программа растёт, любой повтор формулы — это потенциальная ошибка. Сегодня вы написали max + 1, завтра в другом месте забыли + 1, а послезавтра три часа «отлаживаете Random», хотя Random вообще ни при чём. Поэтому хорошая привычка — вынести генерацию в функцию.

Важно: мы пока не строим архитектуру и не придумываем сложные типы. Просто делаем маленькую функцию с ясным контрактом: «дай мне число в диапазоне min..max включительно».

import kotlin.random.Random

fun generateSecret(min: Int, max: Int): Int {
    require(min <= max) { "Некорректный диапазон: min=$min, max=$max" }
    return Random.nextInt(from = min, until = max + 1)
}

В этом коде есть две очень «взрослые» идеи.

Первая идея — предусловие через require. Если кто-то передал min=100, max=1, то мы не пытаемся «как-нибудь догадаться», что автор имел в виду, а честно останавливаем программу сообщением. Это стиль fail‑fast: лучше упасть сразу и понятно, чем играть в «угадай, почему всегда проигрыш».

Вторая идея — мы прячем правило until = max + 1 в одном месте. Теперь в main будет читабельный сценарий: «задали настройки → сгенерировали секрет». А «почему плюс один» остаётся внутри функции и не загрязняет основной код.

Пример использования:

fun main() {
    val min = 1
    val max = 100

    val secret = generateSecret(min, max)
    println("Игра началась!") // Игра началась!
}

Заметьте, мы не печатаем secret. Если распечатать секрет, игра превращается в «Угадай число (но мы тебе подмигнули)». Отладочный вывод мы ещё будем использовать позже, но аккуратно и временно.

5. Защита от ошибки «на 1»: быстрая проверка границ

Ошибки «на 1» неприятны тем, что программа выглядит рабочей. Она не падает, она честно принимает ввод, она пишет подсказки… просто одно значение никогда не может выпасть. Пользователь это не всегда заметит, а вы можете месяц жить с «чуть нечестной» игрой.

Нам нужно научиться делать быструю самопроверку: «а мой генератор реально выдаёт значения на границах?». Полноценные тесты мы пока не пишем (это отдельная тема), но простую проверку в main сделать можем: несколько раз сгенерировать числа и посмотреть, встречаются ли min и max.

Сделаем минимальный диагностический фрагмент. Он не доказывает математически, что всё идеально, но отлично ловит типичную ошибку «забыл + 1».

fun main() {
    val min = 1
    val max = 3

    repeat(10) {
        val secret = generateSecret(min, max)
        println(secret) // например: 2, 1, 3, 2, ...
    }
}

Почему я взял max = 3, а не 100? Потому что чем меньше диапазон, тем быстрее вы увидите все варианты, включая верхнюю границу. Если у вас ошибка и вы используете until = max, то при min=1, max=3 вы будете видеть только 1 и 2 — и очень быстро поймёте, что 3 «пропала».

Если хочется ещё более «логической» проверки, можно на время добавить флаг и печатать, встречался ли максимум. Мы пока без коллекций, поэтому делаем по‑простому — через булевы переменные:

fun main() {
    val min = 1
    val max = 3

    var seenMax = false

    repeat(20) {
        val secret = generateSecret(min, max)
        if (secret == max) seenMax = true
    }

    println("Max выпадал: $seenMax") // Max выпадал: true (скорее всего)
}

Опять же: это не научная статья про вероятности, а практический способ поймать «сдвиг границы» за 10 секунд.

6. Полезные нюансы: диапазоны и seed

Random.nextInt(range) — проще читается, но важно не смешивать стили

Иногда хочется написать прямо «по-человечески»: «дай мне число из min..max». В Kotlin есть перегрузка, которая принимает диапазон (например, 1..100) и генерирует число включительно по границам диапазона.

Это выглядит очень красиво:

import kotlin.random.Random

fun main() {
    val min = 1
    val max = 100

    val secret = Random.nextInt(min..max)
    println("Секрет сгенерирован") // Секрет сгенерирован
}

Почему же мы тогда вообще возимся с from/until?

Потому что в проекте важна дисциплина договорённостей. Сегодня вы генерируете секрет через min..max, завтра в другом месте пишете nextInt(from, until) и случайно забываете +1. Получается два разных «правила границ» в одной программе. А когда в программе два правила, пользователь угадывает число, а разработчик — почему оно не угадывается.

Поэтому для учебного мини‑проекта я рекомендую выбрать один стиль и держаться его. На этом шаге мы фиксируем стиль from/until, потому что он лучше тренирует внимательность к границам (и очень полезен, когда позже появятся массивы и индексы).

Но полезно помнить: Random.nextInt(range) существует, он читабелен, и он честно работает «включительно».

Seed и повторяемая «случайность» для отладки

Случайность — друг игры, но враг воспроизводимости. Иногда вы ловите баг, который проявляется «примерно раз в 50 запусков», и чувствуете себя охотником за призраками: вы точно видели, как оно сломалось, но больше не получается повторить.

Для таких ситуаций существует генератор с seed (зерном). Идея простая: если два генератора созданы с одним и тем же seed, они будут выдавать одну и ту же последовательность случайных чисел (в рамках одной версии Kotlin runtime).

Пример:

import kotlin.random.Random

fun main() {
    val rng = Random(42)
    println(rng.nextInt(1, 5)) // например: 4
    println(rng.nextInt(1, 5)) // например: 1
}

Для нашей игры «Угадай число» мы в обычном режиме seed использовать не будем (иначе «случайность» станет предсказуемой). Но знать про это полезно: в следующих лекциях, когда будем обсуждать сценарии и отладку, seed может стать вашим «режимом повтора», чтобы воспроизводить одно и то же поведение.

7. Типичные ошибки при генерации секрета через Random

Ошибка №1: использовать Random.nextInt(min, max) и думать, что max включён.
Это классика «until-границы»: Random.nextInt(from, until) генерирует число от from включительно до until не включительно. Если вы передали until = max, то максимальное значение никогда не выпадет. Для диапазона min..max включительно нужно передавать until = max + 1.

Ошибка №2: не проверять корректность диапазона и потом удивляться падению.
Если from >= until, стандартная библиотека имеет полное право бросить IllegalArgumentException. Для вас как автора игры это означает: либо вы перепутали min/max, либо неправильно сформировали until. Ставьте require(min <= max) в одном месте (в generateSecret) — и проблема не размажется по проекту.

Ошибка №3: «волшебные числа» вроде 101 вместо max + 1.
Сегодня 101 кажется безобидным, потому что «ну у нас же 1..100». Завтра вы поменяете правила на 1..50 и забудете обновить 101. В итоге игра начнёт загадочно генерировать секреты, которые невозможно угадать по вашим же правилам. Формула должна собираться из параметров: until = max + 1, а не из «воспоминаний о прошлом».

Ошибка №4: случайно спойлерить секрет через println(secret) и забыть убрать.
Парадокс отладки: вывод секрета в консоль очень помогает разработчику, но полностью ломает игру игроку. Если уж печатаете секрет для проверки, делайте это временно и осознанно (например, через флаг debug), а потом выключайте.

Ошибка №5: смешать два подхода к границам (то min..max, то from/until) и получить два разных мира.
Random.nextInt(min..max) работает включительно по границам диапазона. Random.nextInt(from, until) — нет. Если вы используете оба стиля вперемешку, то почти гарантированно где-нибудь ошибётесь «на 1». В учебном проекте лучше выбрать один стиль и быть последовательным.

1
Задача
Kotlin SELF, 15 уровень, 1 лекция
Недоступна
Цифра купона
Цифра купона
1
Задача
Kotlin SELF, 15 уровень, 1 лекция
Недоступна
Пользовательский секрет
Пользовательский секрет
1
Задача
Kotlin SELF, 15 уровень, 1 лекция
Недоступна
Генератор секрета
Генератор секрета
1
Задача
Kotlin SELF, 15 уровень, 1 лекция
Недоступна
Граница генератора
Граница генератора
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ