/
GraphTreeHeap
/
NeuroFighter
Обзор
Документация
Войти
/
GraphTreeHeap
/
NeuroFighter
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
Аналитика
Безопасность
master
src/mechanics.py
536 строк
23 KB
FedMam
moving to the new project version
11 окт 2025, 04:16
11 окт 2025, 04:16
00409b0
Код
Авторство
О чём код?
import math ROOM_WIDTH = 256 ROOM_HEIGHT = 128 SPAWN_POS = 48 CHR_HITBOX_SIZE = 16 CHR_HITBOX_HALFSIZE = CHR_HITBOX_SIZE // 2 FPS = 16 BATTLE_TIME = 180 * FPS N_BUTTONS = 4 SHOW_K_PROJECTILES = 5 class RewardSystem: def __init__(self, every_frame_penalty: float, deal_1_damage_reward: float, take_1_damage_penalty: float, win_reward: float, lose_penalty: float, time_up_penalty: float, movement_reward: float, # * distance close_distance_reward: float, # * (1 - distance / room_width) dodging_reward: float, # * damage / max_damage opponent_dodges_penalty: float, # * damage / max_damage * cooldown / max_cooldown shooting_in_wrong_direction_penalty: float): # * damage / max_damage; shooting in not the same direction as the opponent self.every_frame_penalty = every_frame_penalty self.deal_1_damage_reward = deal_1_damage_reward self.take_1_damage_penalty = take_1_damage_penalty self.win_reward = win_reward self.lose_penalty = lose_penalty self.time_up_penalty = time_up_penalty self.movement_reward = movement_reward self.close_distance_reward = close_distance_reward self.dodging_reward = dodging_reward self.opponent_dodges_penalty = opponent_dodges_penalty self.shooting_in_wrong_direction_penalty = shooting_in_wrong_direction_penalty REWARD_SYSTEM_OBJECTIVE = RewardSystem( every_frame_penalty = -0.01, deal_1_damage_reward = 0.1, take_1_damage_penalty = -0.1, win_reward = 10.0, lose_penalty = -10.0, time_up_penalty = -10.0, movement_reward = 0.0, close_distance_reward = 0.0, dodging_reward = 0.0, opponent_dodges_penalty = 0.0, shooting_in_wrong_direction_penalty = 0.0 ) REWARD_SYSTEM_DEFAULT = RewardSystem( every_frame_penalty = -0.01, deal_1_damage_reward = 0.1, take_1_damage_penalty = -0.08, win_reward = 10.0, lose_penalty = -10.0, time_up_penalty = -10.0, movement_reward = 0.0, close_distance_reward = 0.0002, dodging_reward = 0.2, opponent_dodges_penalty = -0.1, shooting_in_wrong_direction_penalty = -0.02 ) REWARD_SYSTEM_AGGRESSIVE = RewardSystem( every_frame_penalty = -0.015, deal_1_damage_reward = 0.2, take_1_damage_penalty = -0.08, win_reward = 10.0, lose_penalty = -6.0, time_up_penalty = -10.0, movement_reward = 0.0001, close_distance_reward = 0.0008, dodging_reward = 0.1, opponent_dodges_penalty = -0.3, shooting_in_wrong_direction_penalty = -0.08 ) REWARD_SYSTEM_DEFENSIVE = RewardSystem( every_frame_penalty = -0.005, deal_1_damage_reward = 0.1, take_1_damage_penalty = -0.2, win_reward = 6.0, lose_penalty = -10.0, time_up_penalty = -10.0, movement_reward = 0.0, close_distance_reward = 0.0001, dodging_reward = 0.4, opponent_dodges_penalty = -0.05, shooting_in_wrong_direction_penalty = -0.01 ) class Projectile: def __init__(self, init_pos: tuple[int, int], h_speed: int, damage: int): self.pos = init_pos self.h_speed = h_speed self.damage = damage def step(self): self.pos = (self.pos[0] + self.h_speed, self.pos[1]) # buttons: # 0 - left # 1 - right # 2 - jump # 3 - shoot class Character: def __init__(self, init_pos: tuple[int, int], facing_right: bool, character_id: int, name: str, h_speed: int, v_speed: int, jump_height: int, hp: int, shoot_damage: int, shoot_cooldown: int, shoot_speed: int, color: tuple[int, int, int]): self.character_id = character_id self.name = name self.pos = init_pos self.facing_right = facing_right self.h_speed = h_speed self.v_speed = v_speed self.jump_height = jump_height self.jumping = False self.max_hp = hp self.hp = hp self.shoot_damage = shoot_damage self.shoot_cooldown = shoot_cooldown self.shoot_speed = shoot_speed self.shoot_pressed = False self.shoot_cooldown_ctr = 0 self.color = color self.buttons_held = [False] * N_BUTTONS def step(self) -> Projectile | None: # first, facing... if self.buttons_held[0] and not self.buttons_held[1]: # left self.facing_right = False elif self.buttons_held[1] and not self.buttons_held[0]: # right self.facing_right = True # then, shooting... projectile = None if self.buttons_held[3]: self.shoot_pressed = True if self.shoot_cooldown_ctr == 0: self.shoot_cooldown_ctr = self.shoot_cooldown projectile = Projectile(self.pos, self.shoot_speed * (1 if self.facing_right else -1), self.shoot_damage) self.shoot_cooldown_ctr = max(0, self.shoot_cooldown_ctr - 1) # then, horiz movement... if self.buttons_held[0] and not self.buttons_held[1]: # left self.pos = (self.pos[0] - self.h_speed, self.pos[1]) elif self.buttons_held[1] and not self.buttons_held[0]: # right self.pos = (self.pos[0] + self.h_speed, self.pos[1]) self.pos = (max(0, min(ROOM_WIDTH, self.pos[0])), self.pos[1]) # finally, vert movement if self.buttons_held[2]: if self.pos[1] == 0: self.jumping = True else: self.jumping = False if self.jumping or self.pos[1] > 0: if self.pos[1] >= self.jump_height: self.jumping = False if self.jumping: self.pos = (self.pos[0], self.pos[1] + self.v_speed) else: self.pos = (self.pos[0], max(0, self.pos[1] - self.v_speed)) return projectile class CharacterDescription: def __init__(self, character_id: int, name: str, h_speed: int, v_speed: int, jump_height: int, hp: int, shoot_damage: int, shoot_cooldown: int, shoot_speed: int, color: tuple[int, int, int]): self.character_id = character_id self.name = name self.h_speed = h_speed self.v_speed = v_speed self.jump_height = jump_height self.hp = hp self.shoot_damage = shoot_damage self.shoot_cooldown = shoot_cooldown self.shoot_speed = shoot_speed self.color = color def raw(self): return (self.character_id, self.name, self.h_speed, self.v_speed, self.jump_height, self.hp, self.shoot_damage, self.shoot_cooldown, self.shoot_speed, self.color) CHARACTERS: list[CharacterDescription] = [ CharacterDescription(0, 'Zhan Shi', 5, 8, 4*16, 100, 6, 12, 8, [0xff, 0x00, 0x00]), CharacterDescription(1, 'Vortex', 3, 8, 6*16, 120, 4, 8, 16, [0x00, 0x00, 0xff]), CharacterDescription(2, 'Murasaki', 8, 4, 6*16, 80, 8, 16, 4, [0x80, 0x00, 0x80]), CharacterDescription(3, 'Cobra', 4, 16, 8*16, 80, 3, 4, 12, [0xff, 0x00, 0x80]), CharacterDescription(4, 'Raio', 6, 12, 3*16, 100, 25, 64, 4, [0xff, 0xff, 0x00]), CharacterDescription(5, 'Metelitsa', 6, 8, 4*16, 100, 4, 8, 4, [0x00, 0xff, 0xff]), CharacterDescription(6, 'Ironclad', 2, 4, 3*16, 200, 6, 12, 12, [0xc0, 0xc0, 0xc0]), CharacterDescription(7, 'Raptor', 8, 8, 8*16, 120, 1, 2, 14, [0x00, 0xc0, 0x00]), CharacterDescription(8, 'Tecolotl', 6, 6, 8*16, 120, 10, 32, 16, [0xc0, 0x60, 0x00]), CharacterDescription(9, 'Brise', 8, 6, 8*16, 80, 5, 8, 16, [0xff, 0x80, 0xc0]) ] BOSS_CHARACTERS: list[CharacterDescription] = [ CharacterDescription(10, 'Dreadnought', 5, 6, 5*16, 120, 3, 6, 16, [0x40, 0x40, 0x40]), CharacterDescription(11, 'Crusher', 5, 4, 4*16, 150, 25, 24, 8, [0xff, 0x80, 0x00]), CharacterDescription(12, 'Dr.Oxide', 3, 6, 8*16, 100, 8, 10, 3, [0x00, 0xff, 0x00]), CharacterDescription(13, 'The Agent', 6, 8, 6*16, 180, 12, 16, 10, [0xc0, 0x80, 0xff]) ] CHARACTER_MAX_H_SPEED = max([char.h_speed for char in CHARACTERS]) CHARACTER_MAX_V_SPEED = max([char.v_speed for char in CHARACTERS]) CHARACTER_MAX_JUMP_HEIGHT = max([char.jump_height for char in CHARACTERS]) CHARACTER_MAX_HP = max([char.hp for char in CHARACTERS]) CHARACTER_MAX_SHOOT_DAMAGE = max([char.shoot_damage for char in CHARACTERS]) CHARACTER_MAX_SHOOT_COOLDOWN = max([char.shoot_cooldown for char in CHARACTERS]) CHARACTER_MAX_SHOOT_SPEED = max([char.shoot_speed for char in CHARACTERS]) # rectangle: left, bottom, width, height # point: X, Y def rectangle_contains(rectangle: tuple[int, int, int, int], point: tuple[int, int]): return rectangle[0] <= point[0] <= rectangle[0] + rectangle[2] and \ rectangle[1] <= point[1] <= rectangle[1] + rectangle[3] def rectangle_intersects(rect1: tuple[int, int, int, int], rect2: tuple[int, int, int, int]) -> bool: if rect1[0] + rect1[2] < rect2[0] or rect2[0] + rect2[2] < rect1[0]: return False if rect1[1] + rect1[3] < rect2[1] or rect2[1] + rect2[3] < rect1[1]: return False return True def get_closest_projectiles(proj_list: list[Projectile], k: int, pos: tuple[int, int]): return [proj for _, _, proj in sorted([((proj.pos[0] - pos[0]) ** 2 + (proj.pos[1] - pos[1]) ** 2, i, proj) for i, proj in enumerate(proj_list)])[:k]] class Environment: def __init__(self, player_desc: CharacterDescription, opponent_desc: CharacterDescription, reward_system: RewardSystem=REWARD_SYSTEM_DEFAULT): self.player_desc = player_desc self.opponent_desc = opponent_desc self.reward_system = reward_system self.reset() def reset(self): self.player = Character((SPAWN_POS, 0), True, *self.player_desc.raw()) self.opponent = Character((ROOM_WIDTH - SPAWN_POS, 0), False, *self.opponent_desc.raw()) self.player_projectiles = [] self.opponent_projectiles = [] # This reflects which projectiles will be included into the state representaion # and at which indices (so that the bullets don't change indices). self.player_projectile_state_queue = [None] * SHOW_K_PROJECTILES self.opponent_projectile_state_queue = [None] * SHOW_K_PROJECTILES self.total_player_reward = 0 self.total_opponent_reward = 0 self.time_left = BATTLE_TIME def step(self): player_reward = self.reward_system.every_frame_penalty opponent_reward = self.reward_system.every_frame_penalty player_x_before = self.player.pos[0] player_proj = self.player.step() if player_proj is not None: if (self.opponent.pos[0] > self.player.pos[0] and not self.player.facing_right) or \ (self.opponent.pos[0] < self.player.pos[0] and self.player.facing_right): player_reward += self.reward_system.shooting_in_wrong_direction_penalty * player_proj.damage / CHARACTER_MAX_SHOOT_DAMAGE self.player_projectiles.append(player_proj) player_reward += self.reward_system.movement_reward * abs(self.player.pos[0] - player_x_before) player_reward += self.reward_system.close_distance_reward * (1 - abs(self.player.pos[0] - self.opponent.pos[0]) / ROOM_WIDTH) new_player_projs = [] for proj in self.player_projectiles: # first check, and then step # the last condition checks that the bullet doesn't fly through the character if rectangle_contains((self.opponent.pos[0] - CHR_HITBOX_HALFSIZE, self.opponent.pos[1] - CHR_HITBOX_HALFSIZE, CHR_HITBOX_SIZE, CHR_HITBOX_SIZE), proj.pos) or \ (self.opponent.pos[1] - CHR_HITBOX_HALFSIZE <= proj.pos[1] <= self.opponent.pos[1] + CHR_HITBOX_HALFSIZE and (proj.pos[0] >= self.opponent.pos[0]) != (proj.pos[0] + proj.h_speed >= self.opponent.pos[0])): self.opponent.hp -= proj.damage player_reward += self.reward_system.deal_1_damage_reward * proj.damage opponent_reward += self.reward_system.take_1_damage_penalty * proj.damage elif proj.pos[0] >= 0 and proj.pos[0] <= ROOM_WIDTH: new_player_projs.append(proj) else: # projectile dodged opponent_reward += self.reward_system.dodging_reward * proj.damage / CHARACTER_MAX_SHOOT_DAMAGE player_reward += self.reward_system.opponent_dodges_penalty * proj.damage / CHARACTER_MAX_SHOOT_DAMAGE * self.player.shoot_cooldown / CHARACTER_MAX_SHOOT_COOLDOWN proj.step() self.player_projectiles = new_player_projs opponent_x_before = self.opponent.pos[0] opponent_proj = self.opponent.step() if opponent_proj is not None: if (self.player.pos[0] > self.opponent.pos[0] and not self.opponent.facing_right) or \ (self.player.pos[0] < self.opponent.pos[0] and self.opponent.facing_right): opponent_reward += self.reward_system.shooting_in_wrong_direction_penalty * opponent_proj.damage / CHARACTER_MAX_SHOOT_DAMAGE self.opponent_projectiles.append(opponent_proj) opponent_reward += self.reward_system.movement_reward * abs(self.opponent.pos[0] - opponent_x_before) opponent_reward += self.reward_system.close_distance_reward * (1 - abs(self.opponent.pos[0] - self.player.pos[0]) / ROOM_WIDTH) new_opponent_projs = [] for proj in self.opponent_projectiles: if rectangle_contains((self.player.pos[0] - CHR_HITBOX_HALFSIZE, self.player.pos[1] - CHR_HITBOX_HALFSIZE, CHR_HITBOX_SIZE, CHR_HITBOX_SIZE), proj.pos) or \ (self.player.pos[1] - CHR_HITBOX_HALFSIZE <= proj.pos[1] <= self.player.pos[1] + CHR_HITBOX_HALFSIZE and (proj.pos[0] >= self.player.pos[0]) != (proj.pos[0] + proj.h_speed >= self.player.pos[0])): self.player.hp -= proj.damage opponent_reward += self.reward_system.deal_1_damage_reward * proj.damage player_reward += self.reward_system.take_1_damage_penalty * proj.damage elif proj.pos[0] >= 0 and proj.pos[0] <= ROOM_WIDTH: new_opponent_projs.append(proj) else: # projectile dodged player_reward += self.reward_system.dodging_reward * proj.damage / CHARACTER_MAX_SHOOT_DAMAGE opponent_reward += self.reward_system.opponent_dodges_penalty * proj.damage / CHARACTER_MAX_SHOOT_DAMAGE * self.opponent.shoot_cooldown / CHARACTER_MAX_SHOOT_COOLDOWN proj.step() self.opponent_projectiles = new_opponent_projs # the state projectile boxes player_closest_projectiles = get_closest_projectiles(self.player_projectiles, SHOW_K_PROJECTILES, self.opponent.pos) opponent_closest_projectiles = get_closest_projectiles(self.opponent_projectiles, SHOW_K_PROJECTILES, self.player.pos) for i in range(SHOW_K_PROJECTILES): player_proj = self.player_projectile_state_queue[i] if player_proj not in player_closest_projectiles: self.player_projectile_state_queue[i] = None opponent_proj = self.opponent_projectile_state_queue[i] if opponent_proj not in opponent_closest_projectiles: self.opponent_projectile_state_queue[i] = None for player_proj in player_closest_projectiles: if player_proj not in self.player_projectile_state_queue: for i in range(SHOW_K_PROJECTILES): if self.player_projectile_state_queue[i] is None: self.player_projectile_state_queue[i] = player_proj break else: raise ValueError('Projectile state queue logic error!') for opponent_proj in opponent_closest_projectiles: if opponent_proj not in self.opponent_projectile_state_queue: for i in range(SHOW_K_PROJECTILES): if self.opponent_projectile_state_queue[i] is None: self.opponent_projectile_state_queue[i] = opponent_proj break else: raise ValueError('Projectile state queue logic error!') self.time_left -= 1 if self.player.hp <= 0: player_reward += self.reward_system.lose_penalty opponent_reward += self.reward_system.win_reward if self.opponent.hp <= 0: player_reward += self.reward_system.win_reward opponent_reward += self.reward_system.lose_penalty if self.time_left == 0: player_reward += self.reward_system.time_up_penalty opponent_reward += self.reward_system.time_up_penalty self.total_player_reward += player_reward self.total_opponent_reward += opponent_reward return player_reward, opponent_reward def done(self): return self.player.hp <= 0 or self.opponent.hp <= 0 or self.time_left <= 0 def get_state(self, player: bool, my_buttons: bool=True, my_state: bool=True, my_characteristics: bool=True, opp_state: bool=True, opp_characteristics: bool=True, my_projectiles: bool=True, opp_projectiles: bool=True): me = self.player if player else self.opponent enemy = self.opponent if player else self.player my_projs = self.player_projectiles if player else self.opponent_projectiles enemy_projs = self.opponent_projectiles if player else self.player_projectiles my_proj_state_queue = self.player_projectile_state_queue if player else self.opponent_projectile_state_queue enemy_proj_state_queue = self.opponent_projectile_state_queue if player else self.player_projectile_state_queue return (1. if player else 0., *([1. if me.buttons_held[i] else 0. for i in range(N_BUTTONS)] if my_buttons else []), *([me.pos[0] / ROOM_WIDTH, me.pos[1] / ROOM_HEIGHT, me.shoot_cooldown_ctr / me.shoot_cooldown, 1. if me.shoot_cooldown_ctr == 0 else 0., me.hp / me.max_hp] if my_state else []), *([me.h_speed / CHARACTER_MAX_H_SPEED, me.v_speed / CHARACTER_MAX_V_SPEED, me.jump_height / CHARACTER_MAX_JUMP_HEIGHT, me.max_hp / CHARACTER_MAX_HP, me.shoot_damage / CHARACTER_MAX_SHOOT_DAMAGE, me.shoot_cooldown / CHARACTER_MAX_SHOOT_COOLDOWN, me.shoot_speed / CHARACTER_MAX_SHOOT_SPEED,] if my_characteristics else []), *([1. if me.facing_right else -1., 1. if me.pos[1] == 0 else 0., # on ground 1. if me.jumping else 0., 1. if me.pos[0] == 0 else 0., # near the left wall 1. if me.pos[0] == ROOM_WIDTH else 0., # near the right wall ] if my_state else []), *([enemy.pos[0] / ROOM_WIDTH, (enemy.pos[0] - me.pos[0]) / ROOM_WIDTH, enemy.pos[1] / ROOM_HEIGHT, enemy.shoot_cooldown_ctr / enemy.shoot_cooldown, 1. if enemy.shoot_cooldown_ctr == 0 else 0., enemy.hp / enemy.max_hp] if opp_state else []), *([enemy.h_speed / CHARACTER_MAX_H_SPEED, enemy.v_speed / CHARACTER_MAX_V_SPEED, enemy.jump_height / CHARACTER_MAX_JUMP_HEIGHT, enemy.max_hp / CHARACTER_MAX_HP, enemy.shoot_damage / CHARACTER_MAX_SHOOT_DAMAGE, enemy.shoot_cooldown / CHARACTER_MAX_SHOOT_COOLDOWN, enemy.shoot_speed / CHARACTER_MAX_SHOOT_SPEED,] if opp_characteristics else []), *([1. if enemy.facing_right else -1., 1. if enemy.pos[1] == 0 else 0., # on ground 1. if enemy.jumping else 0., 1. if enemy.pos[0] == 0 else 0., # near the left wall 1. if enemy.pos[0] == ROOM_WIDTH else 0., # near the right wall ] if opp_state else []), *([arg for proj in my_proj_state_queue for arg in (((proj.pos[0] - me.pos[0]) / ROOM_WIDTH, (proj.pos[1] - me.pos[1]) / ROOM_HEIGHT, 1. if proj.h_speed > 0 else -1.) if proj is not None else (0., 0., 0.))] if my_projectiles else []), *([arg for proj in enemy_proj_state_queue for arg in (((proj.pos[0] - me.pos[0]) / ROOM_WIDTH, (proj.pos[1] - me.pos[1]) / ROOM_HEIGHT, 1. if proj.h_speed > 0 else -1.) if proj is not None else (0., 0., 0.))] if opp_projectiles else []), self.time_left / BATTLE_TIME) def get_current_action(self, player: bool, multi_discrete: bool=True): me = self.player if player else self.opponent if multi_discrete: return ({ (False, False): 0, (True, True): 0, (True, False): 1, (False, True): 2 }[tuple(me.buttons_held[:2])], 1 if me.buttons_held[2] else 0, 1 if me.buttons_held[3] else 0) else: return (1 if me.buttons_held[3] else 0) * 2 * 3 + \ (1 if me.buttons_held[2] else 0) * 3 + \ ({ (False, False): 0, (True, True): 0, (True, False): 1, (False, True): 2 }[tuple(me.buttons_held[:2])]) def take_action(self, player: bool, action: int | tuple): me = self.player if player else self.opponent if (isinstance(action, tuple) or isinstance(action, list)) and len(action) == 3: move, jump, shoot = action else: move = action % 3 jump = (action // 3) % 2 shoot = (action // 3) // 2 if move == 0: me.buttons_held[0] = me.buttons_held[1] = False elif move == 1: me.buttons_held[0] = True me.buttons_held[1] = False else: me.buttons_held[0] = False me.buttons_held[1] = True me.buttons_held[2] = jump == 1 me.buttons_held[3] = shoot == 1 PLAYER_CHARACTER_STATE_DIM = 10 ENEMY_CHARACTER_STATE_DIM = 11 CHARACTERISTICS_DIM = 7 PROJECTILE_STATE_DIM = 3 STATE_DIM = 1 + N_BUTTONS + PLAYER_CHARACTER_STATE_DIM + ENEMY_CHARACTER_STATE_DIM + 2 * CHARACTERISTICS_DIM + PROJECTILE_STATE_DIM * SHOW_K_PROJECTILES * 2 + 1 ACTION_DIM = (3, 2, 2) ACTION_DIM_S = 12