/// <summary> /// Creates valid actions for a battle turn for a specific team. /// </summary> /// <param name="team">The team to create actions for.</param> /// <exception cref="InvalidOperationException">Thrown when <paramref name="team"/> has no active battlers or <paramref name="team"/>'s <see cref="PBETeam.Battle"/>'s <see cref="PBEBattle.BattleState"/> is not <see cref="PBEBattleState.WaitingForActions"/>.</exception> /// <exception cref="ArgumentOutOfRangeException">Thrown when a Pokémon has no moves, the AI tries to use a move with invalid targets, or <paramref name="team"/>'s <see cref="PBETeam.Battle"/>'s <see cref="PBEBattle.BattleFormat"/> is invalid.</exception> public static IEnumerable <PBEAction> CreateActions(PBETeam team) { if (team.Battle.BattleState != PBEBattleState.WaitingForActions) { throw new InvalidOperationException($"{nameof(team.Battle.BattleState)} must be {PBEBattleState.WaitingForActions} to create actions."); } PBEPokemon[] active = team.ActiveBattlers.ToArray(); if (active.Length == 0) { throw new InvalidOperationException($"{nameof(team)} must have active battlers."); } var actions = new PBEAction[active.Length]; for (int i = 0; i < active.Length; i++) { PBEPokemon pkmn = active[i]; if (pkmn.Moves.All(m => m == PBEMove.None)) { throw new ArgumentOutOfRangeException(nameof(pkmn.Moves), $"{pkmn.Shell.Nickname} has no moves."); } actions[i].PokemonId = pkmn.Id; actions[i].Decision = PBEDecision.Fight; if (pkmn.IsForcedToStruggle()) { actions[i].FightMove = PBEMove.Struggle; } else if (pkmn.TempLockedMove != PBEMove.None) { actions[i].FightMove = pkmn.TempLockedMove; } else if (pkmn.ChoiceLockedMove != PBEMove.None) { actions[i].FightMove = pkmn.ChoiceLockedMove; } else { int index; do { index = PBEUtils.RNG.Next(0, pkmn.Moves.Length); actions[i].FightMove = pkmn.Moves[index]; } while (pkmn.PP[index] == 0); // PP of PBEMove.None is always 0 as well } if (pkmn.TempLockedTargets != PBETarget.None) { actions[i].FightTargets = pkmn.TempLockedTargets; } else { actions[i].FightTargets = DecideTargets(pkmn, actions[i].FightMove); } } return(actions); }
public PBEActionsResponsePacket(byte[] buffer, PBEBattle battle) { Buffer = buffer; using (var r = new BinaryReader(new MemoryStream(buffer))) { r.ReadInt16(); // Skip Code var actions = new PBEAction[r.ReadByte()]; for (int i = 0; i < actions.Length; i++) { actions[i] = PBEAction.FromBytes(r); } Actions = Array.AsReadOnly(actions); } }
/// <summary> /// Creates valid actions for a battle turn for a specific team. /// </summary> /// <param name="team">The team to create actions for.</param> /// <exception cref="InvalidOperationException">Thrown when <paramref name="team"/> has no active battlers or <paramref name="team"/>'s <see cref="PBETeam.Battle"/>'s <see cref="PBEBattle.BattleState"/> is not <see cref="PBEBattleState.WaitingForActions"/>.</exception> /// <exception cref="ArgumentOutOfRangeException">Thrown when a Pokémon has no moves, the AI tries to use a move with invalid targets, or <paramref name="team"/>'s <see cref="PBETeam.Battle"/>'s <see cref="PBEBattle.BattleFormat"/> is invalid.</exception> public static IEnumerable <PBEAction> CreateActions(PBETeam team) { if (team.Battle.BattleState != PBEBattleState.WaitingForActions) { throw new InvalidOperationException($"{nameof(team.Battle.BattleState)} must be {PBEBattleState.WaitingForActions} to create actions."); } PBEPokemon[] active = team.ActiveBattlers.ToArray(); if (active.Length == 0) { throw new InvalidOperationException($"{nameof(team)} must have at least one active battler."); } PBETeam opposingTeam = team == team.Battle.Teams[0] ? team.Battle.Teams[1] : team.Battle.Teams[0]; var actions = new PBEAction[team.ActionsRequired.Count]; var standBy = new List <PBEPokemon>(); for (int i = 0; i < actions.Length; i++) { PBEPokemon pkmn = team.ActionsRequired[i]; if (Array.TrueForAll(pkmn.Moves, m => m == PBEMove.None)) { throw new ArgumentOutOfRangeException(nameof(pkmn.Moves), $"{pkmn.Nickname} has no moves."); } // If a Pokémon is forced to struggle, it is best that it just stays in until it faints if (pkmn.IsForcedToStruggle()) { actions[i].Decision = PBEDecision.Fight; actions[i].FightMove = PBEMove.Struggle; actions[i].FightTargets = GetPossibleTargets(pkmn, pkmn.GetMoveTargets(PBEMove.Struggle))[0]; // Seems a little nasty just to select "Self", since this will be RandomFoeSurrounding } // If a Pokémon has a temp locked move (Dig, Dive, Shadow Force) it must be used else if (pkmn.TempLockedMove != PBEMove.None) { actions[i].Decision = PBEDecision.Fight; actions[i].FightMove = pkmn.TempLockedMove; actions[i].FightTargets = pkmn.TempLockedTargets; } // The Pokémon is free to switch or fight (unless it cannot switch due to Magnet Pull etc) else { // Gather all options of switching and moves PBEPokemon[] availableForSwitch = team.Party.Except(standBy).Where(p => p.FieldPosition == PBEFieldPosition.None && p.HP > 0).ToArray(); PBEMove[] usableMoves = pkmn.GetUsableMoves(); var possibleActions = new List <(PBEAction Action, double Score)>(); for (int m = 0; m < usableMoves.Length; m++) // Score moves { PBEMove move = usableMoves[m]; PBEType moveType = pkmn.GetMoveType(move); PBEMoveTarget moveTargets = pkmn.GetMoveTargets(move); PBETarget[] possibleTargets = PBEMoveData.IsSpreadMove(moveTargets) ? new PBETarget[] { GetSpreadMoveTargets(pkmn, moveTargets) } : GetPossibleTargets(pkmn, moveTargets); foreach (PBETarget possibleTarget in possibleTargets) { // TODO: RandomFoeSurrounding (probably just account for the specific effects that use this target type) var targets = new List <PBEPokemon>(); if (possibleTarget.HasFlag(PBETarget.AllyLeft)) { targets.Add(team.TryGetPokemon(PBEFieldPosition.Left)); } if (possibleTarget.HasFlag(PBETarget.AllyCenter)) { targets.Add(team.TryGetPokemon(PBEFieldPosition.Center)); } if (possibleTarget.HasFlag(PBETarget.AllyRight)) { targets.Add(team.TryGetPokemon(PBEFieldPosition.Right)); } if (possibleTarget.HasFlag(PBETarget.FoeLeft)) { targets.Add(opposingTeam.TryGetPokemon(PBEFieldPosition.Left)); } if (possibleTarget.HasFlag(PBETarget.FoeCenter)) { targets.Add(opposingTeam.TryGetPokemon(PBEFieldPosition.Center)); } if (possibleTarget.HasFlag(PBETarget.FoeRight)) { targets.Add(opposingTeam.TryGetPokemon(PBEFieldPosition.Right)); } double score = 0.0; switch (PBEMoveData.Data[move].Effect) { case PBEMoveEffect.Burn: case PBEMoveEffect.Paralyze: case PBEMoveEffect.Poison: case PBEMoveEffect.Sleep: case PBEMoveEffect.Toxic: { foreach (PBEPokemon target in targets) { if (target == null) { // TODO: If all targets are null, this should give a bad score } else { // TODO: Effectiveness check // TODO: Favor sleep with Bad Dreams (unless ally) if (target.Status1 != PBEStatus1.None) { score += target.Team == opposingTeam ? -60 : 0; } else { score += target.Team == opposingTeam ? +40 : -20; } } } break; } case PBEMoveEffect.Hail: { if (team.Battle.Weather == PBEWeather.Hailstorm) { score -= 100; } break; } case PBEMoveEffect.BrickBreak: case PBEMoveEffect.Dig: case PBEMoveEffect.Dive: case PBEMoveEffect.Fly: case PBEMoveEffect.Hit: case PBEMoveEffect.Hit__MaybeBurn: case PBEMoveEffect.Hit__MaybeConfuse: case PBEMoveEffect.Hit__MaybeFlinch: case PBEMoveEffect.Hit__MaybeFreeze: case PBEMoveEffect.Hit__MaybeLowerTarget_ACC_By1: case PBEMoveEffect.Hit__MaybeLowerTarget_ATK_By1: case PBEMoveEffect.Hit__MaybeLowerTarget_DEF_By1: case PBEMoveEffect.Hit__MaybeLowerTarget_SPATK_By1: case PBEMoveEffect.Hit__MaybeLowerTarget_SPDEF_By1: case PBEMoveEffect.Hit__MaybeLowerTarget_SPDEF_By2: case PBEMoveEffect.Hit__MaybeLowerTarget_SPE_By1: case PBEMoveEffect.Hit__MaybeLowerUser_ATK_DEF_By1: case PBEMoveEffect.Hit__MaybeLowerUser_DEF_SPDEF_By1: case PBEMoveEffect.Hit__MaybeLowerUser_SPATK_By2: case PBEMoveEffect.Hit__MaybeLowerUser_SPE_By1: case PBEMoveEffect.Hit__MaybeLowerUser_SPE_DEF_SPDEF_By1: case PBEMoveEffect.Hit__MaybeParalyze: case PBEMoveEffect.Hit__MaybePoison: case PBEMoveEffect.Hit__MaybeRaiseUser_ATK_By1: case PBEMoveEffect.Hit__MaybeRaiseUser_ATK_DEF_SPATK_SPDEF_SPE_By1: case PBEMoveEffect.Hit__MaybeRaiseUser_DEF_By1: case PBEMoveEffect.Hit__MaybeRaiseUser_SPATK_By1: case PBEMoveEffect.Hit__MaybeRaiseUser_SPE_By1: case PBEMoveEffect.Hit__MaybeToxic: case PBEMoveEffect.HPDrain: case PBEMoveEffect.Recoil: case PBEMoveEffect.FlareBlitz: case PBEMoveEffect.SuckerPunch: case PBEMoveEffect.VoltTackle: { foreach (PBEPokemon target in targets) { if (target == null) { // TODO: If all targets are null, this should give a bad score } else { // TODO: Put type checking somewhere in PBEPokemon (levitate, wonder guard, etc) // TODO: Favor hitting ally with move if it absorbs it // TODO: Check items // TODO: Stat changes and accuracy // TODO: Check base power specifically against hp remaining (include spread move damage reduction) double typeEffectiveness = PBEPokemonData.TypeEffectiveness[moveType][target.KnownType1]; typeEffectiveness *= PBEPokemonData.TypeEffectiveness[moveType][target.KnownType2]; if (typeEffectiveness <= 0.0) // (-infinity, 0.0] Ineffective { score += target.Team == opposingTeam ? -60 : -1; } else if (typeEffectiveness <= 0.25) // (0.0, 0.25] NotVeryEffective { score += target.Team == opposingTeam ? -30 : -5; } else if (typeEffectiveness < 1.0) // (0.25, 1.0) NotVeryEffective { score += target.Team == opposingTeam ? -10 : -10; } else if (typeEffectiveness == 1.0) // [1.0, 1.0] Normal { score += target.Team == opposingTeam ? +10 : -15; } else if (typeEffectiveness < 4.0) // (1.0, 4.0) SuperEffective { score += target.Team == opposingTeam ? +25 : -20; } else // [4.0, infinity) SuperEffective { score += target.Team == opposingTeam ? +40 : -30; } if (pkmn.HasType(moveType) && typeEffectiveness > 0.0) // STAB { score += (pkmn.Ability == PBEAbility.Adaptability ? 15 : 10) * (target.Team == opposingTeam ? +1 : -1); } } } break; } case PBEMoveEffect.Moonlight: case PBEMoveEffect.Rest: case PBEMoveEffect.RestoreTargetHP: case PBEMoveEffect.RestoreUserHP: { PBEPokemon target = targets[0]; if (target == null || target.Team == opposingTeam) { score -= 100; } else // Ally { // 0% = +45, 25% = +30, 50% = +15, 75% = 0, 100% = -15 score -= (60 * target.HPPercentage) - 45; } break; } case PBEMoveEffect.RainDance: { if (team.Battle.Weather == PBEWeather.Rain) { score -= 100; } break; } case PBEMoveEffect.Sandstorm: { if (team.Battle.Weather == PBEWeather.Sandstorm) { score -= 100; } break; } case PBEMoveEffect.SunnyDay: { if (team.Battle.Weather == PBEWeather.HarshSunlight) { score -= 100; } break; } case PBEMoveEffect.Attract: case PBEMoveEffect.ChangeTarget_ACC: case PBEMoveEffect.ChangeTarget_ATK: case PBEMoveEffect.ChangeTarget_DEF: case PBEMoveEffect.ChangeTarget_EVA: case PBEMoveEffect.ChangeTarget_SPDEF: case PBEMoveEffect.ChangeTarget_SPE: case PBEMoveEffect.ChangeUser_ATK: case PBEMoveEffect.ChangeUser_DEF: case PBEMoveEffect.ChangeUser_EVA: case PBEMoveEffect.ChangeUser_SPATK: case PBEMoveEffect.ChangeUser_SPDEF: case PBEMoveEffect.ChangeUser_SPE: case PBEMoveEffect.Confuse: case PBEMoveEffect.Curse: case PBEMoveEffect.Endeavor: case PBEMoveEffect.Fail: case PBEMoveEffect.FinalGambit: case PBEMoveEffect.Flatter: case PBEMoveEffect.FocusEnergy: case PBEMoveEffect.GastroAcid: case PBEMoveEffect.Growth: case PBEMoveEffect.HelpingHand: case PBEMoveEffect.LeechSeed: case PBEMoveEffect.LightScreen: case PBEMoveEffect.LowerTarget_ATK_DEF_By1: case PBEMoveEffect.LowerUser_DEF_SPDEF_By1_Raise_ATK_SPATK_SPE_By2: case PBEMoveEffect.LuckyChant: case PBEMoveEffect.Metronome: case PBEMoveEffect.OneHitKnockout: case PBEMoveEffect.PainSplit: case PBEMoveEffect.Protect: case PBEMoveEffect.PsychUp: case PBEMoveEffect.Psywave: case PBEMoveEffect.RaiseUser_ATK_ACC_By1: case PBEMoveEffect.RaiseUser_ATK_DEF_By1: case PBEMoveEffect.RaiseUser_ATK_DEF_ACC_By1: case PBEMoveEffect.RaiseUser_ATK_SPATK_By1: case PBEMoveEffect.RaiseUser_ATK_SPE_By1: case PBEMoveEffect.RaiseUser_DEF_SPDEF_By1: case PBEMoveEffect.RaiseUser_SPATK_SPDEF_By1: case PBEMoveEffect.RaiseUser_SPATK_SPDEF_SPE_By1: case PBEMoveEffect.RaiseUser_SPE_By2_ATK_By1: case PBEMoveEffect.Reflect: case PBEMoveEffect.SeismicToss: case PBEMoveEffect.Selfdestruct: case PBEMoveEffect.SetDamage: case PBEMoveEffect.Snore: case PBEMoveEffect.Spikes: case PBEMoveEffect.StealthRock: case PBEMoveEffect.Substitute: case PBEMoveEffect.SuperFang: case PBEMoveEffect.Swagger: case PBEMoveEffect.ToxicSpikes: case PBEMoveEffect.Transform: case PBEMoveEffect.TrickRoom: case PBEMoveEffect.Whirlwind: case PBEMoveEffect.WideGuard: { // TODO Moves break; } } var mAction = new PBEAction { Decision = PBEDecision.Fight, FightMove = move, FightTargets = possibleTarget }; possibleActions.Add((mAction, score)); } } if (pkmn.CanSwitchOut()) { for (int s = 0; s < availableForSwitch.Length; s++) // Score switches { PBEPokemon switchPkmn = availableForSwitch[s]; // TODO: Entry hazards // TODO: Known moves of active battlers // TODO: Type effectiveness double score = 0.0; var sAction = new PBEAction { Decision = PBEDecision.SwitchOut, SwitchPokemonId = switchPkmn.Id }; possibleActions.Add((sAction, score)); } } string ToDebugString((PBEAction Action, double Score) t) { string str = "{"; if (t.Action.Decision == PBEDecision.Fight) { str += string.Format("Fight {0} {1}", t.Action.FightMove, t.Action.FightTargets); } else { str += string.Format("Switch {0}", team.TryGetPokemon(t.Action.SwitchPokemonId).Nickname); } str += " [" + t.Score + "]}"; return(str); } IOrderedEnumerable <(PBEAction Action, double Score)> byScore = possibleActions.OrderByDescending(t => t.Score); Debug.WriteLine("{0}'s possible actions: {1}", pkmn.Nickname, byScore.Select(t => ToDebugString(t)).Print()); double bestScore = byScore.First().Score; actions[i] = byScore.Where(t => t.Score == bestScore).ToArray().RandomElement().Action; // Pick random action of the ones that tied for best score } // Action was chosen, finish up for this Pokémon actions[i].PokemonId = pkmn.Id; if (actions[i].Decision == PBEDecision.SwitchOut) { standBy.Add(team.TryGetPokemon(actions[i].SwitchPokemonId)); } } return(actions); }