.\PokeLLMon\poke_env\player\player.py
"""This module defines a base class for players.
"""
import asyncio
import random
from abc import ABC, abstractmethod
from asyncio import Condition, Event, Queue, Semaphore
from logging import Logger
from time import perf_counter
from typing import Any, Awaitable, Dict, List, Optional, Union
import orjson
from poke_env.concurrency import create_in_poke_loop, handle_threaded_coroutines
from poke_env.data import GenData, to_id_str
from poke_env.environment.abstract_battle import AbstractBattle
from poke_env.environment.battle import Battle
from poke_env.environment.double_battle import DoubleBattle
from poke_env.environment.move import Move
from poke_env.environment.pokemon import Pokemon
from poke_env.exceptions import ShowdownException
from poke_env.player.battle_order import (
BattleOrder,
DefaultBattleOrder,
DoubleBattleOrder,
)
from poke_env.ps_client import PSClient
from poke_env.ps_client.account_configuration import (
CONFIGURATION_FROM_PLAYER_COUNTER,
AccountConfiguration,
)
from poke_env.ps_client.server_configuration import (
LocalhostServerConfiguration,
ServerConfiguration,
)
from poke_env.teambuilder.constant_teambuilder import ConstantTeambuilder
from poke_env.teambuilder.teambuilder import Teambuilder
class Player(ABC):
"""
Base class for players.
"""
MESSAGES_TO_IGNORE = {"", "t:", "expire", "uhtmlchange"}
DEFAULT_CHOICE_CHANCE = 1 / 1000
def __init__(
self,
account_configuration: Optional[AccountConfiguration] = None,
*,
avatar: Optional[int] = None,
battle_format: str = "gen9randombattle",
log_level: Optional[int] = None,
max_concurrent_battles: int = 1,
save_replays: Union[bool, str] = False,
server_configuration: Optional[ServerConfiguration] = None,
start_timer_on_battle_start: bool = False,
start_listening: bool = True,
ping_interval: Optional[float] = 20.0,
ping_timeout: Optional[float] = 20.0,
team: Optional[Union[str, Teambuilder]] = None,
def reward_computing_helper(
self,
battle: AbstractBattle,
*,
fainted_value: float = 0.0,
hp_value: float = 0.0,
number_of_pokemons: int = 6,
starting_value: float = 0.0,
status_value: float = 0.0,
victory_value: float = 1.0,
) -> float:
if battle not in self._reward_buffer:
self._reward_buffer[battle] = starting_value
current_value = 0
for mon in battle.team.values():
current_value += mon.current_hp_fraction * hp_value
if mon.fainted:
current_value -= fainted_value
elif mon.status is not None:
current_value -= status_value
current_value += (number_of_pokemons - len(battle.team)) * hp_value
for mon in battle.opponent_team.values():
current_value -= mon.current_hp_fraction * hp_value
if mon.fainted:
current_value += fainted_value
elif mon.status is not None:
current_value += status_value
current_value -= (number_of_pokemons - len(battle.opponent_team)) * hp_value
if battle.won:
current_value += victory_value
elif battle.lost:
current_value -= victory_value
to_return = current_value - self._reward_buffer[battle]
self._reward_buffer[battle] = current_value
return to_return
def _create_account_configuration(self) -> AccountConfiguration:
key = type(self).__name__
CONFIGURATION_FROM_PLAYER_COUNTER.update([key])
username = "%s %d" % (key, CONFIGURATION_FROM_PLAYER_COUNTER[key])
if len(username) > 18:
username = "%s %d" % (
key[: 18 - len(username)],
CONFIGURATION_FROM_PLAYER_COUNTER[key],
)
return AccountConfiguration(username, None)
def _battle_finished_callback(self, battle: AbstractBattle):
pass
def update_team(self, team: Union[Teambuilder, str]):
"""Updates the team used by the player.
:param team: The new team to use.
:type team: str or Teambuilder
"""
if isinstance(team, Teambuilder):
self._team = team
else:
self._team = ConstantTeambuilder(team)
async def _get_battle(self, battle_tag: str) -> AbstractBattle:
battle_tag = battle_tag[1:]
while True:
if battle_tag in self._battles:
return self._battles[battle_tag]
async with self._battle_start_condition:
await self._battle_start_condition.wait()
async def _handle_battle_request(
self,
battle: AbstractBattle,
from_teampreview_request: bool = False,
maybe_default_order: bool = False,
):
if maybe_default_order and random.random() < self.DEFAULT_CHOICE_CHANCE:
message = self.choose_default_move().message
elif battle.teampreview:
if not from_teampreview_request:
return
message = self.teampreview(battle)
else:
message = self.choose_move(battle)
if isinstance(message, Awaitable):
message = await message
message = message.message
await self.ps_client.send_message(message, battle.battle_tag)
async def _handle_challenge_request(self, split_message: List[str]):
"""Handles an individual challenge."""
challenging_player = split_message[2].strip()
if challenging_player != self.username:
if len(split_message) >= 6:
if split_message[5] == self._format:
await self._challenge_queue.put(challenging_player)
async def _update_challenges(self, split_message: List[str]):
"""Update internal challenge state.
Add corresponding challenges to internal queue of challenges, where they will be
processed if relevant.
:param split_message: Recevied message, split.
:type split_message: List[str]
"""
self.logger.debug("Updating challenges with %s", split_message)
challenges = orjson.loads(split_message[2]).get("challengesFrom", {})
for user, format_ in challenges.items():
if format_ == self._format:
await self._challenge_queue.put(user)
async def accept_challenges(
self,
opponent: Optional[Union[str, List[str]]],
n_challenges: int,
packed_team: Optional[str] = None,
):
"""Let the player wait for challenges from opponent, and accept them.
If opponent is None, every challenge will be accepted. If opponent if a string,
all challenges from player with that name will be accepted. If opponent is a
list all challenges originating from players whose name is in the list will be
accepted.
Up to n_challenges challenges will be accepted, after what the function will
wait for these battles to finish, and then return.
:param opponent: Players from which challenges will be accepted.
:type opponent: None, str or list of str
:param n_challenges: Number of challenges that will be accepted
:type n_challenges: int
:packed_team: Team to use. Defaults to generating a team with the agent's teambuilder.
:type packed_team: string, optional.
"""
if packed_team is None:
packed_team = self.next_team
import logging
logging.warning("AAAHHH in accept_challenges")
await handle_threaded_coroutines(
self._accept_challenges(opponent, n_challenges, packed_team)
)
async def _accept_challenges(
self,
opponent: Optional[Union[str, List[str]]],
n_challenges: int,
packed_team: Optional[str],
):
import logging
logging.warning("AAAHHH in _accept_challenges")
if opponent:
if isinstance(opponent, list):
opponent = [to_id_str(o) for o in opponent]
else:
opponent = to_id_str(opponent)
await self.ps_client.logged_in.wait()
self.logger.debug("Event logged in received in accept_challenge")
for _ in range(n_challenges):
while True:
username = to_id_str(await self._challenge_queue.get())
self.logger.debug(
"Consumed %s from challenge queue in accept_challenge", username
)
if (
(opponent is None)
or (opponent == username)
or (isinstance(opponent, list) and (username in opponent))
):
await self.ps_client.accept_challenge(username, packed_team)
await self._battle_semaphore.acquire()
break
await self._battle_count_queue.join()
@abstractmethod
def choose_move(
self, battle: AbstractBattle
) -> Union[BattleOrder, Awaitable[BattleOrder]]:
"""Abstract method to choose a move in a battle.
:param battle: The battle.
:type battle: AbstractBattle
:return: The move order.
:rtype: str
"""
pass
def choose_default_move(self) -> DefaultBattleOrder:
"""Returns showdown's default move order.
This order will result in the first legal order - according to showdown's
ordering - being chosen.
"""
return DefaultBattleOrder()
def choose_random_singles_move(self, battle: Battle) -> BattleOrder:
available_orders = [BattleOrder(move) for move in battle.available_moves]
available_orders.extend(
[BattleOrder(switch) for switch in battle.available_switches]
)
if battle.can_mega_evolve:
available_orders.extend(
[BattleOrder(move, mega=True) for move in battle.available_moves]
)
if battle.can_dynamax:
available_orders.extend(
[BattleOrder(move, dynamax=True) for move in battle.available_moves]
)
if battle.can_tera:
available_orders.extend(
[
BattleOrder(move, terastallize=True)
for move in battle.available_moves
]
)
if battle.can_z_move and battle.active_pokemon:
available_z_moves = set(battle.active_pokemon.available_z_moves)
available_orders.extend(
[
BattleOrder(move, z_move=True)
for move in battle.available_moves
if move in available_z_moves
]
)
if available_orders:
return available_orders[int(random.random() * len(available_orders))]
else:
return self.choose_default_move()
def choose_random_move(self, battle: AbstractBattle) -> BattleOrder:
"""Returns a random legal move from battle.
:param battle: The battle in which to move.
:type battle: AbstractBattle
:return: Move order
:rtype: str
"""
if isinstance(battle, Battle):
return self.choose_random_singles_move(battle)
elif isinstance(battle, DoubleBattle):
return self.choose_random_doubles_move(battle)
else:
raise ValueError(
"battle should be Battle or DoubleBattle. Received %d" % (type(battle))
)
async def ladder(self, n_games: int):
"""Make the player play games on the ladder.
n_games defines how many battles will be played.
:param n_games: Number of battles that will be played
:type n_games: int
"""
await handle_threaded_coroutines(self._ladder(n_games))
async def _ladder(self, n_games: int):
await self.ps_client.logged_in.wait()
start_time = perf_counter()
for _ in range(n_games):
async with self._battle_start_condition:
await self.ps_client.search_ladder_game(self._format, self.next_team)
await self._battle_start_condition.wait()
while self._battle_count_queue.full():
async with self._battle_end_condition:
await self._battle_end_condition.wait()
await self._battle_semaphore.acquire()
await self._battle_count_queue.join()
self.logger.info(
"Laddering (%d battles) finished in %fs",
n_games,
perf_counter() - start_time,
)
async def battle_against(self, opponent: "Player", n_battles: int = 1):
"""Make the player play n_battles against opponent.
This function is a wrapper around send_challenges and accept challenges.
:param opponent: The opponent to play against.
:type opponent: Player
:param n_battles: The number of games to play. Defaults to 1.
:type n_battles: int
"""
await handle_threaded_coroutines(self._battle_against(opponent, n_battles))
async def _battle_against(self, opponent: "Player", n_battles: int):
await asyncio.gather(
self.send_challenges(
to_id_str(opponent.username),
n_battles,
to_wait=opponent.ps_client.logged_in,
),
opponent.accept_challenges(
to_id_str(self.username), n_battles, opponent.next_team
),
)
async def send_challenges(
self, opponent: str, n_challenges: int, to_wait: Optional[Event] = None
):
"""Make the player send challenges to opponent.
opponent must be a string, corresponding to the name of the player to challenge.
n_challenges defines how many challenges will be sent.
to_wait is an optional event that can be set, in which case it will be waited
before launching challenges.
:param opponent: Player username to challenge.
:type opponent: str
:param n_challenges: Number of battles that will be started
:type n_challenges: int
:param to_wait: Optional event to wait before launching challenges.
:type to_wait: Event, optional.
"""
await handle_threaded_coroutines(
self._send_challenges(opponent, n_challenges, to_wait)
)
async def _send_challenges(
self, opponent: str, n_challenges: int, to_wait: Optional[Event] = None
):
await self.ps_client.logged_in.wait()
self.logger.info("Event logged in received in send challenge")
if to_wait is not None:
await to_wait.wait()
start_time = perf_counter()
for _ in range(n_challenges):
await self.ps_client.challenge(opponent, self._format, self.next_team)
await self._battle_semaphore.acquire()
await self._battle_count_queue.join()
self.logger.info(
"Challenges (%d battles) finished in %fs",
n_challenges,
perf_counter() - start_time,
)
def random_teampreview(self, battle: AbstractBattle) -> str:
"""Returns a random valid teampreview order for the given battle.
:param battle: The battle.
:type battle: AbstractBattle
:return: The random teampreview order.
:rtype: str
"""
members = list(range(1, len(battle.team) + 1))
random.shuffle(members)
return "/team " + "".join([str(c) for c in members])
def reset_battles(self):
"""Resets the player's inner battle tracker."""
for battle in list(self._battles.values()):
if not battle.finished:
raise EnvironmentError(
"Can not reset player's battles while they are still running"
)
self._battles = {}
def teampreview(self, battle: AbstractBattle) -> str:
"""Returns a teampreview order for the given battle.
This order must be of the form /team TEAM, where TEAM is a string defining the
team chosen by the player. Multiple formats are supported, among which '3461'
and '3, 4, 6, 1' are correct and indicate leading with pokemon 3, with pokemons
4, 6 and 1 in the back in single battles or leading with pokemons 3 and 4 with
pokemons 6 and 1 in the back in double battles.
Please refer to Pokemon Showdown's protocol documentation for more information.
:param battle: The battle.
:type battle: AbstractBattle
:return: The teampreview order.
:rtype: str
"""
return self.random_teampreview(battle)
@staticmethod
def create_order(
order: Union[Move, Pokemon],
mega: bool = False,
z_move: bool = False,
dynamax: bool = False,
terastallize: bool = False,
move_target: int = DoubleBattle.EMPTY_TARGET_POSITION,
) -> BattleOrder:
"""Formats an move order corresponding to the provided pokemon or move.
:param order: Move to make or Pokemon to switch to.
:type order: Move or Pokemon
:param mega: Whether to mega evolve the pokemon, if a move is chosen.
:type mega: bool
:param z_move: Whether to make a zmove, if a move is chosen.
:type z_move: bool
:param dynamax: Whether to dynamax, if a move is chosen.
:type dynamax: bool
:param terastallize: Whether to terastallize, if a move is chosen.
:type terastallize: bool
:param move_target: Target Pokemon slot of a given move
:type move_target: int
:return: Formatted move order
:rtype: str
"""
return BattleOrder(
order,
mega=mega,
move_target=move_target,
z_move=z_move,
dynamax=dynamax,
terastallize=terastallize,
)
@property
def battles(self) -> Dict[str, AbstractBattle]:
return self._battles
@property
def format(self) -> str:
return self._format
@property
def format_is_doubles(self) -> bool:
format_lowercase = self._format.lower()
return (
"vgc" in format_lowercase
or "double" in format_lowercase
or "metronome" in format_lowercase
)
@property
def n_finished_battles(self) -> int:
return len([None for b in self._battles.values() if b.finished])
@property
def n_lost_battles(self) -> int:
return len([None for b in self._battles.values() if b.lost])
@property
def n_tied_battles(self) -> int:
return self.n_finished_battles - self.n_lost_battles - self.n_won_battles
@property
def n_won_battles(self) -> int:
return len([None for b in self._battles.values() if b.won])
@property
def win_rate(self) -> float:
return self.n_won_battles / self.n_finished_battles
@property
def logger(self) -> Logger:
return self.ps_client.logger
@property
def username(self) -> str:
return self.ps_client.username
@property
def next_team(self) -> Optional[str]:
if self._team:
return self._team.yield_team()
return None
.\PokeLLMon\poke_env\player\random_player.py
"""This module defines a random players baseline
"""
from poke_env.environment import AbstractBattle
from poke_env.player.battle_order import BattleOrder
from poke_env.player.player import Player
class RandomPlayer(Player):
def choose_move(self, battle: AbstractBattle) -> BattleOrder:
return self.choose_random_move(battle)
.\PokeLLMon\poke_env\player\utils.py
"""This module contains utility functions and objects related to Player classes.
"""
import asyncio
import math
from concurrent.futures import Future
from typing import Dict, List, Optional, Tuple
from poke_env.concurrency import POKE_LOOP
from poke_env.data import to_id_str
from poke_env.player.baselines import MaxBasePowerPlayer, SimpleHeuristicsPlayer
from poke_env.player.player import Player
from poke_env.player.random_player import RandomPlayer
_EVALUATION_RATINGS = {
RandomPlayer: 1,
MaxBasePowerPlayer: 7.665994,
SimpleHeuristicsPlayer: 128.757145,
}
def background_cross_evaluate(
players: List[Player], n_challenges: int
) -> "Future[Dict[str, Dict[str, Optional[float]]]]":
return asyncio.run_coroutine_threadsafe(
cross_evaluate(players, n_challenges), POKE_LOOP
)
async def cross_evaluate(
players: List[Player], n_challenges: int
) -> Dict[str, Dict[str, Optional[float]]]:
results: Dict[str, Dict[str, Optional[float]]] = {
p_1.username: {p_2.username: None for p_2 in players} for p_1 in players
}
for i, p_1 in enumerate(players):
for j, p_2 in enumerate(players):
if j <= i:
continue
await asyncio.gather(
p_1.send_challenges(
opponent=to_id_str(p_2.username),
n_challenges=n_challenges,
to_wait=p_2.ps_client.logged_in,
),
p_2.accept_challenges(
opponent=to_id_str(p_1.username),
n_challenges=n_challenges,
packed_team=p_2.next_team,
),
)
results[p_1.username][p_2.username] = p_1.win_rate
results[p_2.username][p_1.username] = p_2.win_rate
p_1.reset_battles()
p_2.reset_battles()
return results
def _estimate_strength_from_results(
number_of_games: int, number_of_wins: int, opponent_rating: float
def evaluate_player(
number_of_games: int,
number_of_wins: int,
opponent_rating: float,
) -> Tuple[float, Tuple[float, float]]:
n, p = number_of_games, number_of_wins / number_of_games
q = 1 - p
if n * p * q < 9:
raise ValueError(
"The results obtained in evaluate_player are too extreme to obtain an "
"accurate player evaluation. You can try to solve this issue by increasing"
" the total number of battles. Obtained results: %d victories out of %d"
" games." % (p * n, n)
)
estimate = opponent_rating * p / q
error = (
math.sqrt(n * p * q) / n * 1.96
)
lower_bound = max(0, p - error)
lower_bound = opponent_rating * lower_bound / (1 - lower_bound)
higher_bound = min(1, p + error)
if higher_bound == 1:
higher_bound = math.inf
else:
higher_bound = opponent_rating * higher_bound / (1 - higher_bound)
return estimate, (lower_bound, higher_bound)
def background_evaluate_player(
player: Player,
n_battles: int = 1000,
n_placement_battles: int = 30,
) -> "Future[Tuple[float, Tuple[float, float]]]":
return asyncio.run_coroutine_threadsafe(
evaluate_player(player, n_battles, n_placement_battles), POKE_LOOP
)
async def evaluate_player(
player: Player,
n_battles: int = 1000,
n_placement_battles: int = 30,
def estimate_player_strength(player: Player, n_battles: int, n_placement_battles: int) -> Tuple[float, Tuple[float, float]]:
"""Estimate player strength.
This functions calculates an estimate of a player's strength, measured as its
expected performance against a random opponent in a gen 8 random battle. The
returned number can be interpreted as follows: a strength of k means that the
probability of the player winning a gen 8 random battle against a random player is k
times higher than the probability of the random player winning.
The function returns a tuple containing the best guess based on the played games
as well as a tuple describing a 95% confidence interval for that estimated strength.
The actual evaluation can be performed against any baseline player for which an
accurate strength estimate is available. This baseline is determined at the start of
the process, by playing a limited number of placement battles and choosing the
opponent closest to the player in terms of performance.
:param player: The player to evaluate.
:type player: Player
:param n_battles: The total number of battle to perform, including placement
battles.
:type n_battles: int
:param n_placement_battles: Number of placement battles to perform per baseline
player.
:type n_placement_battles: int
:raises: ValueError if the results are too extreme to be interpreted.
:raises: AssertionError if the player is not configured to play gen8battles or the
selected number of games to play it too small.
:return: A tuple containing the estimated player strength and a 95% confidence
interval
:rtype: tuple of float and tuple of floats
"""
assert player.format == "gen8randombattle", (
"Player %s can not be evaluated as its current format (%s) is not "
"gen8randombattle." % (player, player.format)
)
if n_placement_battles * len(_EVALUATION_RATINGS) > n_battles // 2:
player.logger.warning(
"Number of placement battles reduced from %d to %d due to limited number of"
" battles (%d). A more accurate evaluation can be performed by increasing "
"the total number of players.",
n_placement_battles,
n_battles // len(_EVALUATION_RATINGS) // 2,
n_battles,
)
n_placement_battles = n_battles // len(_EVALUATION_RATINGS) // 2
assert (
n_placement_battles > 0
), "Not enough battles to perform placement battles. Please increase the number of "
"battles to perform to evaluate the player."
baselines = [p(max_concurrent_battles=n_battles) for p in _EVALUATION_RATINGS]
for p in baselines:
await p.battle_against(player, n_placement_battles)
best_opp = min(
baselines, key=lambda p: (abs(p.win_rate - 0.5), -_EVALUATION_RATINGS[type(p)])
)
remaining_battles = n_battles - len(_EVALUATION_RATINGS) * n_placement_battles
await best_opp.battle_against(player, remaining_battles)
return _estimate_strength_from_results(
best_opp.n_finished_battles,
best_opp.n_lost_battles,
_EVALUATION_RATINGS[type(best_opp)],
)
.\PokeLLMon\poke_env\player\__init__.py
"""
# 导入并引入并发模块 POKE_LOOP
from poke_env.concurrency import POKE_LOOP
# 导入随机玩家、工具类
from poke_env.player import random_player, utils
# 导入基线玩家、简单启发式玩家
from poke_env.player.baselines import MaxBasePowerPlayer, SimpleHeuristicsPlayer
# 导入 GPT 玩家
from poke_env.player.gpt_player import LLMPlayer
# 导入 LLAMA 玩家
from poke_env.player.llama_player import LLAMAPlayer
# 导入战斗指令相关类
from poke_env.player.battle_order import (
BattleOrder,
DefaultBattleOrder,
DoubleBattleOrder,
ForfeitBattleOrder,
)
# 导入 OpenAI API 相关类
from poke_env.player.openai_api import ActType, ObsType, OpenAIGymEnv
# 导入玩家类
from poke_env.player.player import Player
# 导入随机玩家类
from poke_env.player.random_player import RandomPlayer
# 导入工具类中的函数
from poke_env.player.utils import (
background_cross_evaluate,
background_evaluate_player,
cross_evaluate,
evaluate_player,
)
# 导入 PS 客户端
from poke_env.ps_client import PSClient
# 导出的模块列表
__all__ = [
"openai_api",
"player",
"random_player",
"utils",
"ActType",
"ObsType",
"ForfeitBattleOrder",
"POKE_LOOP",
"OpenAIGymEnv",
"PSClient",
"Player",
"RandomPlayer",
"cross_evaluate",
"background_cross_evaluate",
"background_evaluate_player",
"evaluate_player",
"BattleOrder",
"DefaultBattleOrder",
"DoubleBattleOrder",
"MaxBasePowerPlayer",
"SimpleHeuristicsPlayer",
]
.\PokeLLMon\poke_env\ps_client\account_configuration.py
"""
# 导入必要的模块
from typing import Counter, NamedTuple, Optional
# 创建一个计数器对象,用于统计从玩家获取的配置信息
CONFIGURATION_FROM_PLAYER_COUNTER: Counter[str] = Counter()
# 定义一个命名元组对象,表示玩家配置。包含用户名和密码两个条目
class AccountConfiguration(NamedTuple):
"""Player configuration object. Represented with a tuple with two entries: username and
password."""
# 用户名
username: str
# 密码(可选)
password: Optional[str]
.\PokeLLMon\poke_env\ps_client\ps_client.py
"""
这个模块定义了一个与 Showdown 服务器通信的基类。
"""
import asyncio
import json
import logging
from asyncio import CancelledError, Event, Lock, create_task, sleep
from logging import Logger
from time import perf_counter
from typing import Any, List, Optional, Set
import requests
import websockets.client as ws
from websockets.exceptions import ConnectionClosedOK
from poke_env.concurrency import (
POKE_LOOP,
create_in_poke_loop,
handle_threaded_coroutines,
)
from poke_env.exceptions import ShowdownException
from poke_env.ps_client.account_configuration import AccountConfiguration
from poke_env.ps_client.server_configuration import ServerConfiguration
class PSClient:
"""
Pokemon Showdown 客户端。
负责与 Showdown 服务器通信。还实现了一些用于基本任务的高级方法,如更改头像和低级消息处理。
"""
def __init__(
self,
account_configuration: AccountConfiguration,
*,
avatar: Optional[int] = None,
log_level: Optional[int] = None,
server_configuration: ServerConfiguration,
start_listening: bool = True,
ping_interval: Optional[float] = 20.0,
ping_timeout: Optional[float] = 20.0,
"""
:param account_configuration: Account configuration.
:type account_configuration: AccountConfiguration
:param avatar: Player avatar id. Optional.
:type avatar: int, optional
:param log_level: The player's logger level.
:type log_level: int. Defaults to logging's default level.
:param server_configuration: Server configuration.
:type server_configuration: ServerConfiguration
:param start_listening: Whether to start listening to the server. Defaults to
True.
:type start_listening: bool
:param ping_interval: How long between keepalive pings (Important for backend
websockets). If None, disables keepalive entirely.
:type ping_interval: float, optional
:param ping_timeout: How long to wait for a timeout of a specific ping
(important for backend websockets.
Increase only if timeouts occur during runtime).
If None pings will never time out.
:type ping_timeout: float, optional
"""
self._active_tasks: Set[Any] = set()
self._ping_interval = ping_interval
self._ping_timeout = ping_timeout
self._server_configuration = server_configuration
self._account_configuration = account_configuration
self._avatar = avatar
self._logged_in: Event = create_in_poke_loop(Event)
self._sending_lock = create_in_poke_loop(Lock)
self.websocket: ws.WebSocketClientProtocol
self._logger: Logger = self._create_logger(log_level)
if start_listening:
self._listening_coroutine = asyncio.run_coroutine_threadsafe(
self.listen(), POKE_LOOP
)
async def accept_challenge(self, username: str, packed_team: Optional[str]):
assert (
self.logged_in.is_set()
), f"Expected player {self.username} to be logged in."
await self.set_team(packed_team)
await self.send_message("/accept %s" % username)
async def challenge(self, username: str, format_: str, packed_team: Optional[str]):
assert (
self.logged_in.is_set()
), f"Expected player {self.username} to be logged in."
await self.set_team(packed_team)
await self.send_message(f"/challenge {username}, {format_}")
def _create_logger(self, log_level: Optional[int]) -> Logger:
"""Creates a logger for the client.
Returns a Logger displaying asctime and the account's username before messages.
:param log_level: The logger's level.
:type log_level: int
:return: The logger.
:rtype: Logger
"""
logger = logging.getLogger(self.username)
stream_handler = logging.StreamHandler()
if log_level is not None:
logger.setLevel(log_level)
formatter = logging.Formatter(
"%(asctime)s - %(name)s - %(levelname)s - %(message)s"
)
stream_handler.setFormatter(formatter)
logger.addHandler(stream_handler)
return logger
async def _stop_listening(self):
await self.websocket.close()
async def change_avatar(self, avatar_id: Optional[int]):
"""Changes the player's avatar.
:param avatar_id: The new avatar id. If None, nothing happens.
:type avatar_id: int
"""
await self.wait_for_login()
if avatar_id is not None:
await self.send_message(f"/avatar {avatar_id}")
async def listen(self):
self.logger.info("Starting listening to showdown websocket")
try:
async with ws.connect(
self.websocket_url,
max_queue=None,
ping_interval=self._ping_interval,
ping_timeout=self._ping_timeout,
) as websocket:
self.websocket = websocket
async for message in websocket:
self.logger.info("\033[92m\033[1m<<<\033[0m %s", message)
task = create_task(self._handle_message(str(message)))
self._active_tasks.add(task)
task.add_done_callback(self._active_tasks.discard)
except ConnectionClosedOK:
self.logger.warning(
"Websocket connection with %s closed", self.websocket_url
)
except (CancelledError, RuntimeError) as e:
self.logger.critical("Listen interrupted by %s", e)
except Exception as e:
self.logger.exception(e)
async def log_in(self, split_message: List[str]):
"""Log the player with specified username and password.
Split message contains information sent by the server. This information is
necessary to log in.
:param split_message: Message received from the server that triggers logging in.
:type split_message: List[str]
"""
if self.account_configuration.password:
log_in_request = requests.post(
self.server_configuration.authentication_url,
data={
"act": "login",
"name": self.account_configuration.username,
"pass": self.account_configuration.password,
"challstr": split_message[2] + "%7C" + split_message[3],
},
)
self.logger.info("Sending authentication request")
assertion = json.loads(log_in_request.text[1:])["assertion"]
else:
self.logger.info("Bypassing authentication request")
assertion = ""
await self.send_message(f"/trn {self.username},0,{assertion}")
await self.change_avatar(self._avatar)
async def search_ladder_game(self, format_: str, packed_team: Optional[str]):
await self.set_team(packed_team)
await self.send_message(f"/search {format_}")
async def send_message(
self, message: str, room: str = "", message_2: Optional[str] = None
):
"""Sends a message to the specified room.
`message_2` can be used to send a sequence of length 2.
:param message: The message to send.
:type message: str
:param room: The room to which the message should be sent.
:type room: str
:param message_2: Second element of the sequence to be sent. Optional.
:type message_2: str, optional
"""
if message_2:
to_send = "|".join([room, message, message_2])
else:
to_send = "|".join([room, message])
await self.websocket.send(to_send)
async def set_team(self, packed_team: Optional[str]):
if packed_team:
await self.send_message(f"/utm {packed_team}")
else:
await self.send_message("/utm null")
async def stop_listening(self):
await handle_threaded_coroutines(self._stop_listening())
async def wait_for_login(self, checking_interval: float = 0.001, wait_for: int = 5):
start = perf_counter()
while perf_counter() - start < wait_for:
await sleep(checking_interval)
if self.logged_in:
return
assert self.logged_in, f"Expected player {self.username} to be logged in."
@property
def account_configuration(self) -> AccountConfiguration:
"""The client's account configuration.
:return: The client's account configuration.
:rtype: AccountConfiguration
"""
return self._account_configuration
@property
def logged_in(self) -> Event:
"""Event object associated with user login.
:return: The logged-in event
:rtype: Event
"""
return self._logged_in
@property
def logger(self) -> Logger:
"""Logger associated with the player.
:return: The logger.
:rtype: Logger
"""
return self._logger
@property
def server_configuration(self) -> ServerConfiguration:
"""获取客户端的服务器配置信息。
:return: 客户端的服务器配置信息。
:rtype: ServerConfiguration
"""
return self._server_configuration
@property
def username(self) -> str:
"""玩家的用户名。
:return: 玩家的用户名。
:rtype: str
"""
return self.account_configuration.username
@property
def websocket_url(self) -> str:
"""WebSocket 的 URL。
它是从服务器 URL 派生而来。
:return: WebSocket 的 URL。
:rtype: str
"""
return f"ws://{self.server_configuration.server_url}/showdown/websocket"
.\PokeLLMon\poke_env\ps_client\server_configuration.py
from typing import NamedTuple
class ServerConfiguration(NamedTuple):
server_url: str
authentication_url: str
LocalhostServerConfiguration = ServerConfiguration(
"localhost:8000", "https://play.pokemonshowdown.com/action.php?"
)
ShowdownServerConfiguration = ServerConfiguration(
"sim.smogon.com:8000", "https://play.pokemonshowdown.com/action.php?"
)
.\PokeLLMon\poke_env\ps_client\__init__.py
from poke_env.ps_client.account_configuration import AccountConfiguration
from poke_env.ps_client.ps_client import PSClient
from poke_env.ps_client.server_configuration import (
LocalhostServerConfiguration,
ServerConfiguration,
ShowdownServerConfiguration,
)
__all__ = [
"AccountConfiguration",
"LocalhostServerConfiguration",
"PSClient",
"ServerConfiguration",
"ShowdownServerConfiguration",
]
.\PokeLLMon\poke_env\stats.py
import math
from typing import List
from poke_env.data import GenData
STATS_TO_IDX = {
"hp": 0,
"atk": 1,
"def": 2,
"spa": 3,
"spd": 4,
"spe": 5,
"satk": 3,
"sdef": 4,
}
def _raw_stat(base: int, ev: int, iv: int, level: int, nature_multiplier: float) -> int:
"""Converts to raw stat
:param base: the base stat
:param ev: Stat Effort Value (EV)
:param iv: Stat Individual Values (IV)
:param level: pokemon level
:param nature_multiplier: stat multiplier of the nature (either 0.9, 1 or 1.1)
:return: the raw stat
"""
s = math.floor(
(5 + math.floor((math.floor(ev / 4) + iv + 2 * base) * level / 100))
* nature_multiplier
)
return int(s)
def _raw_hp(base: int, ev: int, iv: int, level: int) -> int:
"""Converts to raw hp
:param base: the base stat
:param ev: HP Effort Value (EV)
:param iv: HP Individual Value (IV)
:param level: pokemon level
:return: the raw hp
"""
s = math.floor((math.floor(ev / 4) + iv + 2 * base) * level / 100) + level + 10
return int(s)
def compute_raw_stats(
species: str, evs: List[int], ivs: List[int], level: int, nature: str, data: GenData
) -> List[int]:
"""Converts to raw stats
:param species: pokemon species
:param evs: list of pokemon's EVs (size 6)
:param ivs: list of pokemon's IVs (size 6)
:param level: pokemon level
:param nature: pokemon nature
:return: the raw stats in order [hp, atk, def, spa, spd, spe]
"""
assert len(evs) == 6
assert len(ivs) == 6
base_stats = [0] * 6
for stat, value in data.pokedex[species]["baseStats"].items():
base_stats[STATS_TO_IDX[stat]] = value
nature_multiplier = [1.0] * 6
for stat, multiplier in data.natures[nature].items():
if stat != "num":
nature_multiplier[STATS_TO_IDX[stat]] = multiplier
raw_stats = [0] * 6
if species == "shedinja":
raw_stats[0] = 1
else:
raw_stats[0] = _raw_hp(base_stats[0], evs[0], ivs[0], level)
for i in range(1, 6):
raw_stats[i] = _raw_stat(
base_stats[i], evs[i], ivs[i], level, nature_multiplier[i]
)
return raw_stats
.\PokeLLMon\poke_env\teambuilder\constant_teambuilder.py
"""This module defines the ConstantTeambuilder class, which is a subclass of
ShowdownTeamBuilder that yields a constant team.
"""
from poke_env.teambuilder.teambuilder import Teambuilder
class ConstantTeambuilder(Teambuilder):
def __init__(self, team: str):
if "|" in team:
self.converted_team = team
else:
mons = self.parse_showdown_team(team)
self.converted_team = self.join_team(mons)
def yield_team(self) -> str:
return self.converted_team
.\PokeLLMon\poke_env\teambuilder\teambuilder.py
"""This module defines the Teambuilder abstract class, which represents objects yielding
Pokemon Showdown teams in the context of communicating with Pokemon Showdown.
"""
from abc import ABC, abstractmethod
from typing import List
from poke_env.stats import STATS_TO_IDX
from poke_env.teambuilder.teambuilder_pokemon import TeambuilderPokemon
class Teambuilder(ABC):
"""Teambuilder objects allow the generation of teams by Player instances.
They must implement the yield_team method, which must return a valid
packed-formatted showdown team every time it is called.
This format is a custom format described in Pokemon's showdown protocol
documentation:
https://github.com/smogon/pokemon-showdown/blob/master/PROTOCOL.md#team-format
This class also implements a helper function to convert teams from the classical
showdown team text format into the packed-format.
"""
@abstractmethod
def yield_team(self) -> str:
"""Returns a packed-format team."""
@staticmethod
@staticmethod
def join_team(team: List[TeambuilderPokemon]) -> str:
"""Converts a list of TeambuilderPokemon objects into the corresponding packed
showdown team format.
:param team: The list of TeambuilderPokemon objects that form the team.
:type team: list of TeambuilderPokemon
:return: The formatted team string.
:rtype: str"""
return "]".join([mon.formatted for mon in team])