Ruby-Cogs/pokemonduel/trainer.py
2025-04-02 22:57:51 -04:00

251 lines
11 KiB
Python

import discord
import asyncio
import random
from .enums import Ability, DamageClass, ElementType
from .misc import ExpiringEffect, ExpiringWish, ExpiringItem
from .move import Move
class Trainer():
"""
Represents a genereric pokemon trainer.
This class outlines the methods that Trainer objects
should have, but should not be used directly.
"""
def __init__(self, name: str, party: list):
self.name = name
self.party = party
self.current_pokemon = party[0] if len(party) > 0 else None
for poke in self.party:
poke.owner = self
self.event = asyncio.Event()
self.selected_action = None
#Boolean - True if this trainer's pokemon was removed in such a way that it needs to return mid-turn.
self.mid_turn_remove = False
#Optional[BatonPass] - Holds data baton passed from the previous pokemon to the next, if applicable.
self.baton_pass = None
#Int - Stacks of spikes on this trainer's side of the field
self.spikes = 0
#Int - Stacks of toxic spikes on this trainer's side of the field
self.toxic_spikes = 0
#Boolean - Whether stealth rocks are on this trainer's side of the field
self.stealth_rock = False
#Boolean - Whether a sticky web is on this trainer's side of the field
self.sticky_web = False
#Int - The last index of self.party that was selected
self.last_idx = 0
self.wish = ExpiringWish()
self.aurora_veil = ExpiringEffect(0)
self.light_screen = ExpiringEffect(0)
self.reflect = ExpiringEffect(0)
self.mist = ExpiringEffect(0)
#ExpiringEffect - Stores the number of turns that pokes are protected from NV effects
self.safeguard = ExpiringEffect(0)
#Boolean - Whether the next poke to swap in should be restored via healing wish
self.healing_wish = False
#Boolean - Whether the next poke to swap in should be restored via lunar dance
self.lunar_dance = False
#ExpiringEffect - Stores the number of turns that pokes have doubled speed
self.tailwind = ExpiringEffect(0)
#ExpiringEffect - Stores the number of turns that electric moves have 1/3 power
self.mud_sport = ExpiringEffect(0)
#ExpiringEffect - Stores the number of turns that fire moves have 1/3 power
self.water_sport = ExpiringEffect(0)
#ExpiringEffect - Stores the fact that a party member recently fainted.
self.retaliate = ExpiringEffect(0)
#ExpiringItem - Stores the turns until future sight attacks this trainer's pokemon.
self.future_sight = ExpiringItem()
#Boolean - Whether or not any of this trainer's pokemon have mega evolved yet this battle.
self.has_mega_evolved = False
#Int - Stores the number of times a pokemon in this trainer's party has fainted, including after being revived.
self.num_fainted = 0
#Int - Stores the HP of the subsitute this trainer's next pokemon on the field will receive.
self.next_substitute = 0
def has_alive_pokemon(self) -> bool:
"""Returns True if this trainer still has at least one pokemon that is alive."""
return any((poke.hp > 0 for poke in self.party))
def next_turn(self, battle):
"""
Updates this trainer for a new turn.
Returns a formatted message.
"""
msg = ""
self.selected_action = None
self.mid_turn_remove = False
hp = self.wish.next_turn()
if hp and self.current_pokemon is not None:
msg += self.current_pokemon.heal(hp, source="its wish")
if self.aurora_veil.next_turn():
msg += f"{self.name}'s aurora veil wore off!\n"
if self.light_screen.next_turn():
msg += f"{self.name}'s light screen wore off!\n"
if self.reflect.next_turn():
msg += f"{self.name}'s reflect wore off!\n"
if self.mist.next_turn():
msg += f"{self.name}'s mist wore off!\n"
if self.safeguard.next_turn():
msg += f"{self.name}'s safeguard wore off!\n"
if self.tailwind.next_turn():
msg += f"{self.name}'s tailwind died down!\n"
if self.mud_sport.next_turn():
msg += f"{self.name}'s mud sport wore off!\n"
if self.water_sport.next_turn():
msg += f"{self.name}'s water sport evaporated!\n"
self.retaliate.next_turn()
future_sight_data = self.future_sight.item
if self.future_sight.next_turn() and self.current_pokemon is not None:
msg += f"{self.current_pokemon.name} took the future sight attack!\n"
future_sight_attacker, future_sight_move = future_sight_data
msgadd, _ = future_sight_move.attack(future_sight_attacker, self.current_pokemon, battle)
msg += msgadd
return msg
def switch_poke(self, slot: int, *, mid_turn=False):
"""Switch the currently active poke to the given slot."""
if slot < 0 or slot >= len(self.party):
raise ValueError("out of bounds")
if not self.party[slot].hp > 0:
raise ValueError("no hp")
self.current_pokemon = self.party[slot]
self.mid_turn_remove = False
self.last_idx = slot
if mid_turn:
self.current_pokemon.swapped_in = True
def is_human(self):
"""Returns True if this trainer is a human player, False if it is an AI."""
raise NotImplementedError()
def valid_swaps(self, defender, battle, *, check_trap=True):
"""Returns a list of indexes of pokes in the party that can be swapped to."""
if self.current_pokemon is not None:
if ElementType.GHOST in self.current_pokemon.type_ids:
check_trap = False
if self.current_pokemon.held_item == "shed-shell":
check_trap = False
if check_trap:
if self.current_pokemon.trapping:
return []
if self.current_pokemon.ingrain:
return []
if self.current_pokemon.fairy_lock.active() or defender.fairy_lock.active():
return []
if self.current_pokemon.no_retreat:
return []
if self.current_pokemon.bind.active() and not self.current_pokemon.substitute:
return []
if defender.ability() == Ability.SHADOW_TAG and not self.current_pokemon.ability() == Ability.SHADOW_TAG:
return []
if defender.ability() == Ability.MAGNET_PULL and ElementType.STEEL in self.current_pokemon.type_ids:
return []
if defender.ability() == Ability.ARENA_TRAP and self.current_pokemon.grounded(battle):
return []
result = [idx for idx, poke in enumerate(self.party) if poke.hp > 0]
if self.last_idx in result:
result.remove(self.last_idx)
return result
def valid_moves(self, defender):
"""
https://www.smogon.com/dp/articles/move_restrictions
Returns
- ("forced", Move) - The move-action this trainer is FORCED to use.
- ("idxs", List[int]) - The indexes of moves that are valid to CHOOSE to use.
- ("struggle", List[int]) - If the user attempts to use any move, use struggle instead (no valid moves).
"""
# Check if they are FORCED to use a certain move
if self.current_pokemon.locked_move:
return ("forced", self.current_pokemon.locked_move.move)
# Remove all moves not matching a restriction
result = []
for idx, move in enumerate(self.current_pokemon.moves):
if move.pp <= 0:
continue
if move.damage_class == DamageClass.STATUS and self.current_pokemon.held_item == "assault-vest":
continue
if move.damage_class == DamageClass.STATUS and self.current_pokemon.taunt.active():
continue
if move.effect == 247 and not all(m.used for m in self.current_pokemon.moves if m.effect != 247):
continue
if self.current_pokemon.disable.active() and move is self.current_pokemon.disable.item:
continue
if self.current_pokemon.held_item in ("choice-scarf", "choice-band", "choice-specs") or self.current_pokemon.ability() == Ability.GORILLA_TACTICS:
if self.current_pokemon.choice_move is not None and move is not self.current_pokemon.choice_move:
continue
if self.current_pokemon.torment and self.current_pokemon.last_move is move:
continue
if (
self.current_pokemon.last_move is not None
and self.current_pokemon.last_move.effect == 492
and self.current_pokemon.last_move.id == move.id
and not self.current_pokemon.last_move_failed
):
continue
if defender.imprison and move.id in [x.id for x in defender.moves]:
continue
if self.current_pokemon.heal_block.active() and move.is_affected_by_heal_block():
continue
if self.current_pokemon.silenced.active() and move.is_sound_based():
continue
if move.effect == 339 and not self.current_pokemon.ate_berry:
continue
if move.effect == 453 and not self.current_pokemon.held_item.is_berry():
continue
if self.current_pokemon.encore.active() and move is not self.current_pokemon.encore.item:
continue
result.append(idx)
if not result:
return ("struggle", [0, 1, 2, 3])
return ("idxs", result)
def __repr__(self):
return f"{self.__class__.__name__}(name={self.name!r}, party={self.party!r})"
class MemberTrainer(Trainer):
"""
Represents a pokemon trainer that is a discord.Member.
"""
def __init__(self, member: discord.Member, party):
super().__init__(member.name, party)
self.id = member.id
self.member = member
def is_human(self):
"""Returns True if this trainer is a human player, False if it is an AI."""
return True
class NPCTrainer(Trainer):
"""
Represents a pokemon trainer that is a NPC.
"""
def __init__(self, party):
super().__init__("Trainer John", party)
def move(self, defender, battle):
"""Request a normal move from this trainer AI."""
status_code, movedata = self.valid_moves(defender)
if status_code == "forced":
self.selected_action = movedata
elif status_code == "struggle":
self.selected_action = Move.struggle()
else:
self.selected_action = self.current_pokemon.moves[random.choice(movedata)]
self.event.set()
#TODO: npc ai?
def swap(self, defender, battle, *, mid_turn=False):
"""Request a swap choice from this trainer AI."""
poke_idx = random.choice(self.valid_swaps(defender, battle, check_trap=False))
self.switch_poke(poke_idx, mid_turn=mid_turn)
self.event.set()
#TODO: npc ai?
def is_human(self):
"""Returns True if this trainer is a human player, False if it is an AI."""
return False