Как се мисли игра
Проблем
Откъде започва една игра?
Искаш да напишеш игра. Например надбягване със зар: фигурка тръгва от поле 0, хвърляш зар и тя върви напред, докато стигне поле 20.
Отваряш редактора и... какво пишеш най-напред? Менюто? Зара? Дъската? Ако започнеш от всичко наведнъж, след час имаш много код, който още не се пуска.
Помага да зададеш на играта пет въпроса, преди да пишеш код.
Идеята
Пет въпроса за всяка игра
- Състояние. Кои числа описват света сега? При надбягването е полето на фигурката и номерът на хода. При шаха е къде стои всяка фигура и кой е на ход.
- Правила. Какво е позволено и какво става след него? „Фигурката върви толкова полета, колкото е зарът.“
- Вход. Какво идва отвън? Изборът на играча или числото на зара.
- Цикъл. Ход след ход: вход, обновяване на състоянието, показване. После пак.
- Край. Кога играта свършва и кой печели? „Фигурката е на поле 20.“
Отговорите са почти готовият код: състоянието са променливи, правилата са методи, входът е Console.ReadLine() или Random, цикълът е while или for, а краят е условието на цикъла.
Виж го
Ход след ход
Random random = new Random(42);int field = 0;for (int turn = 1; field < 20; turn++){ int roll = random.Next(1, 7); field = Math.Min(field + roll, 20); Console.WriteLine("Ход " + turn + ": зар " + roll + ", поле " + field);}Console.WriteLine("Финал! Фигурката стигна поле 20.");Стъпка по стъпка, с думи
- Генератор със семе 42 и фигурка на поле 0.
- Ход 1: вход е зарът 5, състоянието става поле 5, екранът показва реда.
- Ход 2: вход е зарът 1, състоянието става поле 6, екранът показва реда.
- Ход 3: вход е зарът 1, състоянието става поле 7, екранът показва реда.
- Ход 4: вход е зарът 4, състоянието става поле 11, екранът показва реда.
- Ход 5: вход е зарът 2, състоянието става поле 13, екранът показва реда.
- Ход 6: вход е зарът 2, състоянието става поле 15, екранът показва реда.
- Ход 7: вход е зарът 5, състоянието става поле 20, екранът показва реда. Поле 20: условието field < 20 вече е невярно.
- Цикълът свърши, защото полето стигна 20. Това е краят на играта.
Цялата игра е един цикъл. На всеки ход идва вход (зарът), състоянието се обновява (новото поле) и се показва (един ред на екрана). Условието на цикъла е краят: докато полето е под 20, има следващ ход.
Math.Min пази фигурката на дъската: ако зарът я изпрати след 20, тя спира на 20.
По-точно, за любопитните
- Тази игра е ходова: нищо не се случва, докато не дойде следващият вход. Така са шахът, картите и повечето игри в този раздел.
- Игрите в реално време (стрелба, състезания с коли) също имат цикъл, но той не чака играча. Той се върти 60 пъти в секунда: прочита клавишите, които са натиснати в момента, мести всичко според изминалото време и рисува кадъра. Ако играчът не натисне нищо, светът пак се движи.
Идеята
Същото семе, същата игра
Ако хвърлянията са истински случайни, всяко пускане е различна игра. Как тогава ще провериш, че играта работи? Не знаеш какво да очакваш.
Затова в тези уроци Random получава начално число, семе. Едно и също семе дава една и съща поредица, всеки път:
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 дават едни и същи числа. Пусни програмата пак и ще са същите. Затова примерите и задачите тук имат точен изход, който може да се провери.
Виж изхода и в другите табове. Всеки език има свой генератор: от едно и също семе те дават различни числа. Затова примерите със случайни числа имат изход за всеки език.
По-точно, за любопитните
- Когато играта е готова за истински играчи, махаш семето:
new 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, нито IsFinished печата нещо. Те само смятат. Печатането е в Main.
Защо така? Метод, който връща число, можеш да извикаш с каквито стойности искаш и да провериш резултата: Move(17, 5) трябва да е 18. Метод, който печата, проверяваш само с очи. А ако утре играта мине от конзолата в прозорец, правилата остават същите, сменя се само показването.
Пробвай
Къде ще стъпи фигурката?
Какво ще отпечата програмата?
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;}
Console.WriteLine(Move(10, 4));Console.WriteLine(Move(17, 3));Console.WriteLine(Move(18, 5));10 + 4 = 14. 17 + 3 = 20, точно. 18 + 5 = 23 е с 3 след 20, затова фигурката отскача на 20 − 3 = 17.
Пробвай
Намери грешката
Фигурката трябва да върви напред, а програмата печата поле 0 на всеки ход:
Ход 1: зар 3, поле 0Ход 2: зар 6, поле 0Ход 3: зар 4, поле 0На кой ред е грешката?
Избери реда с грешката.
Move връща новото поле, но резултатът се изхвърля. Методът не може да промени field на викащия: параметърът е копие на стойността. Трябва field = Move(field, roll);. Ако такъв ред стои в while (!isFinished(field)), играта никога не свършва.Пробвай
Кога свършва играта?
Кое липсва, за да е краят точно на поле 20?
static bool IsFinished(int field)
{
return field 20;
}== сравнява. Едно = е присвояване и не се компилира тук. >= също ще работи, защото Move никога не връща повече от 20, но == казва правилото по-ясно: краят е точно на 20.Упражнение
Три малки игри
Реши задачите в редактора си. Логиката е в методи, които връщат стойност, а печатането е в Main.
1. Надбягване за двама. Ана и Борис хвърлят по ред, с new Random(3). Важи правилото „точно 20“ с Move от урока. Полетата им са в масив int[2]. Който стигне 20 пръв, печели.
Ана: зар 2, поле 2Борис: зар 5, поле 5Ана: зар 6, поле 8Борис: зар 2, поле 7Ана: зар 4, поле 12Борис: зар 2, поле 9Ана: зар 2, поле 14Борис: зар 6, поле 15Ана: зар 3, поле 17Борис: зар 3, поле 18Ана: зар 4, поле 19Борис: зар 5, поле 17Ана: зар 5, поле 16Борис: зар 1, поле 18Ана: зар 1, поле 17Борис: зар 6, поле 16Ана: зар 1, поле 18Борис: зар 6, поле 18Ана: зар 5, поле 17Борис: зар 3, поле 19Ана: зар 2, поле 19Борис: зар 4, поле 17Ана: зар 3, поле 18Борис: зар 1, поле 18Ана: зар 6, поле 16Борис: зар 3, поле 19Ана: зар 4, поле 20Печели Ана2. Познай числото. Програмата избира тайно число от 1 до 100 с new Random(2026). Играчът пише догадки, по една на ред. След всяка програмата казва „По-голямо е“, „По-малко е“ или „Позна“. Накрая казва от колко опита. Подсказката е метод string Hint(int guess, int secret), който връща текста.
502512181650: По-малко е25: По-малко е12: По-голямо е18: По-малко е16: ПознаПозна от 5 опитаВходът е различен за всеки език, защото генераторите им избират различно тайно число от едно и също семе.
3. Кой спечели морския шах? Първият ред е броят на дъските. Всяка дъска е три реда по три знака: X, O или . за празно поле. Напиши метод char Winner(char[,] board), който връща X, O или ., ако няма три еднакви в ред, колона или диагонал.
3XOXOXOOOXXXOOOOX.XXOXXOOOXXПечели XПечели OНяма победителОще задачи
- Капани. Поле 13 връща фигурката на поле 5. Добави правилото в
Moveи пусни надбягването пак. - Отпечатай семето. Направи семето променлива, отпечатай го в началото и пусни играта с три различни семена.
- Морски шах за двама. Играчите пишат ред и колона на хода си. След всеки ход покажи дъската и провери с
Winnerдали играта е свършила.
Тест