Как се мисли игра
Проблем
Откъде започва една игра?
Искаш да напишеш игра. Например надбягване със зар: фигурка тръгва от поле 0, хвърляш зар и тя върви напред, докато стигне поле 20.
Отваряш редактора и... какво пишеш най-напред? Менюто? Зара? Дъската? Ако започнеш от всичко наведнъж, след час имаш много код, който още не се пуска.
Помага да зададеш на играта пет въпроса, преди да пишеш код.
Идеята
Пет въпроса за всяка игра
- Състояние. Кои числа описват света сега? При надбягването е полето на фигурката и номерът на хода. При шаха е къде стои всяка фигура и кой е на ход.
- Правила. Какво е позволено и какво става след него? „Фигурката върви толкова полета, колкото е зарът.“
- Вход. Какво идва отвън? Изборът на играча или числото на зара.
- Цикъл. Ход след ход: вход, обновяване на състоянието, показване. После пак.
- Край. Кога играта свършва и кой печели? „Фигурката е на поле 20.“
Отговорите са почти готовият код: състоянието са променливи, правилата са функции, входът е input() или Random, цикълът е while или for, а краят е условието на цикъла.
Виж го
Ход след ход
random = Random(42)field = 0turn = 1while field < 20: roll = random.randint(1, 6) field = min(field + roll, 20) print(f"Ход {turn}: зар {roll}, поле {field}") turn += 1print("Финал! Фигурката стигна поле 20.")Стъпка по стъпка, с думи
- Генератор със семе 42 и фигурка на поле 0. Ходът започва от 1.
- Ход 1: вход е зарът 6, състоянието става поле 6, екранът показва реда.
- Ход 2: вход е зарът 1, състоянието става поле 7, екранът показва реда.
- Ход 3: вход е зарът 1, състоянието става поле 8, екранът показва реда.
- Ход 4: вход е зарът 6, състоянието става поле 14, екранът показва реда.
- Ход 5: вход е зарът 3, състоянието става поле 17, екранът показва реда.
- Ход 6: вход е зарът 2, състоянието става поле 19, екранът показва реда.
- Ход 7: вход е зарът 2, състоянието става поле 20, екранът показва реда. Поле 20: условието while field < 20 вече е невярно.
- Цикълът свърши, защото полето стигна 20. Това е краят на играта.
Цялата игра е един цикъл. На всеки ход идва вход (зарът), състоянието се обновява (новото поле) и се показва (един ред на екрана). Условието на цикъла е краят: докато полето е под 20, има следващ ход.
min пази фигурката на дъската: ако зарът я изпрати след 20, тя спира на 20.
В Python няма for с три части, като в Java. Затова ходът се брои с turn, а цикълът е while: докато полето е под 20.
По-точно, за любопитните
- Тази игра е ходова: нищо не се случва, докато не дойде следващият вход. Така са шахът, картите и повечето игри в този раздел.
- Игрите в реално време (стрелба, състезания с коли) също имат цикъл, но той не чака играча. Той се върти 60 пъти в секунда: прочита клавишите, които са натиснати в момента, мести всичко според изминалото време и рисува кадъра. Ако играчът не натисне нищо, светът пак се движи.
Идеята
Същото семе, същата игра
Ако хвърлянията са истински случайни, всяко пускане е различна игра. Как тогава ще провериш, че играта работи? Не знаеш какво да очакваш.
Затова в тези уроци генераторът е Random от модула random и получава начално число, семе. Едно и също семе дава една и съща поредица, всеки път. randint(1, 6) дава цяло число от 1 до 6, и двете включени:
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 дават едни и същи числа. Пусни програмата пак и ще са същите. Затова примерите и задачите тук имат точен изход, който може да се провери.
Виж изхода и в другите табове. Всеки език има свой генератор: от едно и също семе те дават различни числа. Затова примерите със случайни числа имат изход за всеки език.
По-точно, за любопитните
- Когато играта е готова за истински играчи, махаш семето:
Random(). Тогава генераторът сам избира семе и всяка игра е различна.
- Честа практика е семето да се отпечата в началото на играта. Ако играч намери бъг, с неговото семе можеш да изиграеш точно същата игра.
Идеята
Първо най-малката работеща версия
Програмата от сцената е цяла игра в десетина реда. Тя няма двама играчи, нито меню, нито правила освен „върви напред“. Но върви от началото до края и показва, че цикълът и краят са верни.
Оттам играта расте с по едно правило. Второто правило: фигурката трябва да стигне поле 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, нито is_finished печата нещо. Те само смятат. Печатането е в кода под функциите.
Защо така? Функция, която връща число, можеш да извикаш с каквито стойности искаш и да провериш резултата: move(17, 5) трябва да е 18. Функция, която печата, проверяваш само с очи. А ако утре играта мине от конзолата в прозорец или в браузъра, правилата остават същите, сменя се само показването.
Новото поле в move е new_field, а не next: в Python next е име на вградена функция и е добре да не се закрива.
Пробвай
Къде ще стъпи фигурката?
Какво ще отпечата програмата?
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
print(move(10, 4))print(move(17, 3))print(move(18, 5))10 + 4 = 14. 17 + 3 = 20, точно. 18 + 5 = 23 е с 3 след 20, затова фигурката отскача на 20 − 3 = 17.
Пробвай
Намери грешката
Фигурката трябва да върви напред, а програмата печата поле 0 на всеки ход:
Ход 1: зар 3, поле 0Ход 2: зар 2, поле 0Ход 3: зар 4, поле 0На кой ред е грешката?
Избери реда с грешката.
move връща новото поле, но резултатът се изхвърля. Функцията не може да промени field на викащия: тя получава само стойността 0, а своя field няма. Трябва field = move(field, roll). Ако такъв ред стои в while not is_finished(field):, играта никога не свършва.Пробвай
Кога свършва играта?
Кое липсва, за да е краят точно на поле 20?
def is_finished(field):
return field 20== сравнява. Едно = е присвояване и тук Python спира още преди да тръгне, със SyntaxError. >= също ще работи, защото move никога не връща повече от 20, но == казва правилото по-ясно: краят е точно на 20.Упражнение
Три малки игри
Реши задачите в редактора си. Логиката е в функции, които връщат стойност, а печатането е в кода под тях.
1. Надбягване за двама. Ана и Борис хвърлят по ред, с Random(3). Важи правилото „точно 20“ с move от урока. Полетата им са в списък [0, 0]. Който стигне 20 пръв, печели.
Ана: зар 2, поле 2Борис: зар 5, поле 5Ана: зар 5, поле 7Борис: зар 2, поле 7Ана: зар 3, поле 10Борис: зар 5, поле 12Ана: зар 4, поле 14Борис: зар 6, поле 18Ана: зар 5, поле 19Борис: зар 1, поле 19Ана: зар 5, поле 16Борис: зар 1, поле 20Печели Борис2. Познай числото. Програмата избира тайно число от 1 до 100 с Random(2026). Играчът пише догадки, по една на ред. След всяка програмата казва „По-голямо е“, „По-малко е“ или „Позна“. Накрая казва от колко опита. Подсказката е функция hint(guess, secret), която връща текста.
50251218151650: По-малко е25: По-малко е12: По-голямо е18: По-малко е15: По-голямо е16: ПознаПозна от 6 опитаВходът е различен за всеки език, защото генераторите им избират различно тайно число от едно и също семе.
3. Кой спечели морския шах? Първият ред е броят на дъските. Всяка дъска е три реда по три знака: X, O или . за празно поле. Напиши функция winner(board), която връща X, O или ., ако няма три еднакви в ред, колона или диагонал.
Дъската може да е списък от трите прочетени реда: всеки ред е текст, а board[r][c] е знакът на ред r, колона c. Отделен тип за един знак, като char в Java, Python няма: знакът е текст с дължина 1.
3XOXOXOOOXXXOOOOX.XXOXXOOOXXПечели XПечели OНяма победителОще задачи
- Капани. Поле 13 връща фигурката на поле 5. Добави правилото в
moveи пусни надбягването пак. - Отпечатай семето. Направи семето променлива, отпечатай го в началото и пусни играта с три различни семена.
- Морски шах за двама. Играчите пишат ред и колона на хода си. След всеки ход покажи дъската и провери с
winnerдали играта е свършила.
Тест