Бойната система
Накратко: Боят се върти на тикове: всеки добавя бързината си към лента с енергия, който стигне 100, действа и плаща 100. Ударът е точност срещу избягване, щета = оръжие + сила − защита (поне 1), критичен удар удвоява, а видът щета решава какво следва.
Проблем
Вълкът е по-бърз от голема
В мината героят среща вълк кръвник и гоблин едновременно. Кой удря пръв и колко пъти? Първият отговор е по кръгове: героят, после всеки враг по реда им, и отначало. Но вълкът е два пъти по-бърз от рудния голем (бързина 14 срещу 7). Ако и двамата удрят веднъж на кръг, бързината не значи нищо, а играч, който е вложил точки в нея, не получава нищо.
Затова редът е друг, а ударът има свои правила. Ето екран на боя от играта (от минаването, с което се проверява играта): героят е срещу гигантски плъх, дневникът пази последните редове, а ударът на героя е пропуснат.
=== Бой ===Ива, варварин ур.1 Ж [###########-] 46/48 М [########] 4/41) Гигантски плъх [########] 9/9 Бой! Срещу теб: гигантски плъх. Гигантски плъх драска Ива (1). Ива пропуска гигантски плъх. Гигантски плъх драска Ива (1).1 удар 2 умение 3 предмет 4 бягствоЗа този кадър трябват пет неща: ред на ходовете, точност срещу избягване, щета, видове щета и думи в дневника. Урокът ги строи едно по едно с малки програми, а накрая ги събира в бой на героя с вълк и гоблин и показва същото в играта. Бойците тук са със готови показатели: как се събират от база, екипировка и ефекти, е в урока за същества.
Идеята
Бойци с готови показатели
Боецът е клас с показатели, живот и една лента с енергия:
class Fighter: def __init__(self, name, max_hp, strength, agility, defense, speed, weapon_min, weapon_max, kind, resist, weak): self.name = name self.max_hp = max_hp self.strength = strength self.agility = agility self.defense = defense self.speed = speed self.weapon_min = weapon_min self.weapon_max = weapon_max self.kind = kind self.resist = resist self.weak = weak self.hp = max_hp self.energy = 0
def accuracy(self): return 75 + self.agility * 2
def evasion(self): return self.agility * 2
def crit_chance(self): return 5 + self.agility // 10
def is_alive(self): return self.hp > 0От ловкостта идват три числа за боя:
- точност = 75 + 2 × ловкост: колко добре улучваш;
- избягване = 2 × ловкост: колко трудно те улучват;
- шанс за критичен удар = 5 + ловкост / 10 (цяло деление): всеки удар е с поне 5% шанс.
Вълкът е с ловкост 8, героят с 4: точността на героя е 75 + 8 = 83, избягването на вълка е 16. В играта същите три формули стоят в Creature, но показателите идват от слоевете (база + екипировка + ефекти):
# Суровата стойност (може да е отрицателна): база + екипировка + ефекти.def raw_stat(self, stat): value = self.base[stat] + self.gear_bonus(stat) for effect in self.effects: value += effect.modifier(stat) return value
def stat(self, stat): value = self.raw_stat(stat) return 0 if value < 0 else value
def max_hp(self): value = self.stat('maxHp') return 1 if value < 1 else value
def max_mana(self): return self.stat('maxMana')
# Под 25% живот раната е тежка: ударите стават по-неточни.def is_heavily_wounded(self): return self.hp * 4 < self.max_hp()
def accuracy(self): return 75 + self.stat('agility') * 2 + self.raw_stat('accuracy') - (15 if self.is_heavily_wounded() else 0)
def evasion(self): return self.stat('agility') * 2 + self.raw_stat('evasion')
def crit_chance(self): return 5 + self.stat('agility') // 10self.agility // 10 е цяло деление. Показателите не са отрицателни, затова // и / на другите езици дават едно и също.Идеята
Енергия вместо кръгове
Вместо кръгове всеки боец има лента с енергия. Светът върти тикове: на всеки тик всеки добавя бързината си към лентата. Който стигне 100, може да действа; когато действа, плаща 100. По-бързият стига 100 по-често, затова действа по-често, и никой не чака „кръга“.
def next_actor(self): while True: best = None for f in self.participants(): if f.energy >= 100 and (best is None or f.energy > best.energy): best = f if best is not None: return best for f in self.participants(): f.energy += max(1, f.speed) self.ticks += 1Ето правилата, наредени както ги чете кодът:
- Между живите бойци се търси този с най-много енергия от тези на 100 или повече. При равенство печели по-ранният в списъка, а героят е пръв. (Сравнението е
>, не>=: по-късният трябва да е строго по-пълен.) - Ако такъв няма, започва тик: всеки добавя
max(1, speed). Долната граница 1 пази цикъла: боец с бързина 0 или по-малка (ефект на забавяне) иначе никога не би стигнал 100 и цикълът щеше да е безкраен. - Победителят не се нулира, а плаща 100. Остатъкът пази предимството му.
Програмата подрежда героя (бързина 10), вълка (14) и гоблина (10) и печата първите седем хода:
hero = Fighter("Ива", 48, 7, 4, 3, 10, 4, 7, "slash", "", "")wolf = Fighter("Вълк кръвник", 20, 3, 8, 1, 14, 2, 5, "slash", "", "")goblin = Fighter("Гоблин копиеносец", 16, 2, 5, 1, 10, 2, 5, "pierce", "", "")battle = Battle([hero, wolf, goblin])for _ in range(7): actor = battle.next_actor() print(f"Тик {battle.ticks}: {actor.name} действа (енергия {actor.energy}).") actor.energy -= 100Тик 8: Вълк кръвник действа (енергия 112).Тик 10: Ива действа (енергия 100).Тик 10: Гоблин копиеносец действа (енергия 100).Тик 15: Вълк кръвник действа (енергия 110).Тик 20: Ива действа (енергия 100).Тик 20: Гоблин копиеносец действа (енергия 100).Тик 22: Вълк кръвник действа (енергия 108).Вълкът е пръв на тик 8 (14 × 8 = 112). На тик 10 героят и гоблинът са на 100 едновременно: героят е по-ранен в списъка, а гоблинът действа веднага след него, без нов тик. Вълкът пак действа на тик 15, не на 16: от първия ход му остават 12.
Виж го
Опашката в движение
Трите ленти растат тик по тик. Следи чертата на 100 и номерата на плочките долу:
def next_actor(self): while True: best = None for f in self.participants(): if f.energy >= 100 and (best is None or f.energy > best.energy): best = f if best is not None: return best for f in self.participants(): f.energy += max(1, f.speed) self.ticks += 1
# в цикъла на бояactor = battle.next_actor()actor.energy -= 100Стъпка по стъпка, с думи
- Трима бойци: Ива, Вълк кръвник и Гоблин копиеносец. Всеки има лента с енергия, която започва от 0, и бързина (10, 14, 10). Който стигне 100, действа.
- Няма кой да действа още, затова светът прави тик: всеки добавя бързината си към лентата (поне 1). Лентите растат с различна скорост.
- Тик 8: Вълк кръвник е на 112. Той стига 100 пръв и действа. Плаща 100, но не нула: остават му 12.
- Тик 10: Ива и Гоблин копиеносец са на 100 едновременно. Равенство: действа по-ранният в списъка, а героят е пръв.
- Героят плати, а Гоблин копиеносец още е на 100. Няма нов тик: следващото извикване на nextActor го връща веднага.
- Тик 15: Вълк кръвник действа пак, вече на 110. Остатъкът от първия ход му спестява тик: без него щеше да чака до тик 16.
- След 22 тика: Вълк кръвник е действал 3 пъти, Ива и Гоблин копиеносец по 2. Бързината е скорост на лентата, не брой ходове: така по-бързият действа по-често и няма „кръгове“.
В играта същата опашка стои в началото на класа Battle, а ходът започва с плащането на 100:
def next_actor(self): while True: best = None for creature in self.participants(): if creature.energy >= 100 and (best is None or creature.energy > best.energy): best = creature if best is not None: return best for creature in self.participants(): creature.energy += max(1, creature.stat('speed'))# Началото на хода: ефектите действат, презарежданията намаляват. Зашеметеният пропуска.def begin_turn(self, actor): actor.energy -= 100 stunned = actor.has_effect('stun') tick_effects(actor, self) actor.tick_cooldowns() self.handle_deaths() if not actor.is_alive(): return False # След зашеметяване следващият ход е сигурен: иначе трима врагове с тояги заключват героя. actor.stun_proof = stunned if stunned: self.say(t('combat.stunned', {'name': actor.name})) return False return TrueПървият ред на beginTurn е actor.energy -= 100. После идват ефектите (кървене, отрова), презарежданията и пропускането на хода при зашеметяване: това са темите на следващите уроци (кръв, рани и ефекти). Тук важното е, че редът на ходовете е отделен от самия удар: nextActor казва кой действа, а какво прави, е друг въпрос.
По-точно, за любопитните
Тази опашка е опашка с приоритет: следващият е този с най-висок приоритет (най-много енергия). Тук се реализира с линейно обхождане на всички живи бойци при всяко извикване. В боя са най-много четирима, затова това са няколко сравнения. При стотици участници (битка на армии) се ползва двоична купчина (heap): следващият се взима за O(log n), не за O(n).
Тиковете също могат да се прескочат: вместо да се добавя по една бързина, се смята за всеки ⌈(100 − енергия) / бързина⌉ тика до готовността, взема се най-малкото и всички се напредват с него наведнъж. Резултатът е същият, но без цикъл. Канонът пази простия цикъл: той се чете с един поглед и стига за четирима.
Ограничението в играта е до 200 хода на героя на бой (heroTurns): ако нещо в правилата заседне, боят свършва като загуба, а не виси вечно.
Идеята
Улучи или пропусни
Ударът не винаги улучва. Шансът е точността на нападателя минус избягването на целта, затегнат между 10 и 95:
def clamp(value, low, high): if value < low: return low if value > high: return high return value
def hit_chance(accuracy, evasion): return clamp(accuracy - evasion, 10, 95)Затягането (clamp) значи, че всеки удар има поне 10% шанс да улучи и поне 5% да пропусне: ловка цел не е недосегаема, а най-точният удар не е сигурен. После боят хвърля зар от 1 до 100 и пропуска, ако хвърленото е над шанса.
Програмата показва четири двойки (точност, избягване) и пет хвърляния със семе 2026 срещу шанса на героя към вълка, 83 − 16 = 67%:
pairs = [(83, 16), (91, 8), (83, 80), (120, 0)]for accuracy, evasion in pairs: print(f"{accuracy} - {evasion} = {accuracy - evasion}, шанс {hit_chance(accuracy, evasion)}%")
chance = hit_chance(83, 16)dice = Random(2026)for _ in range(5): roll = dice.range(1, 100) print(f"хвърляш {roll}" + (" — пропуск" if roll > chance else " — удар"))83 - 16 = 67, шанс 67%91 - 8 = 83, шанс 83%83 - 80 = 3, шанс 10%120 - 0 = 120, шанс 95%хвърляш 76 — пропускхвърляш 59 — удархвърляш 23 — удархвърляш 37 — удархвърляш 58 — ударЗаровете са генераторът от канона (Random, mulberry32): със същото семе падат същите числа на четирите езика. Затова един бой със семе е повторяем и може да се запише, да се възпроизведе и да се провери с тест.
def hit_chance(accuracy, evasion):
return clamp(accuracy - evasion, , 95)Долната граница е 10: дори срещу най-ловкия враг (83 − 80 = 3) ударът улучва в един от десет случая. Горната е 95.
Идеята
Щета и критичен удар
Улучил ли си, щетата е:
оръжие + сила − защита, поне 1.
Оръжието дава число от интервал (ръчната брадва на героя е 4–7), силата на нападателя се добавя, а защитата на целта се вади. Последното ограничение е важно: слаб удар срещу здрава броня не лекува целта и не е нула, а си остава 1.
Критичен удар има шанс 5% + ловкост / 10 (и още 10% за пробождащо оръжие) и удвоява оръжието и силата. Защитата се вади след удвояването, затова бронята отнема същото и от критичния удар:
def damage_of(weapon, strength, defense, crit): damage = weapon + strength if crit: damage *= 2 damage -= defense if damage < 1: damage = 1 return damage
def show(label, weapon, strength, defense, crit): total = f"{weapon} + {strength}" if crit: total = f"({total}) * 2" print(f"{label}: {total} - {defense} -> {damage_of(weapon, strength, defense, crit)}")show("обикновен удар", 6, 7, 1, False)show("критичен удар", 6, 7, 1, True)show("слаб удар срещу броня", 2, 1, 6, False)
for agility in [4, 8, 25, 40]: print(f"критичен удар: ловкост {agility} -> {5 + agility // 10}%")обикновен удар: 6 + 7 - 1 -> 12критичен удар: (6 + 7) * 2 - 1 -> 25слаб удар срещу броня: 2 + 1 - 6 -> 1критичен удар: ловкост 4 -> 5%критичен удар: ловкост 8 -> 5%критичен удар: ловкост 25 -> 7%критичен удар: ловкост 40 -> 9%Първият ред е ударът на героя срещу вълк (защита 1). Вторият е същият удар, но критичен: 25, повече от два пъти. Третият е слаб удар срещу броня 6: изваждането дава −3, а играта връща 1. Последните четири реда са шансът за критичен удар при различна ловкост: расте бавно, с 1% на десет точки.
Идеята
Видове щета
Ударът има вид: режещо, пробождащо, тъпо, огън, отрова. Видът казва две неща. Първо, как реагира целта: някои същества са устойчиви (щетата се дели на 2), други са слаби (умножава се по 3/2). Второ, какво следва след удара: режещото кърви, тъпото зашеметява, огънят гори, отровата отравя (ефектите са темата на следващия урок).
def resisted(target, kind, damage): if target.resist == kind: return damage // 2 if target.weak == kind: return damage * 3 // 2 return damageСкелетът е устойчив на режещо и слаб на тъпо. Удар от 12 срещу него дава:
skeleton = Fighter("Скелет", 28, 4, 4, 2, 10, 3, 6, "slash", "slash", "blunt")kinds = ["slash", "pierce", "blunt", "fire", "poison"]names = ["режещо", "пробождащо", "тъпо", "огън", "отрова"]for i in range(len(kinds)): print(f"{names[i]}: 12 -> {resisted(skeleton, kinds[i], 12)}")режещо: 12 -> 6пробождащо: 12 -> 12тъпо: 12 -> 18огън: 12 -> 12отрова: 12 -> 12Тъпото (боздуганът, тоягата) разбива кости, а мечът се плъзга по тях. Затова не всяко оръжие върши работа срещу всеки враг. В таблицата на враговете от данните на играта за това има две колони: resist и weak. След щетата играта проверява вида на удара и слага ефект:
def after_hit(battle, target, damage_type, crit): if not target.is_alive(): return rng = battle.game.rng if damage_type == 'slash' and (crit or rng.chance(35)): apply_effect(target, make_effect('bleed', 3, 1), battle) elif damage_type == 'blunt' and rng.chance(10): apply_effect(target, make_effect('stun', 1, 0), battle) elif damage_type == 'fire' and rng.chance(30): apply_effect(target, make_effect('burn', 3, 2), battle) elif damage_type == 'poison': apply_effect(target, make_effect('poison', 3, 2), battle)В канона resist и weak на врага са списъци от видове (скелетът е устойчив на режещо, големът на пробождащо и огън), а не по един вид. Тук е по един низ, за да е кратък кодът.
Идеята
Думата вместо числото
Дневникът на боя не пише „щета 12“. Пише „Ива смазва вълка (12)“. Думата за силата на удара зависи не от числото, а от дела от живота на целта, който отнема:
def percent(a, b): return a * 100 // b
def damage_word(damage, max_hp): share = percent(damage, max_hp) if share < 10: return "драска" if share < 25: return "удря" if share < 45: return "разсича" return "смазва"for damage in [1, 2, 4, 5, 8, 9, 10, 12]: print(f"{damage} от 20: {percent(damage, 20)}%, {damage_word(damage, 20)}")1 от 20: 5%, драска2 от 20: 10%, удря4 от 20: 20%, удря5 от 20: 25%, разсича8 от 20: 40%, разсича9 от 20: 45%, смазва10 от 20: 50%, смазва12 от 20: 60%, смазваПод 10% е драскотина, под 25% удар, под 45% разрез, а над това смазване. Така 12 щета са смазване за вълк с 20 живот, а за героя с 48 са само разрез. Играчът разбира, че ударът е тежък, без да сравнява числа. Ето функцията в играта (имената на думите са ключове към текстовете на четирите езика):
# Думата за силата на удара зависи от това каква част от живота на целта отнема.def damage_word(damage, max_hp): share = percent(damage, max_hp) if share < 10: return t('verb.scratch') if share < 25: return t('verb.hit') if share < 45: return t('verb.cut') return t('verb.crush')Виж го
Целият бой
Сега всичко се събира. Ударът проверява точността, хвърля щета и критичен, вади защитата, прилага устойчивостта, не позволява под 1, казва го с дума и намалява живота:
def strike(battle, dice, attacker, target): hit_chance = clamp(attacker.accuracy() - target.evasion(), 10, 95) if dice.range(1, 100) > hit_chance: battle.say(f"{attacker.name} пропуска {target.name}.") return False damage = dice.range(attacker.weapon_min, attacker.weapon_max) + attacker.strength crit_chance = attacker.crit_chance() if attacker.kind == "pierce": crit_chance += 10 crit = dice.chance(crit_chance) if crit: damage *= 2 damage -= target.defense damage = resisted(target, attacker.kind, damage) if damage < 1: damage = 1 line = f"{attacker.name} {damage_word(damage, target.max_hp)} {target.name} ({damage})." if crit: line += " Кръвта плисва!" battle.say(line) target.hp = max(0, target.hp - damage) return TrueБитката е цикъл: nextActor, плащане на 100, ударът на героя към първия жив враг или на врага към героя, дума за падането, край, когато единият отбор не е жив. Героят (Ива) срещу вълк и гоблин, със семе 16:
dice = Random(16)hero = Fighter("Ива", 48, 7, 4, 3, 10, 4, 7, "slash", "", "")wolf = Fighter("Вълк кръвник", 20, 3, 8, 1, 14, 2, 5, "slash", "", "")goblin = Fighter("Гоблин копиеносец", 16, 2, 5, 1, 10, 2, 5, "pierce", "", "")battle = Battle([hero, wolf, goblin])battle.say("Бой! Срещу теб: " + wolf.name + ", " + goblin.name + ".")while True: actor = battle.next_actor() actor.energy -= 100 target = battle.participants()[1] if actor is hero else hero strike(battle, dice, actor, target) if not target.is_alive(): battle.say(target.name + (" пада. Мината поглъща светлината." if target is hero else " пада и не става.")) if not hero.is_alive() or len(battle.participants()) == 1: breakbattle.say(("Победа!" if hero.is_alive() else "Загуба.") + " " + hero.name + f": живот {hero.hp}/{hero.max_hp}")for line in battle.log: print(line)Бой! Срещу теб: Вълк кръвник, Гоблин копиеносец.Вълк кръвник драска Ива (4).Ива смазва Вълк кръвник (13).Гоблин копиеносец пропуска Ива.Вълк кръвник удря Ива (7). Кръвта плисва!Ива смазва Вълк кръвник (13).Вълк кръвник пада и не става.Гоблин копиеносец пропуска Ива.Ива смазва Гоблин копиеносец (10).Гоблин копиеносец драска Ива (2).Ива смазва Гоблин копиеносец (11).Гоблин копиеносец пада и не става.Победа! Ива: живот 35/48В дневника има 2 пропуска и 1 критичен удар: вълкът удря героя за 7, повече от обикновените 2–5, а в края се казва, че кръвта плисва. Дневникът е списък от редове. Екранът на боя рисува последните от тях:
def draw(self, buffer): battle = self.battle hero = battle.hero buffer.text(0, 0, '=== ' + t('battle.title') + ' ===', 'yellow') buffer.text(0, 1, hero.name + ', ' + nm(hero.hero_class()) + ' ' + t('hud.level', {'level': hero.level}), 'white') buffer.text(2, 2, t('hud.hp'), 'red') buffer.bar(4, 2, 12, hero.hp, hero.max_hp(), 'red') buffer.text(19, 2, str(hero.hp) + '/' + str(hero.max_hp()), 'default') buffer.text(27, 2, t('hud.mana'), 'blue') buffer.bar(29, 2, 8, hero.mana, hero.max_mana(), 'blue') buffer.text(40, 2, str(hero.mana) + '/' + str(hero.max_mana()), 'default') # Редовете се редят плътно един след друг: героят, враговете, дневникът, командите. row = 3 hero_effects = effect_labels(hero) if len(hero_effects) > 0: buffer.text(2, row, hero_effects, 'green') row += 1 enemies = battle.alive_enemies() i = 0 while i < len(enemies) and i < 4: enemy = enemies[i] buffer.text(0, row, str(i + 1) + ') ', 'default') buffer.text(3, row, pad(cap(enemy.name), 22), enemy.kind.color) buffer.bar(25, row, 8, enemy.hp, enemy.max_hp(), 'red') buffer.text(36, row, str(enemy.hp) + '/' + str(enemy.max_hp()), 'default') buffer.text(44, row, effect_labels(enemy), 'green') row += 1 i += 1 log_rows = 7 first = max(0, len(battle.log) - log_rows) for i in range(first, len(battle.log)): buffer.text(1, row, battle.log[i], log_color(battle.log[i])) row += 1 if battle.over: buffer.text(0, row, t('battle.won' if battle.outcome == 'win' else 'battle.away' if battle.outcome == 'fled' else 'battle.lost'), 'yellow') else: buffer.text(0, row, self.note if len(self.note) > 0 else t('battle.commands'), 'red' if len(self.note) > 0 else 'white')И самият удар в играта е същият, с две разлики: ударът има сила (power), с която уменията удрят по-силно от обикновения удар, а след щетата идва afterHit, който слага ефекти.
def strike(battle, attacker, target, profile, power): rng = battle.game.rng hit_chance = clamp(attacker.accuracy() - target.evasion(), 10, 95) if rng.range(1, 100) > hit_chance: battle.say(t('combat.miss', {'attacker': attacker.name, 'target': target.name})) return False damage = rng.range(profile.min, profile.max) + attacker.stat('strength') damage = (damage * power) // 100 crit_chance = attacker.crit_chance() if profile.type == 'pierce': crit_chance += 10 crit = rng.chance(crit_chance) if crit: damage *= 2 damage -= target.stat('defense') damage = resisted(target, profile.type, damage) if damage < 1: damage = 1 line = t('combat.hit', { 'attacker': attacker.name, 'verb': damage_word(damage, target.max_hp()), 'target': target.name, 'damage': damage, }) if crit: line += t('combat.critBone' if not target.is_hero() and target.is_bloodless() else 'combat.critBlood') battle.say(line) deal_damage(battle, target, damage) after_hit(battle, target, profile.type, crit) return TrueПо-точно, за любопитните
Критичният удар е ×2, но боецът с копие (пробождащо) има още 10% шанс за него: в канона пробождащото е „оръжието на критичните удари“. Тук това е един ред: critChance += 10.
Заклинанията в играта винаги улучват и защитата не ги спира (spellHit): те се броят само с бонус на заклинателя и вид щета. Затова вещица с огнена топка и варварин с брадва са различен избор, а не по-слабата и по-силната версия на един и същ воин.
Пробвай
Предвиди изхода
Рицар и магьосник имат бързина 10, стрелецът 15. Списъкът е в този ред: рицар, магьосник, стрелец. Какво печата програмата?
battle = Battle([ Fighter("Рицар", 30, 5, 5, 1, 10, 3, 6, "slash", "", ""), Fighter("Магьосник", 30, 5, 5, 1, 10, 3, 6, "slash", "", ""), Fighter("Стрелец", 30, 5, 5, 1, 15, 3, 6, "slash", "", ""),])order = []for _ in range(8): actor = battle.next_actor() actor.energy -= 100 order.append(actor.name)print(", ".join(order))Стрелецът (бързина 15) стига 100 пръв, на тик 7 (105), и действа. Рицарят и магьосникът (10) стигат 100 заедно на тик 10: при равенство действа по-ранният в списъка, затова рицарят е преди магьосника. После стрелецът на тик 14 (5 + 15 × 7 = 110), а на тик 20 и тримата са на 100 и действат по реда на списъка. Втората възможност е с обратна подредба при равенство (магьосникът преди рицаря: сравнение >= вместо >). Третата е по кръгове, както в началото на урока, без да се гледа бързината.
Пробвай
Намери грешката
Колега реши, че след хода енергията трябва да е нула, и пипна един ред. Ето какво печата програмата сега:
hero = Fighter("Ива", 48, 7, 4, 3, 10, 4, 7, "slash", "", "")wolf = Fighter("Вълк кръвник", 20, 3, 8, 1, 14, 2, 5, "slash", "", "")goblin = Fighter("Гоблин копиеносец", 16, 2, 5, 1, 10, 2, 5, "pierce", "", "")battle = Battle([hero, wolf, goblin])for _ in range(7): actor = battle.next_actor() print(f"Тик {battle.ticks}: {actor.name} действа (енергия {actor.energy}).") actor.energy = 0Тик 8: Вълк кръвник действа (енергия 112).Тик 10: Ива действа (енергия 100).Тик 10: Гоблин копиеносец действа (енергия 100).Тик 16: Вълк кръвник действа (енергия 112).Тик 20: Ива действа (енергия 100).Тик 20: Гоблин копиеносец действа (енергия 100).Тик 24: Вълк кръвник действа (енергия 112).Вълкът действа на тик 8 и после на тик 16, не на 15, както в урока. Кой ред е виновен?
Избери реда с грешката.
actor.energy = 0 изхвърля всичко над 100. Вълкът е на 112 и остатъкът 12 се губи, затова всеки ход му струва цели 8 тика и бързината 14 е същата като 13. Правилното е actor.energy -= 100: плаща се точно 100, а останалото се пази.
По-точно
Какво не е в тази система
Опашката по енергия има малко правила, но зад нея има избори.
Всеки ход струва 100. Бързо действие (глътка от отвара) би могло да струва 50, а тежко (силно умение) 150. Тогава цената е свойство на действието и ходът плаща energy -= cost. Така балансът се настройва по-фино, но играчът трябва да вижда последствията, а за това му трябва опашката на екрана.
Опашката не се вижда. Екранът на боя в канона не показва кой е следващият. Показването следва от същото правило: симулираш nextActor върху копие на бойците и пишеш имената. Не го правим, защото тогава играчът планира по-дълбоко и боят става по-бавен за игра.
Затягането на шанса е решение за играта. С 10–95 всеки бой има случайност. По-тясно, например 30–70, е по-шумен бой, по-широко, например 1–99, е по-предвидим. Числата не са „правилни“: настройват се със симулация (последният урок на модула).
Критичният удар не бива да расте бързо. Двойната щета е силна, затова шансът е 5% + ловкост / 10, а не ловкост. При растеж с 1% на десет точки ловкостта не превръща боя в лотария.
Заровете са отделен вход. strike не търси случайно число сам, а получава генератора. Затова боят със семе е повторяем: с него се пише тест, който проверява цял бой байт по байт, и се прави запис на боя (урокът за действията).
Упражнение
Опашка за запис
Реши задачата в редактора си.
Опашка за запис. Програмата чете от входа на първия ред броя ходове N, а после по един боец на ред във вида име бързина. Бойците са в реда, в който са записани (това е редът при равенство). Програмата печата първите N хода като в урока: тик, име и енергията на бойеца, преди да плати 100.
С вход 10 | Голем 7 | Магьосник 10 | Стрелец 15 (по един на ред) изходът е:
Тик 7: Стрелец действа (енергия 105).Тик 10: Магьосник действа (енергия 100).Тик 14: Стрелец действа (енергия 110).Тик 15: Голем действа (енергия 105).Тик 20: Магьосник действа (енергия 100).Тик 20: Стрелец действа (енергия 100).Тик 27: Стрелец действа (енергия 105).Тик 29: Голем действа (енергия 103).Тик 30: Магьосник действа (енергия 100).Тик 34: Стрелец действа (енергия 110).Внимавай на тик 20: магьосникът и стрелецът са на 100 заедно и действа по-ранният в списъка. Внимавай и на тик 14: стрелецът действа пак с 110, защото остатъкът от първия му ход се пренася.
Още задачи
- Нулева бързина. Подай на боец бързина 0 и −3. Какво ще стане без
max(1, speed)? А със затягането? - Опашката на екрана. Напиши
preview(battle, count), която връща имената на следващитеcountучастници, без да променя бойците (работи върху копие). - Колко боя печели героят? Пусни боя от урока със семена от 1 до 1000 и преброй победите. Колко от тях имат критичен удар? Това е първата стъпка към симулацията от последния урок.
Тест
Провери се
Свързани уроци
Раздел „Свързани уроци“- Нужно отпреди: Класове и обекти · Конструктори и self · Методи на обекта · Списъци: append, remove и sort
- Задачи по темата: Хрониките на Арканум