RPG 3: цялата игра
Проблем
От един бой към игра
Героите и чудовищата вече се бият. Играта обаче е повече от един бой: създаваш герой, нареждаш чудовищата, с които ще се бие, и той минава през тях едно по едно. Между боевете почива или се лекува с магия.
Ако всичко това е в main, там ще има героя, масив с чудовищата, номера на следващото, броячи, проверки. А и кой ще пази играча да не започне бой без герой или да поиска магия от варварин?
Състоянието на играта и правилата за него трябва да са на едно място: в клас Game.
Идеята
Класът Game
Героят и чудовищата са състоянието на играта. Методите на Game са правилата: какво може да направи играчът и какво става след това. Първото е създаването на герой:
private Character hero;private Monster[] monsters = new Monster[5];private int count;private int next;private Random random;
public Game(Random random) { this.random = random;}
public void createHero(String name, String race) { if (race.equals("паладин")) { hero = new Paladin(name); } else if (race.equals("варварин")) { hero = new Barbarian(name); } else if (race.equals("вещица")) { hero = new Witch(name); } else { throw new GameException("Няма такава раса: " + race); }}private Character hero;private Monster[] monsters = new Monster[5];private int count;private int next;private Random random;
public Game(Random random){ this.random = random;}
public void CreateHero(string name, string race){ if (race == "паладин") { hero = new Paladin(name); } else if (race == "варварин") { hero = new Barbarian(name); } else if (race == "вещица") { hero = new Witch(name); } else { throw new GameException("Няма такава раса: " + race); }}def __init__(self, random): self._hero = None self._monsters = [None] * 5 self._count = 0 self._next = 0 self._random = random
def create_hero(self, name, race): if race == "паладин": self._hero = Paladin(name) elif race == "варварин": self._hero = Barbarian(name) elif race == "вещица": self._hero = Witch(name) else: raise GameError(f"Няма такава раса: {race}")#hero = null;#monsters = new Array(5);#count = 0;#next = 0;#random;
constructor(random) { this.#random = random;}
createHero(name, race) { if (race === "паладин") { this.#hero = new Paladin(name); } else if (race === "варварин") { this.#hero = new Barbarian(name); } else if (race === "вещица") { this.#hero = new Witch(name); } else { throw new GameError(`Няма такава раса: ${race}`); }}new Random(), а в примерите е със семе, за да е изходът винаги един и същ.Непознатата раса не е повод да се създаде герой наслуки. Методът хвърля GameException: свое изключение за всеки ход, който правилата не позволяват. За него има отделна карта по-нататък.
Идеята
Редица от чудовища
Чудовищата чакат на опашка: кое е добавено първо, с него е първият бой. Тук опашката е масив и два индекса:
public void addMonster(Monster monster) { if (count == monsters.length) { throw new GameException("Редицата е пълна: най-много " + monsters.length + " чудовища"); } monsters[count] = monster; count++;}
public Monster createRandomMonster() { int kind = random.nextInt(3); if (kind == 0) { return new FallenShaman(); } if (kind == 1) { return new SkeletonKing(); } return new Butcher();}
public boolean hasMonsters() { return next < count;}public void AddMonster(Monster monster){ if (count == monsters.Length) { throw new GameException("Редицата е пълна: най-много " + monsters.Length + " чудовища"); } monsters[count] = monster; count++;}
public Monster CreateRandomMonster(){ int kind = random.Next(3); if (kind == 0) { return new FallenShaman(); } if (kind == 1) { return new SkeletonKing(); } return new Butcher();}
public bool HasMonsters(){ return next < count;}def add_monster(self, monster): if self._count == len(self._monsters): raise GameError(f"Редицата е пълна: най-много {len(self._monsters)} чудовища") self._monsters[self._count] = monster self._count += 1
def create_random_monster(self): kind = self._random.randrange(3) if kind == 0: return FallenShaman() if kind == 1: return SkeletonKing() return Butcher()
def has_monsters(self): return self._next < self._countaddMonster(monster) { if (this.#count === this.#monsters.length) { throw new GameError(`Редицата е пълна: най-много ${this.#monsters.length} чудовища`); } this.#monsters[this.#count] = monster; this.#count++;}
createRandomMonster() { const kind = Math.floor(this.#random() * 3); if (kind === 0) { return new FallenShaman(); } if (kind === 1) { return new SkeletonKing(); } return new Butcher();}
hasMonsters() { return this.#next < this.#count;}countе колко чудовища са добавени. Новото отива наmonsters[count].nextе кое е следващото за бой. След всеки бой расте с 1, а когато стигнеcount, редицата е свършила.
addMonster хвърля GameException, преди да пише извън масива.createRandomMonster е бонусът от старото упражнение: чудовище по жребий.
По-точно, за любопитните
-
Старото упражнение пазеше чудовищата в истинска опашка,
Queue<Monster>. Опашката сама помни кое е първо и не се пълни. Тя идва в модул „Колекции“ (ArrayDeque): тамcountиnextизчезват, а редицата расте колкото трябва. - Чудовищата преди
nextостават в масива. Не пречат: никой не ги чете вече.
Идеята
Нива и опит
След всяка победа героят получава опит, колкото е бил животът на чудовището. На ниво 1 трябват 100 опит за ниво 2, на ниво 2 трябват 200 за ниво 3 и т.н. Новото ниво дава 10 живот повече, 3 сила повече и пълен живот:
public boolean gainExperience(int exp) { experience += exp; boolean up = false; while (experience >= level * 100) { experience -= level * 100; levelUp(); up = true; } return up;}
public void levelUp() { level++; maxHealth += 10; power += 3; health = maxHealth;}public bool GainExperience(int exp){ Experience += exp; bool up = false; while (Experience >= Level * 100) { Experience -= Level * 100; LevelUp(); up = true; } return up;}
public void LevelUp(){ Level++; MaxHealth += 10; Power += 3; Health = MaxHealth;}def gain_experience(self, exp): self._experience += exp up = False while self._experience >= self._level * 100: self._experience -= self._level * 100 self.level_up() up = True return up
def level_up(self): self._level += 1 self._max_health += 10 self._power += 3 self._health = self._max_healthgainExperience(exp) { this._experience += exp; let up = false; while (this._experience >= this._level * 100) { this._experience -= this._level * 100; this.levelUp(); up = true; } return up;}
levelUp() { this._level++; this._maxHealth += 10; this._power += 3; this._health = this._maxHealth;}Методите са в Character. Опитът над прага остава за следващото ниво. Цикълът while е за случая, в който една победа стига за две нива.
Полетата maxHealth, level и experience са нови в Character. С maxHealth лечението вече не вдига живота над най-големия.
Пробвай
На кое ниво е варваринът?
Грок започва на ниво 1 със сила 20. Печели срещу шамана (60 опит) и срещу краля (90 опит).
Какво ще отпечата програмата? Ниво, опит, сила.
public boolean gainExperience(int exp) { experience += exp; boolean up = false; while (experience >= level * 100) { experience -= level * 100; levelUp(); up = true; } return up;}
public void levelUp() { level++; maxHealth += 10; power += 3; health = maxHealth;}
Character grok = new Barbarian("Грок");grok.gainExperience(60);grok.gainExperience(90);System.out.println(grok.getLevel() + " " + grok.getExperience() + " " + grok.getPower());60 + 90 = 150 опит. На ниво 1 прагът е 100: Грок минава на ниво 2, остават 50. За ниво 3 трябват 200. Силата е 20 + 3 = 23.
Виж го
Битката и почивката
startBattle първо проверява дали боят е възможен. После взима следващото чудовище и пуска боя до смърт от втората част. Връща дали героят е оцелял:
public boolean startBattle() { if (hero == null) { throw new GameException("Първо създай герой"); } if (!hasMonsters()) { throw new GameException("Няма чудовища в редицата"); } Monster monster = monsters[next]; next++; int rounds = 0; while (hero.isAlive() && monster.isAlive()) { rounds++; hero.attack(monster); if (monster.isAlive()) { monster.attack(hero); } } if (!hero.isAlive()) { System.out.println(hero.getName() + " пада в рунд " + rounds + ". Побеждава " + monster.getName()); return false; } System.out.println(monster.getName() + " е победен за " + rounds + " рунда. " + hero.getName() + ": живот " + hero.getHealth() + "/" + hero.getMaxHealth()); if (hero.gainExperience(monster.getReward())) { System.out.println(hero.getName() + " е на ниво " + hero.getLevel() + ": живот " + hero.getMaxHealth() + ", сила " + hero.getPower()); } return true;}public bool StartBattle(){ if (hero == null) { throw new GameException("Първо създай герой"); } if (!HasMonsters()) { throw new GameException("Няма чудовища в редицата"); } Monster monster = monsters[next]; next++; int rounds = 0; while (hero.IsAlive() && monster.IsAlive()) { rounds++; hero.Attack(monster); if (monster.IsAlive()) { monster.Attack(hero); } } if (!hero.IsAlive()) { Console.WriteLine(hero.Name + " пада в рунд " + rounds + ". Побеждава " + monster.Name); return false; } Console.WriteLine(monster.Name + " е победен за " + rounds + " рунда. " + hero.Name + ": живот " + hero.Health + "/" + hero.MaxHealth); if (hero.GainExperience(monster.Reward)) { Console.WriteLine(hero.Name + " е на ниво " + hero.Level + ": живот " + hero.MaxHealth + ", сила " + hero.Power); } return true;}def start_battle(self): if self._hero is None: raise GameError("Първо създай герой") if not self.has_monsters(): raise GameError("Няма чудовища в редицата") hero = self._hero monster = self._monsters[self._next] self._next += 1 rounds = 0 while hero.is_alive() and monster.is_alive(): rounds += 1 hero.attack(monster) if monster.is_alive(): monster.attack(hero) if not hero.is_alive(): print(f"{hero.name} пада в рунд {rounds}. Побеждава {monster.name}") return False print(f"{monster.name} е победен за {rounds} рунда. {hero.name}: живот {hero.health}/{hero.max_health}") if hero.gain_experience(monster.reward): print(f"{hero.name} е на ниво {hero.level}: живот {hero.max_health}, сила {hero.power}") return TruestartBattle() { if (this.#hero === null) { throw new GameError("Първо създай герой"); } if (!this.hasMonsters()) { throw new GameError("Няма чудовища в редицата"); } const hero = this.#hero; const monster = this.#monsters[this.#next]; this.#next++; let rounds = 0; while (hero.isAlive() && monster.isAlive()) { rounds++; hero.attack(monster); if (monster.isAlive()) { monster.attack(hero); } } if (!hero.isAlive()) { console.log(`${hero.name} пада в рунд ${rounds}. Побеждава ${monster.name}`); return false; } console.log(`${monster.name} е победен за ${rounds} рунда. ${hero.name}: живот ${hero.health}/${hero.maxHealth}`); if (hero.gainExperience(monster.reward)) { console.log(`${hero.name} е на ниво ${hero.level}: живот ${hero.maxHealth}, сила ${hero.power}`); } return true;}recover е изборът след боя. Магия може само герой, който реализира IHealable. Почивката е 5 хода по 10 живот:
public void recover(String way) { if (way.equals("почивка")) { for (int turn = 0; turn < 5; turn++) { hero.restore(10); } System.out.println(hero.getName() + " почива 5 хода: живот " + hero.getHealth() + "/" + hero.getMaxHealth()); } else if (way.equals("магия")) { if (!(hero instanceof IHealable)) { throw new GameException(hero.getName() + " не владее лечебна магия"); } System.out.println(hero.useAbility()); } else { throw new GameException("Не разбирам „" + way + "“: почивка или магия?"); }}public void Recover(string way){ if (way == "почивка") { for (int turn = 0; turn < 5; turn++) { hero.Restore(10); } Console.WriteLine(hero.Name + " почива 5 хода: живот " + hero.Health + "/" + hero.MaxHealth); } else if (way == "магия") { if (!(hero is IHealable)) { throw new GameException(hero.Name + " не владее лечебна магия"); } Console.WriteLine(hero.UseAbility()); } else { throw new GameException("Не разбирам „" + way + "“: почивка или магия?"); }}def recover(self, way): hero = self._hero if way == "почивка": for _ in range(5): hero.restore(10) print(f"{hero.name} почива 5 хода: живот {hero.health}/{hero.max_health}") elif way == "магия": if not isinstance(hero, Healable): raise GameError(f"{hero.name} не владее лечебна магия") print(hero.use_ability()) else: raise GameError(f"Не разбирам „{way}“: почивка или магия?")recover(way) { const hero = this.#hero; if (way === "почивка") { for (let turn = 0; turn < 5; turn++) { hero.restore(10); } console.log(`${hero.name} почива 5 хода: живот ${hero.health}/${hero.maxHealth}`); } else if (way === "магия") { if (typeof hero.heal !== "function") { throw new GameError(`${hero.name} не владее лечебна магия`); } console.log(hero.useAbility()); } else { throw new GameError(`Не разбирам „${way}“: почивка или магия?`); }}В старото упражнение героят чакаше 5 секунди. Тук почивката е в ходове, защото в ходова игра времето се мери в ходове, а и проверката на играта не бива да чака.
Виж го
Цялата игра
Варваринът Грок срещу шамана и две случайни чудовища. След всеки бой почива. Ако падне, играта свършва:
Game game = new Game(new Random(12));game.createHero("Грок", "варварин");game.addMonster(new FallenShaman());game.addMonster(game.createRandomMonster());game.addMonster(game.createRandomMonster());while (game.hasMonsters()) { if (!game.startBattle()) { System.out.println("Край на играта"); break; } game.recover("почивка");}Падналият шаман е победен за 3 рунда. Грок: живот 90/110Грок почива 5 хода: живот 110/110Падналият шаман е победен за 3 рунда. Грок: живот 90/110Грок е на ниво 2: живот 120, сила 23Грок почива 5 хода: живот 120/120Касапинът е победен за 6 рунда. Грок: живот 70/120Грок почива 5 хода: живот 120/120Game game = new Game(new Random(12));game.CreateHero("Грок", "варварин");game.AddMonster(new FallenShaman());game.AddMonster(game.CreateRandomMonster());game.AddMonster(game.CreateRandomMonster());while (game.HasMonsters()){ if (!game.StartBattle()) { Console.WriteLine("Край на играта"); break; } game.Recover("почивка");}Падналият шаман е победен за 3 рунда. Грок: живот 90/110Грок почива 5 хода: живот 110/110Касапинът е победен за 7 рунда. Грок: живот 48/110Грок е на ниво 2: живот 120, сила 23Грок почива 5 хода: живот 120/120Скелетният крал е победен за 5 рунда. Грок: живот 68/120Грок почива 5 хода: живот 118/120game = Game(Random(12))game.create_hero("Грок", "варварин")game.add_monster(FallenShaman())game.add_monster(game.create_random_monster())game.add_monster(game.create_random_monster())while game.has_monsters(): if not game.start_battle(): print("Край на играта") break game.recover("почивка")Падналият шаман е победен за 3 рунда. Грок: живот 90/110Грок почива 5 хода: живот 110/110Скелетният крал е победен за 6 рунда. Грок: живот 45/110Грок е на ниво 2: живот 120, сила 23Грок почива 5 хода: живот 120/120Скелетният крал е победен за 5 рунда. Грок: живот 68/120Грок почива 5 хода: живот 118/120const game = new Game(makeRandom(12));game.createHero("Грок", "варварин");game.addMonster(new FallenShaman());game.addMonster(game.createRandomMonster());game.addMonster(game.createRandomMonster());while (game.hasMonsters()) { if (!game.startBattle()) { console.log("Край на играта"); break; } game.recover("почивка");}Падналият шаман е победен за 3 рунда. Грок: живот 90/110Грок почива 5 хода: живот 110/110Скелетният крал е победен за 6 рунда. Грок: живот 45/110Грок е на ниво 2: живот 120, сила 23Грок почива 5 хода: живот 120/120Скелетният крал е победен за 5 рунда. Грок: живот 68/120Грок почива 5 хода: живот 118/120Game game = new Game(new Random(12));game.createHero("Грок", "варварин");game.addMonster(new FallenShaman());game.addMonster(game.createRandomMonster());game.addMonster(game.createRandomMonster());while (game.hasMonsters()) { if (!game.startBattle()) { System.out.println("Край на играта"); break; } game.recover("почивка");}Стъпка по стъпка, с думи
- Играта получава генератор със семе 12. Героят е Грок, а масивът има 5 места, още празни.
- Всяко addMonster() пише на monsters[count] и вдига count. Второто и третото са по жребий: Падналият шаман и Касапинът.
- next е 0: излиза Падналият шаман. Победен е за 3 рунда и на Грок остават 90/110.
- next става 1. Почивката е 5 хода по 10 живот: 110/110.
- next е 1: излиза Падналият шаман. Победен е за 3 рунда и на Грок остават 90/110. Опитът стига за ниво 2: живот 120, сила 23.
- next става 2. Почивката е 5 хода по 10 живот: 120/120.
- next е 2: излиза Касапинът. Победен е за 6 рунда и на Грок остават 70/120.
- next става 3. Почивката е 5 хода по 10 живот: 120/120.
- next е 3, колкото е count: hasMonsters() връща false и цикълът свършва.
Идеята
Неправилен ход: GameException
Играчът може да поиска бой без герой, раса, която я няма, или магия от варварин. Това не е бъг в програмата. Това е ход, който правилата не позволяват. Методите на Game хвърлят за него свое изключение:
class GameException extends RuntimeException { public GameException(String message) { super(message); }}class GameException : Exception{ public GameException(string message) : base(message) { }}class GameError(Exception): passclass GameError extends Error { constructor(message) { super(message); this.name = "GameError"; }}Играта хваща само него, казва на играча защо не става и продължава със следващия ход:
Game game = new Game(new Random(12));String[] commands = { "бой", "герой Грок дракон", "герой Грок варварин", "магия", "сън" };for (String command : commands) { String[] words = command.split(" "); try { if (words[0].equals("бой")) { game.startBattle(); } else if (words[0].equals("герой")) { game.createHero(words[1], words[2]); System.out.println("Героят е " + words[1]); } else { game.recover(words[0]); } } catch (GameException e) { System.out.println("Не става: " + e.getMessage()); }}Не става: Първо създай геройНе става: Няма такава раса: драконГероят е ГрокНе става: Грок не владее лечебна магияНе става: Не разбирам „сън“: почивка или магия?Game game = new Game(new Random(12));string[] commands = { "бой", "герой Грок дракон", "герой Грок варварин", "магия", "сън" };foreach (string command in commands){ string[] words = command.Split(' '); try { if (words[0] == "бой") { game.StartBattle(); } else if (words[0] == "герой") { game.CreateHero(words[1], words[2]); Console.WriteLine("Героят е " + words[1]); } else { game.Recover(words[0]); } } catch (GameException e) { Console.WriteLine("Не става: " + e.Message); }}Не става: Първо създай геройНе става: Няма такава раса: драконГероят е ГрокНе става: Грок не владее лечебна магияНе става: Не разбирам „сън“: почивка или магия?game = Game(Random(12))commands = ["бой", "герой Грок дракон", "герой Грок варварин", "магия", "сън"]for command in commands: words = command.split() try: if words[0] == "бой": game.start_battle() elif words[0] == "герой": game.create_hero(words[1], words[2]) print(f"Героят е {words[1]}") else: game.recover(words[0]) except GameError as e: print(f"Не става: {e}")Не става: Първо създай геройНе става: Няма такава раса: драконГероят е ГрокНе става: Грок не владее лечебна магияНе става: Не разбирам „сън“: почивка или магия?const game = new Game(makeRandom(12));const commands = ["бой", "герой Грок дракон", "герой Грок варварин", "магия", "сън"];for (const command of commands) { const words = command.split(" "); try { if (words[0] === "бой") { game.startBattle(); } else if (words[0] === "герой") { game.createHero(words[1], words[2]); console.log(`Героят е ${words[1]}`); } else { game.recover(words[0]); } } catch (e) { if (!(e instanceof GameError)) { throw e; } console.log(`Не става: ${e.message}`); }}Не става: Първо създай геройНе става: Няма такава раса: драконГероят е ГрокНе става: Грок не владее лечебна магияНе става: Не разбирам „сън“: почивка или магия?Защо свое изключение, а не готовото IllegalArgumentException? Защото catch (GameException e) хваща само отказите по правилата. Ако в играта има бъг и тя хвърли NullPointerException, той няма да се скрие като „Не става“, а ще спре програмата и ще го видиш.
Пробвай
Намери грешката
Шестото чудовище трябва да получи „Редицата е пълна“. Вместо това програмата спира с ArrayIndexOutOfBoundsException. На кой ред е грешката?
Избери реда с грешката.
count е 5, а 5 > monsters.length е false. Проверката пропуска и шестото се пише на monsters[5], а последното място е monsters[4]. Масивът е пълен, когато count стане равно на дължината му: count == monsters.length.Пробвай
Кое липсва?
От кой клас наследява GameException?
class GameException extends {
public GameException(String message) {
super(message);
}
}RuntimeException: така изключението е непроверено и методите на Game не трябва да го обявяват с throws. С Exception всеки викащ ще трябва да го хване или обяви, а отказите по правилата са очаквани и се хващат на едно място.Упражнение
Играй и разширявай
Реши задачите в редактора си. Започни от класовете в урока.
1. Меню. Командите идват от входа, по една на ред, до „край“:
- „герой Име раса“ създава героя;
- „чудовище шаман“ (или „крал“, „касапин“) добавя готово чудовище, а „чудовище Име живот сила“ добавя свое: клас
Beastот расата „звяр“; - „случайно“ добавя случайно чудовище;
- „бой“ пуска следващия бой, а „почивка“ и „магия“ са изборът след него.
Всеки отказ е GameException, която менюто хваща и казва „Не става: …“. Семето е 12.
герой Морена елфгерой Морена вещицачудовище Вълк 50 9чудовище дракончудовище шаманслучайнобоймагиябойпочивкабойкрайНе става: Няма такава раса: елфГероят е МоренаВ редицата: ВълкНе става: Няма такова чудовище: драконВ редицата: Падналият шаманВ редицата: Падналият шаманВълк е победен за 4 рунда. Морена: живот 68/95Морена призовава стихиите: живот 83/95Падналият шаман е победен за 4 рунда. Морена: живот 47/95Морена е на ниво 2: живот 105, сила 19Морена почива 5 хода: живот 105/105Падналият шаман е победен за 4 рунда. Морена: живот 69/1052. Напредък. Добави в Game метод showProgress(), който показва нивото и колко опит има героят от колко нужни за следващото ниво. Паладинът Ардан минава през два шамана и Скелетния крал. След всеки бой показва напредъка и се лекува с магия.
Падналият шаман е победен за 5 рунда. Ардан: живот 72/120Ниво 1, опит 60/100Ардан се лекува: живот 92/120Падналият шаман е победен за 5 рунда. Ардан: живот 44/120Ардан е на ниво 2: живот 130, сила 17Ниво 2, опит 20/200Ардан се лекува: живот 130/130Скелетният крал е победен за 8 рунда. Ардан: живот 18/130Ниво 2, опит 110/200Ардан се лекува: живот 38/1303. Бос. Клас Diablo, наследник на Monster: 160 живот, 14 сила. Всеки трети негов удар е двоен. Метод canChallenge(Character hero) казва дали героят е поне на ниво 2. Ако не е, startBattle хвърля GameException и Диабло остава в редицата. Пусни го веднъж срещу Грок на ниво 1, после в нова игра след шамана и краля.
Не става: Диабло се бие само с герои от ниво 2 нагореПадналият шаман е победен за 3 рунда. Грок: живот 90/110Грок почива 5 хода: живот 110/110Скелетният крал е победен за 6 рунда. Грок: живот 45/110Грок е на ниво 2: живот 120, сила 23Грок почива 5 хода: живот 120/120Диабло е победен за 7 рунда. Грок: живот 24/120Грок е на ниво 3: живот 130, сила 26Грок почива 5 хода: живот 130/130Продължи сам
Старото упражнение предлагаше още идеи. Някои искат инструмент, който идва по-нататък:
- Умения с ниво. Клас
Skillс име и ниво,useSkill(Character target)иupgradeSkill(). Става и сега, с масив от умения в героя. - Инвентар. Клас
Itemс име и ефект,applyEffect(Character target), иInventory, в който предметите се добавят и махат. С масив е тромаво. СписъкътArrayListидва в модул „Колекции“. - Босове с изисквания. Бос, който иска определени предмети и умения, преди да приеме боя, и пуска плячка след победа. Иска инвентара отгоре.
- Екрани. Меню, карта, инвентар: всеки екран е обект с
show(),hide()иupdate(), а отворените екрани са в стек, за да може „назад“. Стекът идва в модул „Колекции“. - Запис и зареждане. Героят, нивото и най-добрият резултат се пишат във файл и се четат при следващото пускане. Файловете идват в модул „Текст и файлове“.
Тест