1
0
Fork 0
mirror of https://github.com/EDeev/pixel_gamble.git synced 2026-10-07 23:49:29 +03:00
pixel_gamble/game/pixelgamble/bot.py
Egor Deev 4ecb9257c8 Pixel Gamble 2.0: законченная игра на ресурсах Ninja Adventure
- 10 волн, 5 видов врагов со своим поведением и босс в три фазы
- меч, копьё и посох, 11 улучшений между волнами, опыт и уровни
- меню, пауза, настройки, итоги партии и рекорд, русский и английский
- мир 480×270 с масштабированием, интерфейс в 960×540, скорости от dt
- асинхронный цикл: та же игра в браузере через pygbag
- тесты без окна, автоигрок, CI, релизы exe/Linux, GitHub Pages
- README на двух языках; исходная версия — релиз v1.0.0
2026-10-07 12:29:41 +00:00

92 lines
4 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Автоигрок для проверок без окна (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