1. Основний цикл і чому він важливий
Коли ви пишете гру, майже завжди є фрагмент коду, який відповідає за «серцебиття» програми: повторюваний крок «запитали → обробили → відповіли → перейшли далі». У нашій грі цим кроком є спроби гравця. Якщо цикл написано акуратно, гра відчувається чесною й передбачуваною. Якщо ж цикл написано недбало — ви отримуєте «безсмертного» гравця, нескінченні спроби, перемогу «за 8 спроб із 7» та інші сюжетні повороти, яких не було в сценарії.
Якщо дивитися на структуру програми, порядок зазвичай такий: спочатку задаємо налаштування, потім генеруємо секрет, далі запускаємо цикл, а після нього друкуємо підсумок. Завдяки цьому main() читається як сценарій.
Міні-нарис (поки без деталей):
fun main() {
// 1) налаштування
// 2) секрет
// 3) цикл спроб
// 4) підсумок
}
Психологічно корисно памʼятати: цикл — це місце, де найчастіше народжуються помилки «на 1». Тому сьогодні ми будемо багато говорити не лише про те, «як написати», а й про те, «де поставити attempt++, щоб потім не плакати».
2. Лічильник спроб: attempt = 1 і умова attempt <= maxAttempts
Цикл спроб можна написати по-різному, але для новачка найпрозоріший варіант — явний лічильник attempt. Ми починаємо з attempt = 1, бо людям звичніше рахувати «перша спроба», а не «нульова». Далі повторюємо кроки, доки attempt <= maxAttempts. Цю умову легко читати, і зазвичай вона збігається з тим, як сформульовано правила.
У Kotlin цикл while працює так: спочатку перевіряємо умову, потім виконуємо тіло, а далі знову перевіряємо умову. Це базова поведінка while — рівно те, що нам потрібно для логіки «поки спроби ще є — граємо».
Міні-приклад «скелета циклу» без гри:
fun main() {
val maxAttempts = 3
var attempt = 1
while (attempt <= maxAttempts) {
println("Спроба $attempt із $maxAttempts")
attempt++
}
// Спроба 1 із 3
// Спроба 2 із 3
// Спроба 3 із 3
}
Зверніть увагу: attempt++ стоїть наприкінці ітерації. На «сухому» прикладі це виглядає природно, але в реальній грі зʼявляться гілки continue і break. І саме тоді місце інкремента стане критичним.
Таблиця «<= чи <»
Іноді новачок пише attempt < maxAttempts — і несподівано отримує на одну спробу менше. Щоб не гадати, зручно тримати в голові просту відповідність:
| Що хочемо сказати словами | Звична математика | Умова циклу за attempt = 1 |
|---|---|---|
| «рівно N спроб» | |
|
| «доки спроба менша за N» | |
|
3. Стійке введення: Int?, null і правило про спроби
Користувацьке введення — як погода: можна сподіватися на сонце, але краще мати парасольку. У нашій грі «парасолька» — це toIntOrNull() і контракт функції readGuessOrNull(min, max): Int?. Тобто ми не падаємо з винятком, а отримуємо null, якщо введено не число або число поза діапазоном. Це ми зробили в минулій лекції, а сьогодні важливо правильно вбудувати цю функцію в цикл.
І тут зʼявляється питання дизайну гри: некоректне введення вважається спробою чи ні? Це не вибір «правильно/неправильно», а домовленість. Головне — визначитися й виразити правило в коді так, щоб його було складно випадково зламати.
Варіант A: некоректне введення НЕ витрачає спробу
У цьому варіанті, якщо ви ввели "abc" або 9999, ми друкуємо «Некоректне введення» і просимо спробувати ще раз, не збільшуючи attempt.
Ключовий трюк тут — continue. Він перериває поточну ітерацію циклу й переходить до наступної перевірки умови. Kotlin підтримує continue і break як стандартні оператори керування циклом.
Приклад (фрагмент циклу):
val guess = readGuessOrNull(min, max)
if (guess == null) {
println("Некоректне введення. Спробуйте ще раз.")
continue
}
Зауважте «магію» (просту й чесну, без темних обрядів): після continue код нижче не виконується. Отже, ми ніколи не порівнюємо null із секретом і не друкуємо «занадто багато» для неіснуючого числа.
Варіант B: некоректне введення витрачає спробу
Іноді за правилами хочеться строгості: увів нісенітницю — спроба згоріла. Тоді в гілці guess == null ми робимо attempt++ — і лише потім continue.
Міні-приклад:
val guess = readGuessOrNull(min, max)
if (guess == null) {
println("Некоректне введення. Спроба втрачена.")
attempt++ // важливо: спроба згорає саме тут
continue
}
Дуже поширена логічна помилка — забути attempt++ у цьому варіанті й отримати нескінченний цикл на некоректному введенні. Тоді гра перетворюється на «вічну»: можна вводити «кіт» без кінця, і спроби ніколи не закінчаться. Іноді це навіть кумедно, але зазвичай це баг.
4. Порівняння здогадки й секрету: підказка, перемога і break
Після того як ми отримали коректну здогадку guess: Int, починається найприємніше: порівнюємо її із секретом і друкуємо підказку. У минулій лекції ми вже заготовили функції:
- compareGuess(guess, secret): Int, повертає -1, 0 або 1;
- hintText(result): String, повертає «Занадто мало/багато/Вгадав!».
Сьогодні важливо вбудувати це так, щоб:
1) підказка друкувалася завжди для коректного введення;
2) перемога негайно завершувала цикл;
3) лічильник спроб збільшувався лише тоді, коли це потрібно.
Міні-фрагмент:
val result = compareGuess(guess, secret)
println(hintText(result))
if (result == 0) {
isWon = true
break
}
break миттєво виходить із циклу. Kotlin гарантує таку поведінку для break/continue у циклах.
Тут є тонкість, яку ви швидко відчуєте на практиці: якщо спочатку зробити attempt++, а вже потім перевірити перемогу, то в підсумковому повідомленні перемога може зʼявитися «на спробу пізніше». Тому перемогу зазвичай фіксують до збільшення лічильника.
5. Де ставити attempt++: головне джерело багів «на 1»
Лічильник спроб — маленька змінна, але з великим характером. Вона раз у раз намагається «поїхати» не туди: збільшитися зайвий раз, не збільшитися в одній із гілок або збільшитися до перевірки перемоги. Тому зараз ми спеціально розберемо «правильну геометрію» attempt++ для найпопулярнішого варіанта: некоректне введення не витрачає спробу.
Ідея проста: спроба вважається витраченою тоді, коли введення було коректним, ми порівняли його із секретом — і це не перемога. Отже, attempt++ логічно поставити наприкінці вдалої (але не переможної) ітерації.
Міні-приклад «правильного місця»:
// guess != null, порівняли, не вгадав
attempt++
А ось приклад «майже правильно, але потім буде боляче»:
attempt++ // збільшили заздалегідь
val guess = readGuessOrNull(min, max)
Чому боляче? Бо якщо введення виявилося некоректним і ми зробили continue, спроба вже збільшилася, хоча за обраною політикою вона не мала збільшуватися. Тобто ви випадково перемкнулися на правило «некоректне введення витрачає спробу» — але без явного наміру.
6. Підсумок після циклу: перемога чи поразка без плутанини
Після циклу в новачків часто виникає спокуса: «ну якщо цикл закінчився, значить програв». Але цикл може завершитися й через перемогу (через break). Тому нам потрібна явна ознака перемоги, наприклад var isWon = false.
Підсумковий блок має бути простим: якщо переміг — вітаємо; інакше — показуємо секрет. І тут важливо не переплутати, яка спроба вважається «кількістю спроб». Якщо attempt збільшується лише після невдалих спроб, то у разі перемоги attempt якраз указує на номер переможної спроби — і це зручно.
Приклад підсумкового виводу:
if (isWon) {
println("Перемога! Ви вгадали за $attempt спроб(и).")
} else {
println("Спроби закінчилися. Загадане число: $secret")
}
Якщо у вашій версії attempt збільшується в іншому місці (наприклад, на початку циклу), підсумок теж доведеться перебудувати. Це нормальна частина роботи програміста: код — це договір. Якщо ви змінюєте один пункт договору, змінюються й наслідки.
7. Збирання в єдиний main()
Зараз зберемо все в одну програму. Я навмисно триматиму фрагменти коду невеликими й «склеюваними», щоб ви могли переносити їх крок за кроком, а не копіювати величезний аркуш.
Налаштування і старт гри
Цей блок зазвичай іде першим:
import kotlin.random.Random
fun main() {
val min = 1
val max = 100
val maxAttempts = 7
val secret = generateSecret(min, max)
var attempt = 1
var isWon = false
println("Я загадав число від $min до $max. У вас $maxAttempts спроб(и).")
}
Тут generateSecret(min, max) — функція з попередньої лекції (з require(min <= max) і Random.nextInt(from, until)).
Основний цикл
Це центральний фрагмент. Зверніть увагу на порядок: статус спроби → введення → обробка null → порівняння → підказка → перемога → збільшення лічильника спроб.
while (attempt <= maxAttempts) {
println("Спроба $attempt із $maxAttempts")
val guess = readGuessOrNull(min, max)
if (guess == null) {
println("Некоректне введення. Введіть ціле число в діапазоні.")
continue
}
val result = compareGuess(guess, secret)
println(hintText(result))
if (result == 0) {
isWon = true
break
}
attempt++
}
У цьому коді continue робить структуру рівною: якщо введення некоректне, ми не «провалюємося» далі й не розмножуємо вкладені if. Таке застосування continue — один із небагатьох випадків, коли він справді покращує читабельність.
Підсумок гри
Після циклу — лише підсумок:
if (isWon) {
println("Перемога! Ви вгадали число за $attempt спроб(и).")
} else {
println("Ви програли. Загадане число: $secret")
}
Якщо некоректне введення має витрачати спробу
Іноді корисно побачити, що «політика» — це не філософія, а буквально два рядки коду. Уся гра лишається тією самою, змінюється лише гілка guess == null: тепер ми збільшуємо лічильник спроб і продовжуємо.
Ось фрагмент, який відрізняється:
val guess = readGuessOrNull(min, max)
if (guess == null) {
println("Некоректне введення. Спроба втрачена.")
attempt++
continue
}
Важлива думка: не потрібно переписувати весь цикл, якщо ви змінюєте правило. Добре, коли правило локалізовано в одному місці — це і є «підтримуваність», тільки без гучних слів.
Схема потоку виконання
Коли ви вчитеся програмувати, мозок часто робить так: читає код і намагається уявити всі переходи «як фільм». Це працює на 5 рядках, але починає буксувати на 30. Тому корисно один раз намалювати схему, а далі звірятися з нею, коли щось не працює.
flowchart TD
A[Початок гри: налаштування, secret, attempt=1] --> B{attempt <= maxAttempts?}
B -- ні --> Z[Друкуємо підсумок: перемога/поразка]
B -- так --> C["Друкуємо 'Спроба attempt із maxAttempts'"]
C --> D["readGuessOrNull(min, max)"]
D --> E{guess == null?}
E -- так --> F[Повідомлення про помилку введення]
F --> B
E -- ні --> G["compareGuess(guess, secret)"]
G --> H["Друкуємо hintText(result)"]
H --> I{result == 0?}
I -- так --> J[isWon = true; break]
J --> Z
I -- ні --> K[attempt++]
K --> B
Якщо ви змінюєте політику «некоректне введення витрачає спробу», змінюється лише блок F: там зʼявляється attempt++ перед поверненням до перевірки умови.
8. Типові помилки в основному циклі спроб
Помилка № 1: attempt++ стоїть не там, де «витрачається спроба».
Найчастіший баг виглядає так: ви збільшили attempt на початку ітерації, потім отримали некоректне введення й зробили continue. У результаті спроба «згоріла», хоча за вашими правилами вона не мала згорати. А буває й навпаки: ви хотіли, щоб некоректне введення витрачало спробу, але забули збільшити attempt у цій гілці — і отримали нескінченний цикл на «поганих» рядках.
Помилка № 2: перемогу перевіряють після інкремента — і в підсумку спроба виходить на 1 більшою.
Якщо спочатку зробити attempt++, а потім виявити перемогу, підсумок «Перемога за 4 спроби» легко перетворюється на «за 5», хоча виграли ви на 4-й. Лікується просто: фіксуйте перемогу до збільшення лічильника й виходьте з циклу одразу через break.
Помилка № 3: забули обробку null, і логіка порівняння намагається працювати з неіснуючим числом.
Іноді це проявляється навіть не як падіння, а як дивна «архітектура»: студент починає вигадувати чарівне значення на кшталт -999, щоб позначити помилку введення. Набагато чистіше використовувати Int? і явну перевірку guess == null, а потім continue, щоб решта коду працювала лише з коректними даними.
Помилка № 4: цикл закінчується, а програма завжди друкує «поразка», навіть якщо був break.
Це стається, коли підсумковий вивід завʼязаний лише на факт виходу з циклу, а не на причину виходу. Якщо ви виходите через перемогу за допомогою break, цикл теж завершується. Тому потрібен прапорець isWon (або інший явний стан), за яким ви друкуєте підсумок.
Помилка № 5: неузгоджені межі діапазону.
Візуально це виглядає як «гра мене ненавидить»: ви вводите max, а вам кажуть «поза діапазоном». Або ви не можете вгадати число, бо секрет генерується, наприклад, 1..99, хоча ви думаєте, що це 1..100. Рішення просте: min/max мають бути єдиними налаштуваннями, і генерація секрету, і перевірка введення мають спиратися на них.
Помилка № 6: continue використовують так, що код стає важче читати.
continue хороший, коли він допомагає «раніше вийти» з ітерації через помилкове введення й тримає основний шлях рівним. Але якщо continue зʼявляється в пʼяти місцях, читати стає важко: погляд постійно «стрибає». У нашій грі одного continue в гілці помилки введення зазвичай достатньо.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ