mirror of
https://github.com/EDeev/pixel_gamble.git
synced 2026-10-07 23:49:29 +03:00
- 10 волн, 5 видов врагов со своим поведением и босс в три фазы - меч, копьё и посох, 11 улучшений между волнами, опыт и уровни - меню, пауза, настройки, итоги партии и рекорд, русский и английский - мир 480×270 с масштабированием, интерфейс в 960×540, скорости от dt - асинхронный цикл: та же игра в браузере через pygbag - тесты без окна, автоигрок, CI, релизы exe/Linux, GitHub Pages - README на двух языках; исходная версия — релиз v1.0.0
92 lines
4 KiB
Python
92 lines
4 KiB
Python
"""Автоигрок для проверок без окна (CI) и записи GIF: не идеален, но доходит далеко."""
|
||
|
||
import random
|
||
|
||
from .app import MenuScene, PlayScene, ResultsScene
|
||
from .entity import Vector
|
||
from .player import WEAPONS
|
||
|
||
|
||
class Bot:
|
||
def __init__(self, seed=1, restart=False):
|
||
self.rng = random.Random(seed)
|
||
self.restart = restart
|
||
self.finished = [] # итоги сыгранных партий
|
||
self.stuck_time = 0.0
|
||
self.escape = Vector()
|
||
self.escape_time = 0.0
|
||
self.last_pos = None
|
||
self.last_foe, self.last_hp, self.no_progress = None, 0, 0.0
|
||
|
||
def drive(self, app):
|
||
scene = app.scene
|
||
if isinstance(scene, MenuScene):
|
||
app.new_game()
|
||
elif isinstance(scene, ResultsScene):
|
||
if not self.finished or self.finished[-1] is not scene:
|
||
self.finished.append(scene)
|
||
if self.restart:
|
||
app.new_game()
|
||
elif isinstance(scene, PlayScene) and scene.game.state == "upgrade":
|
||
scene.game.choose(self.pick(scene.game.offer))
|
||
|
||
def pick(self, offer):
|
||
order = ("wand", "heart", "damage", "haste", "heal", "lance", "multishot", "speed", "reach", "vampire",
|
||
"knockback")
|
||
return min(range(len(offer)), key=lambda i: order.index(offer[i]))
|
||
|
||
def controls(self, game, dt=1 / 60):
|
||
player = game.player
|
||
foes = [e for e in game.enemies if e.active]
|
||
move = Vector()
|
||
target = None
|
||
|
||
# здоровье на исходе — к ближайшему сердечку
|
||
if game.hearts and player.hp <= player.max_hp // 2:
|
||
heart = min(game.hearts, key=lambda h: h.pos.distance_squared_to(player.pos))
|
||
move = heart.pos - player.pos
|
||
|
||
if foes:
|
||
foe = min(foes, key=lambda e: e.pos.distance_squared_to(player.pos))
|
||
delta = foe.pos - player.pos
|
||
dist = delta.length()
|
||
ranged = WEAPONS[player.weapon]["kind"] == "bolt"
|
||
want = 70 if ranged else (20 if player.weapon == "sword" else 30)
|
||
if not move:
|
||
if dist > want + 6:
|
||
move = delta
|
||
elif dist < want - 12 or foe.boss:
|
||
move = -delta + Vector(-delta.y, delta.x) * 0.8
|
||
if dist < (130 if ranged else want + 14):
|
||
target = foe.pos
|
||
# стреляем, а урона нет (мешает камень) — подходим с другой стороны
|
||
if target is not None and foe is self.last_foe and foe.hp >= self.last_hp:
|
||
self.no_progress += dt
|
||
else:
|
||
self.no_progress = 0.0
|
||
self.last_foe, self.last_hp = foe, foe.hp
|
||
if self.no_progress > 2.5:
|
||
move = Vector(delta).rotate(70 * getattr(self, "side", 1))
|
||
if player.weapon != "wand" and "wand" in player.weapons and len(foes) > 3:
|
||
player.switch(index=player.weapons.index("wand"))
|
||
|
||
for shot in game.enemy_shots:
|
||
to_me = player.pos - shot.pos
|
||
if to_me.length_squared() < 40 ** 2 and shot.vel.dot(to_me) > 0:
|
||
move = Vector(-shot.vel.y, shot.vel.x)
|
||
break
|
||
|
||
# упёрся в дерево или камень — обходим вбок, каждый раз с другой стороны
|
||
if self.escape_time > 0:
|
||
self.escape_time -= dt
|
||
move = self.escape
|
||
elif move and self.last_pos is not None and player.pos.distance_squared_to(self.last_pos) < 0.05:
|
||
self.stuck_time += dt
|
||
if self.stuck_time > 0.25:
|
||
self.side = -getattr(self, "side", 1)
|
||
self.escape = Vector(move).rotate(80 * self.side)
|
||
self.escape_time, self.stuck_time = 0.6 + self.rng.random() * 0.4, 0.0
|
||
else:
|
||
self.stuck_time = 0.0
|
||
self.last_pos = Vector(player.pos)
|
||
return move, target is not None, target
|