Как се мисли игра
Проблем
Откъде започва една игра?
Искаш да напишеш игра. Например надбягване със зар: фигурка тръгва от поле 0, хвърляш зар и тя върви напред, докато стигне поле 20.
Отваряш редактора и... какво пишеш най-напред? Менюто? Зара? Дъската? Ако започнеш от всичко наведнъж, след час имаш много код, който още не се пуска.
Помага да зададеш на играта пет въпроса, преди да пишеш код.
Идеята
Пет въпроса за всяка игра
- Състояние. Кои числа описват света сега? При надбягването е полето на фигурката и номерът на хода. При шаха е къде стои всяка фигура и кой е на ход.
- Правила. Какво е позволено и какво става след него? „Фигурката върви толкова полета, колкото е зарът.“
- Вход. Какво идва отвън? Изборът на играча или числото на зара.
- Цикъл. Ход след ход: вход, обновяване на състоянието, показване. После пак.
- Край. Кога играта свършва и кой печели? „Фигурката е на поле 20.“
Отговорите са почти готовият код: състоянието са променливи, правилата са функции, входът е ask() или генератор на случайни числа, цикълът е while или for, а краят е условието на цикъла.
Виж го
Ход след ход
const random = makeRandom(42);let field = 0;for (let turn = 1; field < 20; turn++) { const roll = Math.floor(random() * 6) + 1; field = Math.min(field + roll, 20); console.log(`Ход ${turn}: зар ${roll}, поле ${field}`);}console.log("Финал! Фигурката стигна поле 20.");Стъпка по стъпка, с думи
- Генератор със семе 42 и фигурка на поле 0.
- Ход 1: вход е зарът 2, състоянието става поле 2, екранът показва реда.
- Ход 2: вход е зарът 6, състоянието става поле 8, екранът показва реда.
- Ход 3: вход е зарът 1, състоянието става поле 9, екранът показва реда.
- Ход 4: вход е зарът 5, състоянието става поле 14, екранът показва реда.
- Ход 5: вход е зарът 1, състоянието става поле 15, екранът показва реда.
- Ход 6: вход е зарът 4, състоянието става поле 19, екранът показва реда.
- Ход 7: вход е зарът 1, състоянието става поле 20, екранът показва реда. Поле 20: условието field < 20 вече е невярно.
- Цикълът свърши, защото полето стигна 20. Това е краят на играта.
Цялата игра е един цикъл. На всеки ход идва вход (зарът), състоянието се обновява (новото поле) и се показва (един ред на екрана). Условието на цикъла е краят: докато полето е под 20, има следващ ход.
Math.min пази фигурката на дъската: ако зарът я изпрати след 20, тя спира на 20.
По-точно, за любопитните
- Тази игра е ходова: нищо не се случва, докато не дойде следващият вход. Така са шахът, картите и повечето игри в този раздел.
- Игрите в реално време (стрелба, състезания с коли) също имат цикъл, но той не чака играча. Той се върти 60 пъти в секунда: прочита клавишите, които са натиснати в момента, мести всичко според изминалото време и рисува кадъра. Ако играчът не натисне нищо, светът пак се движи.
Идеята
Същото семе, същата игра
Ако хвърлянията са истински случайни, всяко пускане е различна игра. Как тогава ще провериш, че играта работи? Не знаеш какво да очакваш.
Math.random() не става: на нея не може да се даде начално число. Затова в тези уроци генераторът е малка функция makeRandom(seed), която получава начално число, семе. Тя връща функция, която при всяко извикване дава следващото число от 0 до 1 (без 1), точно като Math.random(). Едно и също семе дава една и съща поредица, всеки път.
makeRandom засега копирай, както ask(). Вътре са побитови операции, които няма да ни трябват. Зарът е Math.floor(random() * 6) + 1: число от 0 до 6 (без 6), отрязано до цяло, плюс 1.
Random first = new Random(42);Random second = new Random(42);for (int i = 0; i < 5; i++) { int a = first.nextInt(6) + 1; int b = second.nextInt(6) + 1; System.out.println(a + " и " + b);}3 и 34 и 41 и 13 и 31 и 1Random first = new Random(42);Random second = new Random(42);for (int i = 0; i < 5; i++){ int a = first.Next(1, 7); int b = second.Next(1, 7); Console.WriteLine(a + " и " + b);}5 и 51 и 11 и 14 и 42 и 2from random import Random
first = Random(42)second = Random(42)for i in range(5): a = first.randint(1, 6) b = second.randint(1, 6) print(f"{a} и {b}")6 и 61 и 11 и 16 и 63 и 3function makeRandom(seed) { return () => { seed = (seed + 0x9e3779b9) | 0; let z = seed; z = Math.imul(z ^ (z >>> 16), 0x85ebca6b); z = Math.imul(z ^ (z >>> 13), 0xc2b2ae35); return ((z ^ (z >>> 16)) >>> 0) / 4294967296; };}
const first = makeRandom(42);const second = makeRandom(42);for (let i = 0; i < 5; i++) { const a = Math.floor(first() * 6) + 1; const b = Math.floor(second() * 6) + 1; console.log(`${a} и ${b}`);}2 и 26 и 61 и 15 и 51 и 1Двата генератора с 42 дават едни и същи числа. Пусни програмата пак и ще са същите. Затова примерите и задачите тук имат точен изход, който може да се провери.
Виж изхода и в другите табове. Всеки език има свой генератор: от едно и също семе те дават различни числа. Затова примерите със случайни числа имат изход за всеки език.
По-точно, за любопитните
- Когато играта е готова за истински играчи, сменяш генератора:
const random = Math.random;. Останалият код не се променя, защото и двете се викат по същия начин,random(). Тогава всяка игра е различна. makeRandomне е от стандартната библиотека. В истински проект генератор със семе идва от пакет, напримерseedrandom.
- Честа практика е семето да се отпечата в началото на играта. Ако играч намери бъг, с неговото семе можеш да изиграеш точно същата игра.
Идеята
Първо най-малката работеща версия
Програмата от сцената е цяла игра в десетина реда. Тя няма двама играчи, нито меню, нито правила освен „върви напред“. Но върви от началото до края и показва, че цикълът и краят са верни.
Оттам играта расте с по едно правило. Второто правило: фигурката трябва да стигне поле 20 точно. Ако зарът я изпрати по-далеч, тя отскача назад с толкова полета, колкото е прехвърлила. От 17 със зар 5 тя стига 20 и се връща на 18.
Ново правило се добавя, само когато предишното работи. Ако нещо се счупи, знаеш, че е в последното, което си писал.
Идеята
Логиката връща, екранът печата
Правилото „точно 20“ е във функция, която връща новото поле. Краят е функция, която връща true или false:
static int move(int field, int roll) { int next = field + roll; if (next > 20) { return 20 - (next - 20); } return next;}
static boolean isFinished(int field) { return field == 20;}
Random random = new Random(7);int field = 0;int turn = 0;while (!isFinished(field)) { turn++; int roll = random.nextInt(6) + 1; field = move(field, roll); System.out.println("Ход " + turn + ": зар " + roll + ", поле " + field);}System.out.println("Точно на 20 след " + turn + " хода");Ход 1: зар 5, поле 5Ход 2: зар 3, поле 8Ход 3: зар 4, поле 12Ход 4: зар 5, поле 17Ход 5: зар 5, поле 18Ход 6: зар 5, поле 17Ход 7: зар 5, поле 18Ход 8: зар 6, поле 16Ход 9: зар 1, поле 17Ход 10: зар 1, поле 18Ход 11: зар 1, поле 19Ход 12: зар 1, поле 20Точно на 20 след 12 ходаstatic int Move(int field, int roll){ int next = field + roll; if (next > 20) { return 20 - (next - 20); } return next;}
static bool IsFinished(int field){ return field == 20;}
Random random = new Random(7);int field = 0;int turn = 0;while (!IsFinished(field)){ turn++; int roll = random.Next(1, 7); field = Move(field, roll); Console.WriteLine("Ход " + turn + ": зар " + roll + ", поле " + field);}Console.WriteLine("Точно на 20 след " + turn + " хода");Ход 1: зар 3, поле 3Ход 2: зар 6, поле 9Ход 3: зар 4, поле 13Ход 4: зар 1, поле 14Ход 5: зар 3, поле 17Ход 6: зар 5, поле 18Ход 7: зар 1, поле 19Ход 8: зар 6, поле 15Ход 9: зар 6, поле 19Ход 10: зар 6, поле 15Ход 11: зар 3, поле 18Ход 12: зар 6, поле 16Ход 13: зар 3, поле 19Ход 14: зар 1, поле 20Точно на 20 след 14 ходаdef move(field, roll): new_field = field + roll if new_field > 20: return 20 - (new_field - 20) return new_field
def is_finished(field): return field == 20
random = Random(7)field = 0turn = 0while not is_finished(field): turn += 1 roll = random.randint(1, 6) field = move(field, roll) print(f"Ход {turn}: зар {roll}, поле {field}")print(f"Точно на 20 след {turn} хода")Ход 1: зар 3, поле 3Ход 2: зар 2, поле 5Ход 3: зар 4, поле 9Ход 4: зар 6, поле 15Ход 5: зар 1, поле 16Ход 6: зар 1, поле 17Ход 7: зар 5, поле 18Ход 8: зар 1, поле 19Ход 9: зар 3, поле 18Ход 10: зар 5, поле 17Ход 11: зар 1, поле 18Ход 12: зар 5, поле 17Ход 13: зар 2, поле 19Ход 14: зар 1, поле 20Точно на 20 след 14 ходаfunction move(field, roll) { const next = field + roll; if (next > 20) { return 20 - (next - 20); } return next;}
function isFinished(field) { return field === 20;}
const random = makeRandom(7);let field = 0;let turn = 0;while (!isFinished(field)) { turn++; const roll = Math.floor(random() * 6) + 1; field = move(field, roll); console.log(`Ход ${turn}: зар ${roll}, поле ${field}`);}console.log(`Точно на 20 след ${turn} хода`);Ход 1: зар 1, поле 1Ход 2: зар 3, поле 4Ход 3: зар 6, поле 10Ход 4: зар 4, поле 14Ход 5: зар 3, поле 17Ход 6: зар 2, поле 19Ход 7: зар 5, поле 16Ход 8: зар 2, поле 18Ход 9: зар 2, поле 20Точно на 20 след 9 ходаНито move, нито isFinished печата нещо. Те само смятат. Печатането е в кода под функциите.
Защо така? Функция, която връща число, можеш да извикаш с каквито стойности искаш и да провериш резултата: move(17, 5) трябва да е 18. Функция, която печата, проверяваш само с очи. А ако утре играта мине от конзолата в прозорец или в браузъра, правилата остават същите, сменя се само показването.
Пробвай
Къде ще стъпи фигурката?
Какво ще отпечата програмата?
function move(field, roll) { const next = field + roll; if (next > 20) { return 20 - (next - 20); } return next;}
function isFinished(field) { return field === 20;}
console.log(move(10, 4));console.log(move(17, 3));console.log(move(18, 5));10 + 4 = 14. 17 + 3 = 20, точно. 18 + 5 = 23 е с 3 след 20, затова фигурката отскача на 20 − 3 = 17.
Пробвай
Намери грешката
Фигурката трябва да върви напред, а програмата печата поле 0 на всеки ход:
Ход 1: зар 1, поле 0Ход 2: зар 3, поле 0Ход 3: зар 6, поле 0На кой ред е грешката?
Избери реда с грешката.
move връща новото поле, но резултатът се изхвърля. Функцията не може да промени field на викащия: тя получава само стойността 0, а своя field няма. Трябва field = move(field, roll);. Ако такъв ред стои в while (!isFinished(field)), играта никога не свършва.Пробвай
Кога свършва играта?
Кое липсва, за да е краят точно на поле 20?
function isFinished(field) {
return field 20;
}=== сравнява. == също сравнява, но преди това може да превърне едната стойност в друг тип, затова в JavaScript се пише ===. >= също ще работи, защото move никога не връща повече от 20. Внимавай с едно =: return field = 20; не е грешка, а присвояване. Функцията връща 20, а 20 в if се брои за истина, и играта „свършва“ още на първия ход.Упражнение
Три малки игри
Реши задачите в редактора си. Логиката е в функции, които връщат стойност, а печатането е в кода под тях.
1. Надбягване за двама. Ана и Борис хвърлят по ред, с makeRandom(3). Важи правилото „точно 20“ с move от урока. Полетата им са в масив [0, 0]. Който стигне 20 пръв, печели.
Ана: зар 6, поле 6Борис: зар 2, поле 2Ана: зар 2, поле 8Борис: зар 3, поле 5Ана: зар 3, поле 11Борис: зар 1, поле 6Ана: зар 1, поле 12Борис: зар 2, поле 8Ана: зар 3, поле 15Борис: зар 1, поле 9Ана: зар 2, поле 17Борис: зар 4, поле 13Ана: зар 4, поле 19Борис: зар 6, поле 19Ана: зар 1, поле 20Печели Ана2. Познай числото. Програмата избира тайно число от 1 до 100 с makeRandom(2026). Играчът пише догадки, по една на ред. След всяка програмата казва „По-голямо е“, „По-малко е“ или „Позна“. Накрая казва от колко опита. Подсказката е функция hint(guess, secret), която връща текста.
5075888184868750: По-голямо е75: По-голямо е88: По-малко е81: По-голямо е84: По-голямо е86: По-голямо е87: ПознаПозна от 7 опитаВходът е различен за всеки език, защото генераторите им избират различно тайно число от едно и също семе.
3. Кой спечели морския шах? Първият ред е броят на дъските. Всяка дъска е три реда по три знака: X, O или . за празно поле. Напиши функция winner(board), която връща X, O или ., ако няма три еднакви в ред, колона или диагонал.
Дъската може да е масив от трите прочетени реда: всеки ред е текст, а board[r][c] е знакът на ред r, колона c. Отделен тип за един знак, като char в Java, JavaScript няма: знакът е текст с дължина 1.
3XOXOXOOOXXXOOOOX.XXOXXOOOXXПечели XПечели OНяма победителОще задачи
- Капани. Поле 13 връща фигурката на поле 5. Добави правилото в
moveи пусни надбягването пак. - Отпечатай семето. Направи семето променлива, отпечатай го в началото и пусни играта с три различни семена.
- Морски шах за двама. Играчите пишат ред и колона на хода си. След всеки ход покажи дъската и провери с
winnerдали играта е свършила.
Тест