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 включно» | |
|
| «від 0 до 9 включно» | |
|
| «від 10 до 20 включно» | |
|
Ось чому помилка «на 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». У навчальному проєкті краще вибрати один стиль і бути послідовним.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ